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.IOException;
21 import java.io.InputStream;
22 import java.text.ParseException;
23 import java.util.ArrayList;
24 import java.util.List;
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.StdinControllerSettings;
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.Duration;
36 import com.tomgibara.pronto.util.Waiter;
37
38 /**
39 * A ControllerPart implementation that provides functionality for the
40 * StdinControllerSettings interface.
41 *
42 * @param <S>
43 * the type of state
44 * @param <L>
45 * the type of label
46 * @param <P>
47 * the type of parameter
48 *
49 * @author Tom Gibara
50 *
51 */
52
53 public class StdinControllerPart<S, L, P> implements ControllerPart<S, L, P> {
54
55
56
57 private static int extractLine(final byte[] bytes, final int bytesRead, final List<String> lines) {
58 int i;
59 for (i = 0; i < bytesRead; i++) {
60 byte b = bytes[i];
61 if (b == '\n' || b == '\r') break;
62 }
63 final int bytesLeft;
64 if (i == bytes.length || i < bytesRead) {
65 String line = new String(bytes, 0, i);
66 lines.add(line);
67 if (i == bytes.length) {
68 bytesLeft = 0;
69 } else {
70 bytesLeft = bytesRead - i - 1;
71 if (bytesLeft > 0) {
72 System.arraycopy(bytes, i + 1, bytes, 0, bytesLeft);
73 }
74 }
75 } else {
76 bytesLeft = bytesRead;
77 }
78 return bytesLeft;
79 }
80
81
82 static int extractLines(final byte[] bytes, final int bytesRead, final List<String> lines) {
83 int bytesLeft = bytesRead;
84 while (true) {
85 if (bytesLeft == 0) break;
86 int tmp = extractLine(bytes, bytesLeft, lines);
87 if (tmp == bytesLeft) break;
88 bytesLeft = tmp;
89 }
90 return bytesLeft;
91 }
92
93 /**
94 * The greatest number of characters that will be read as a single line of
95 * input.
96 */
97 private static final int MAX_LINE_LENGTH = StaticProperties.getInt("pronto.control.stdin.max-line-length");
98
99 /**
100 * The length of time between successive polls of STDIN if no duration is
101 * specified by the settings.
102 */
103 private static final Duration DEFAULT_CHECK_PERIOD = StaticProperties
104 .getDuration("pronto.control.stdin.check-period");
105
106 /**
107 * A pattern for matching lines - the first group is expected to be the
108 * label, the second is expected to be the transition parameter.
109 */
110 private static final Pattern LINE_PATTERN = StaticProperties.getPattern("pronto.control.stdin.line-pattern");
111
112
113
114 /**
115 * The lock that must be held when performing any action that is dependent
116 * upon the object's lifespan.
117 */
118 private final Object lock = new Object();
119
120 /**
121 * The controller that aggregates this part.
122 */
123 private final ControllerImpl<S, L, P> controller;
124
125 /**
126 * The settings used to construct this part.
127 */
128 private final StdinControllerSettings settings;
129
130 /**
131 * A future that represents the repeated polling of STDIN.
132 */
133 private ScheduledFuture<?> future;
134
135 /**
136 * Stores the bytes that have been read from STDIN but not parsed into
137 * complete lines for execution.
138 */
139
140
141 private final byte[] bytes = new byte[MAX_LINE_LENGTH];
142
143 /**
144 * The number of valid bytes in the bytes buffer.
145 */
146 private int bytesRead = 0;
147
148 /**
149 * Indicates whether the object is currently checking the STDIN. This is
150 * used to confirm a successful shutdown when the part is stopped.
151 */
152 private boolean isChecking = false;
153
154
155
156 /**
157 * Creates a new StdinControllerPart.
158 *
159 * @param controller
160 * the controller that aggregates this part
161 * @param settings
162 * the settings for this part
163 */
164
165 public StdinControllerPart(final ControllerImpl<S, L, P> controller, final StdinControllerSettings settings) {
166 this.controller = controller;
167 this.settings = settings;
168 }
169
170
171
172 /**
173 * Uses the shared pronto executor to schedule periodic checks on STDIN.
174 */
175
176 public void start() {
177 synchronized (lock) {
178 if (future != null) return;
179
180
181 Pronto.getInstance().getUseLock().lock();
182
183
184 Duration duration = settings.getCheckPeriod();
185 if (duration == null) duration = DEFAULT_CHECK_PERIOD;
186 final long period = duration.getTime();
187 future = Pronto.getInstance().getExecutor().scheduleWithFixedDelay(new Runnable() {
188 public void run() {
189 synchronized (lock) {
190 isChecking = true;
191 try {
192 checkStdin();
193 } finally {
194 isChecking = false;
195 }
196 }
197 }
198 }, period, period, TimeUnit.MILLISECONDS);
199 }
200 }
201
202 /**
203 * Requests that the Pronto executor stops any future STDIN checks and then
204 * waits (subject to the supplied timeout) until the ceasation of any
205 * ongoing checking has been confirmed.
206 *
207 * @param timeout
208 * the (approx.) number of milliseconds for which a call to the
209 * method will block, or 0L.
210 * @return true if the part was able to confirm that all operation had
211 * ceased
212 */
213
214 public boolean stop(final long timeout) {
215 synchronized (lock) {
216 if (future == null) return true;
217
218 try {
219
220 future.cancel(true);
221 future = null;
222
223
224 return new Waiter(lock).waitForCondition(timeout, new Waiter.Condition() {
225 public boolean isMet() {
226 return !isChecking;
227 }
228 });
229 } finally {
230
231 Pronto.getInstance().getUseLock().unlock();
232 }
233 }
234 }
235
236 public ControllerImpl<S, L, P> getController() {
237 return controller;
238 }
239
240
241
242 /**
243 * This method is called by the pronto executor to checks STDIN for new
244 * input. Any residual bytes are left in the bytes field for next time
245 * this method is called.
246 */
247
248
249
250
251
252
253
254 private void checkStdin() {
255 final InputStream in = System.in;
256 try {
257 while (true) {
258
259 int available = in.available();
260
261
262
263 if (available > 0) {
264
265 int size = Math.min(available, MAX_LINE_LENGTH - bytesRead);
266 bytesRead += in.read(bytes, bytesRead, size);
267 } else {
268 break;
269 }
270
271
272 ArrayList<String> lines = new ArrayList<String>();
273 bytesRead = extractLines(bytes, bytesRead, lines);
274
275
276 for (String line : lines) {
277 processLine(line);
278 }
279 }
280 } catch (IOException e) {
281 ControlFactoryImpl.LOGGER.log(Level.WARNING, "IOException reading STDIN.", e);
282 }
283
284 }
285
286 private void processLine(final String line) {
287 if (line.length() == 0) return;
288
289
290 Matcher matcher = LINE_PATTERN.matcher(line);
291 if (!matcher.matches()) {
292 if (settings.isInteractive()) {
293 System.out.println("Bad input.");
294 } else {
295 ControlFactoryImpl.LOGGER.log(Level.INFO, "Bad line \"{0}\" on STDIN", line);
296 }
297 return;
298 }
299
300
301 String labelName = matcher.group(1);
302 final L label = controller.adapter.labelFromName(labelName);
303 if (label == null) {
304 if (settings.isInteractive()) {
305 System.out.println(String.format("Unrecognized label: %s", label));
306 } else {
307 ControlFactoryImpl.LOGGER.log(Level.INFO, "Unrecognized label \"{0}\" on STDIN", labelName);
308 }
309 return;
310 }
311
312
313 String paramStr = matcher.group(2);
314 final P param;
315 try {
316 param = controller.adapter.parseParameter(paramStr);
317 } catch (ParseException e) {
318 if (settings.isInteractive()) {
319 System.out.println(String.format("Unparsable parameter: %s", paramStr));
320 } else {
321 ControlFactoryImpl.LOGGER.log(Level.INFO, "Unparsable parameter \"{0}\" on STDIN", paramStr);
322 }
323 return;
324 }
325
326
327 try {
328 controller.engine.transition(null, label, param);
329 if (settings.isInteractive()) {
330 System.out.println("State changed.");
331 }
332 } catch (ProntoStateException e) {
333 if (settings.isInteractive()) {
334 System.out.println(String.format("State change failed: %s", e.getLocalizedMessage()));
335 } else {
336 ControlFactoryImpl.LOGGER
337 .log(Level.WARNING, "State transition failed in response to input on STDIN", e);
338 }
339 return;
340 }
341 }
342
343 }