View Javadoc

1   /*
2    * Copyright (C) 2006  Tom Gibara
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17   */
18  package com.tomgibara.pronto.control.impl;
19  
20  import java.io.BufferedReader;
21  import java.io.File;
22  import java.io.FileReader;
23  import java.io.IOException;
24  import java.text.ParseException;
25  import java.util.concurrent.ScheduledFuture;
26  import java.util.concurrent.TimeUnit;
27  import java.util.logging.Level;
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  
31  import com.tomgibara.pronto.control.FileControllerSettings;
32  import com.tomgibara.pronto.core.Pronto;
33  import com.tomgibara.pronto.core.impl.StaticProperties;
34  import com.tomgibara.pronto.state.ProntoStateException;
35  import com.tomgibara.pronto.util.CharStreams;
36  import com.tomgibara.pronto.util.Duration;
37  import com.tomgibara.pronto.util.Objects;
38  import com.tomgibara.pronto.util.Waiter;
39  
40  /**
41   * A ControllerPart implementation that provides functionality for the
42   * FileControllerSettings interface.
43   * 
44   * @param <S>
45   *            the type of state
46   * @param <L>
47   *            the type of label
48   * @param <P>
49   *            the type of parameter
50   * 
51   * @author Tom Gibara
52   * 
53   */
54  
55  public class FileControllerPart<S, L, P> implements ControllerPart<S, L, P> {
56  
57      /**
58       * A pattern for matching comment lines (which are ignored).
59       */
60      private static final Pattern COMMENT_PATTERN = StaticProperties.getPattern("pronto.control.file.comment-pattern");
61  
62      /**
63       * A pattern for matching lines - the first group is expected to be the
64       * label, the second is expected to be the transition parameter.
65       */
66      private static final Pattern LINE_PATTERN = StaticProperties.getPattern("pronto.control.file.line-pattern");
67  
68      /**
69       * The length of time between polls of the file if no duration is specified
70       * by the settings.
71       */
72      private static final Duration DEFAULT_CHECK_PERIOD = StaticProperties
73              .getDuration("pronto.control.file.check-period");
74  
75      /**
76       * The lock that must be held when performing any action that depends on the
77       * object's lifespan.
78       */
79      private final Object lock = new Object();
80  
81      /**
82       * The controller that aggregates this part.
83       */
84      private final ControllerImpl<S, L, P> controller;
85  
86      /**
87       * The settings used to construct this part.
88       */
89      private final FileControllerSettings settings;
90  
91      /**
92       * A future that represents the repeated polling of the control file.
93       */
94      private ScheduledFuture<?> future;
95  
96      /**
97       * The time at which the control file was last modified, or 0 if the file
98       * has not (or cannot) be accessed.
99       */
100     private long lastModified;
101 
102     /**
103      * The file to which the lastModified time belongs. This file may change due
104      * to changes in the settings object.
105      */
106     private File lastFile;
107 
108     /**
109      * Indicates whether the object is currently checking the control file. This
110      * is used to confirm a successful shutdown when the part is stopped.
111      */
112     private boolean isChecking = false;
113 
114     /**
115      * Creates a new FileControllerPart.
116      * 
117      * @param controller
118      *            the controller that aggregates this part
119      * @param settings
120      *            the settings for this part
121      */
122 
123     public FileControllerPart(final ControllerImpl<S, L, P> controller, final FileControllerSettings settings) {
124         this.controller = controller;
125         this.settings = settings;
126     }
127 
128     // controller part methods
129 
130     /**
131      * Uses the shared pronto executor to schedule periodic file checks.
132      */
133 
134     public void start() {
135         synchronized (lock) {
136             if (future != null) return;
137 
138             // initialize our state
139             lastFile = null;
140             lastModified = 0L;
141 
142             // obtain the lock which permits us to use the global resources
143             Pronto.getInstance().getUseLock().lock();
144 
145             // establish a periodicly executed runnable that will check the file
146             Duration duration = settings.getCheckPeriod();
147             if (duration == null) duration = DEFAULT_CHECK_PERIOD;
148             final long period = duration.getTime();
149             future = Pronto.getInstance().getExecutor().scheduleWithFixedDelay(new Runnable() {
150                 public void run() {
151                     checkFile();
152                 }
153             }, period, period, TimeUnit.MILLISECONDS);
154         }
155     }
156 
157     /**
158      * Requests that the Pronto executor stops any future file checks and then
159      * waits (subject to the supplied timeout) until the ceasation of any
160      * ongoing checking has been confirmed.
161      * 
162      * @param timeout
163      *            the (approx.) number of milliseconds for which a call to the
164      *            method will block, or 0L.
165      * @return true if the part was able to confirm that all operation had
166      *         ceased
167      */
168 
169     public boolean stop(final long timeout) {
170         synchronized (lock) {
171             if (future == null) return true;
172 
173             try {
174                 // stop the periodic file checking
175                 future.cancel(true);
176                 future = null;
177 
178                 // wait until any ongoing checking has ceased
179                 return new Waiter(lock).waitForCondition(timeout, new Waiter.Condition() {
180                     public boolean isMet() {
181                         return !isChecking;
182                     }
183                 });
184             } finally {
185                 // now release the pronto use-lock
186                 Pronto.getInstance().getUseLock().unlock();
187             }
188         }
189     }
190 
191     public ControllerImpl<S, L, P> getController() {
192         return controller;
193     }
194 
195     // private utility methods
196 
197     /**
198      * Causes the file currently specified by the part's settings to be checked
199      * for a change in its last-modified time. If the file is identified to have
200      * changed, then the lastModified field is updated, the file executed and
201      * (if specified by the settings) subsequently deleted.
202      */
203 
204     private void checkFile() {
205         synchronized (lock) {
206             isChecking = true;
207             try {
208                 long lm = accessLastModified();
209                 if (lastFile == null) return;
210                 boolean changed = settings.isOlderIgnored() ? lm > lastModified : lm != lastModified;
211                 if (!changed) return;
212                 lastModified = lm;
213                 BufferedReader in = null;
214                 try {
215                     in = new BufferedReader(new FileReader(lastFile));
216                     String line;
217                     while ((line = in.readLine()) != null) {
218                         line = line.trim();
219                         if (line.length() == 0) continue;
220                         if (COMMENT_PATTERN.matcher(line).matches()) continue;
221                         Matcher matcher = LINE_PATTERN.matcher(line);
222                         if (!matcher.matches()) {
223                             ControlFactoryImpl.LOGGER.log(Level.INFO, "Bad line \"{0}\" in file: {1}", new Object[] {
224                                     line, lastFile });
225                             continue;
226                         }
227                         String labelName = matcher.group(1);
228                         L label = controller.adapter.labelFromName(labelName);
229                         if (label == null) {
230                             ControlFactoryImpl.LOGGER.log(Level.INFO, "Unrecognized label \"{0}\" in file: {1}",
231                                     new Object[] { labelName, lastFile });
232                             continue;
233                         }
234                         String paramStr = matcher.group(2);
235                         P param;
236                         try {
237                             param = controller.adapter.parseParameter(paramStr);
238                         } catch (ParseException e) {
239                             ControlFactoryImpl.LOGGER.log(Level.INFO, "Unparsable parameter \"{0}\" in file: {1}",
240                                     new Object[] { paramStr, lastFile });
241                             continue;
242                         }
243 
244                         try {
245                             controller.engine.transition(null, label, param);
246                         } catch (ProntoStateException e) {
247                             ControlFactoryImpl.LOGGER.log(Level.INFO, String.format(
248                                     "State transition failed in response to input from file: %s", lastFile), e);
249                         }
250                     }
251                     in.close();
252                 } catch (IOException e) {
253                     ControlFactoryImpl.LOGGER.log(Level.WARNING, "IOException reading file.", e);
254                     CharStreams.safeClose(in);
255                 } finally {
256                     if (settings.isFileDeleted()) {
257                         lastFile.delete();
258                     }
259                 }
260             } finally {
261                 isChecking = false;
262             }
263         }
264     }
265 
266     /**
267      * Updates the lastModified and lastFile fields if the file returned by the
268      * settings has changed. The current last modified date of the most recent
269      * file is always returned.
270      * 
271      * @return the last-modified time of the file currently returned from the
272      *         settings object or, 0L if there is no file specified.
273      */
274 
275     private long accessLastModified() {
276         // first identify the file
277         File file = settings.getFile();
278         long lm = getLastModified(file);
279         if (Objects.notEqual(file, lastFile)) {
280             lastFile = file;
281             lastModified = settings.isPreexistingIgnored() ? lm : 0L;
282         }
283         return lm;
284     }
285 
286     /**
287      * Returns the current last-modified time of the supplied file as reported
288      * by the JVM.
289      * 
290      * @param file
291      *            the file whose last-modified time should be returned, or null
292      * @return the time at which the file was last modified, or 0L if the file
293      *         was inaccessible or null
294      */
295 
296     private long getLastModified(final File file) {
297         if (file == null) return 0L;
298         return file.lastModified();
299     }
300 
301 }