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.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      // statics
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      // package scoped for testing
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     // fields
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     // reserving a smaller array and then growing it might be more space
140     // efficient but the extra work doesn't seem justified
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     // constructors
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     // part methods
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             // obtain the lock which permits us to use the global resources
181             Pronto.getInstance().getUseLock().lock();
182 
183             // establish a periodicly executed runnable that will check the file
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                 // stop the periodic file checking
220                 future.cancel(true);
221                 future = null;
222 
223                 // wait until any ongoing checking has ceased
224                 return new Waiter(lock).waitForCondition(timeout, new Waiter.Condition() {
225                     public boolean isMet() {
226                         return !isChecking;
227                     }
228                 });
229             } finally {
230                 // release the pronto use-lock
231                 Pronto.getInstance().getUseLock().unlock();
232             }
233         }
234     }
235 
236     public ControllerImpl<S, L, P> getController() {
237         return controller;
238     }
239 
240     // private utility methods
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      * Cannot do the natural thing of spawning a thread, wrapping System.in a
249      * reader, blocking on the read and then interrupting to stop because
250      * reading on STDIN is not interruptible!! See #4514257 and #4385444
251      * 
252      * This approach assumes that 'good' values are returned from available().
253      */
254     private void checkStdin() {
255         final InputStream in = System.in;
256         try {
257             while (true) {
258                 // first try to read some more data
259                 int available = in.available();
260                 // assumes that nothing else is going to slurp these bytes
261                 // first, otherwise we will block - something we are working
262                 // very hard to avoid
263                 if (available > 0) {
264                     // byte[] buff = new byte[available];
265                     int size = Math.min(available, MAX_LINE_LENGTH - bytesRead);
266                     bytesRead += in.read(bytes, bytesRead, size);
267                 } else {
268                     break; // there's no more data - stop looping
269                 }
270 
271                 // now extract any lines from the data that was read
272                 ArrayList<String> lines = new ArrayList<String>();
273                 bytesRead = extractLines(bytes, bytesRead, lines);
274 
275                 // attempt to execute each line
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; // ignore empty lines
288 
289         // match string
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         // extract label
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         // extract param
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         // perform state change
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 }