1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
139 lastFile = null;
140 lastModified = 0L;
141
142
143 Pronto.getInstance().getUseLock().lock();
144
145
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
175 future.cancel(true);
176 future = null;
177
178
179 return new Waiter(lock).waitForCondition(timeout, new Waiter.Condition() {
180 public boolean isMet() {
181 return !isChecking;
182 }
183 });
184 } finally {
185
186 Pronto.getInstance().getUseLock().unlock();
187 }
188 }
189 }
190
191 public ControllerImpl<S, L, P> getController() {
192 return controller;
193 }
194
195
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
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 }