View Javadoc

1   // Copyright (C) 2001 - 2012 Philip Aston
2   // All rights reserved.
3   //
4   // This file is part of The Grinder software distribution. Refer to
5   // the file LICENSE which is part of The Grinder distribution for
6   // licensing details. The Grinder distribution is available on the
7   // Internet at http://grinder.sourceforge.net/
8   //
9   // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
12  // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
13  // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
14  // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15  // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
16  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
17  // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
18  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
19  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
20  // OF THE POSSIBILITY OF SUCH DAMAGE.
21  
22  package net.grinder.console.model;
23  
24  import java.awt.Rectangle;
25  import java.beans.PropertyChangeListener;
26  import java.beans.PropertyChangeSupport;
27  import java.io.File;
28  import java.net.InetAddress;
29  import java.net.UnknownHostException;
30  import java.util.NoSuchElementException;
31  import java.util.StringTokenizer;
32  import java.util.regex.Pattern;
33  import java.util.regex.PatternSyntaxException;
34  
35  import net.grinder.common.GrinderProperties;
36  import net.grinder.communication.CommunicationDefaults;
37  import net.grinder.console.common.ConsoleException;
38  import net.grinder.console.common.DisplayMessageConsoleException;
39  import net.grinder.console.common.Resources;
40  import net.grinder.util.Directory;
41  
42  
43  /**
44   * Class encapsulating the console options.
45   *
46   * <p>Adds fixed interface and listener mechanism, but delegates to
47   * {@link GrinderProperties} for storage.</p>
48   *
49   * <p>Implements the read-only part of {@code Map<String, Object>}.</p>
50   *
51   * @author Philip Aston
52   */
53  public final class ConsoleProperties {
54  
55    /** Property name. */
56    public static final String COLLECT_SAMPLES_PROPERTY =
57      "grinder.console.numberToCollect";
58  
59    /** Property name. */
60    public static final String IGNORE_SAMPLES_PROPERTY =
61      "grinder.console.numberToIgnore";
62  
63    /** Property name. */
64    public static final String SAMPLE_INTERVAL_PROPERTY =
65      "grinder.console.sampleInterval";
66  
67    /** Property name. */
68    public static final String SIG_FIG_PROPERTY =
69      "grinder.console.significantFigures";
70  
71    /** Property name. */
72    public static final String CONSOLE_HOST_PROPERTY =
73      "grinder.console.consoleHost";
74  
75    /** Property name. */
76    public static final String CONSOLE_PORT_PROPERTY =
77      "grinder.console.consolePort";
78  
79    /** Property name. */
80    public static final String HTTP_HOST_PROPERTY =
81      "grinder.console.httpHost";
82  
83    /** Property name. */
84    public static final String HTTP_PORT_PROPERTY =
85      "grinder.console.httpPort";
86  
87    /** Property name. */
88    public static final String RESET_CONSOLE_WITH_PROCESSES_PROPERTY =
89      "grinder.console.resetConsoleWithProcesses";
90  
91    /** Property name. */
92    public static final String RESET_CONSOLE_WITH_PROCESSES_ASK_PROPERTY =
93      "grinder.console.resetConsoleWithProcessesAsk";
94  
95    /** Property name. */
96    public static final String PROPERTIES_NOT_SET_ASK_PROPERTY =
97      "grinder.console.propertiesNotSetAsk";
98  
99    /** Property name. */
100   public static final String START_WITH_UNSAVED_BUFFERS_ASK_PROPERTY =
101     "grinder.console.startWithUnsavedBuffersAsk";
102 
103   /** Property name. */
104   public static final String STOP_PROCESSES_ASK_PROPERTY =
105     "grinder.console.stopProcessesAsk";
106 
107   /** Property name. */
108   public static final String DISTRIBUTE_ON_START_ASK_PROPERTY =
109     "grinder.console.distributeAutomaticallyAsk";
110 
111   /** Property name. */
112   public static final String PROPERTIES_FILE_PROPERTY =
113     "grinder.console.propertiesFile";
114 
115   /** Property name. */
116   public static final String DISTRIBUTION_DIRECTORY_PROPERTY =
117     "grinder.console.scriptDistributionDirectory";
118 
119   /** Property name. */
120   public static final String DISTRIBUTION_FILE_FILTER_EXPRESSION_PROPERTY =
121     "grinder.console.distributionFileFilterExpression";
122 
123   /**
124    * Default regular expression for filtering distribution files.
125    */
126   public static final String DEFAULT_DISTRIBUTION_FILE_FILTER_EXPRESSION =
127     "^CVS/$|" +
128     "^\\.svn/$|" +
129     "^.*~$|" +
130     "^(out_|error_|data_)\\w+-\\d+\\.log\\d*$";
131 
132   /** Property name. */
133   public static final String SCAN_DISTRIBUTION_FILES_PERIOD_PROPERTY =
134     "grinder.console.scanDistributionFilesPeriod";
135 
136   /** Property name. */
137   public static final String LOOK_AND_FEEL_PROPERTY =
138     "grinder.console.lookAndFeel";
139 
140   /** Property name. */
141   public static final String EXTERNAL_EDITOR_COMMAND_PROPERTY =
142     "grinder.console.externalEditorCommand";
143 
144   /** Property name. */
145   public static final String EXTERNAL_EDITOR_ARGUMENTS_PROPERTY =
146     "grinder.console.externalEditorArguments";
147 
148   /** Property name. */
149   public static final String FRAME_BOUNDS_PROPERTY =
150     "grinder.console.frameBounds";
151 
152   /** Property name. */
153   public static final String SAVE_TOTALS_WITH_RESULTS_PROPERTY =
154     "grinder.console.saveTotalsWithResults";
155 
156   private final PropertyChangeSupport m_changeSupport =
157     new PropertyChangeSupport(this);
158 
159   private final IntProperty m_collectSampleCount =
160     new IntProperty(COLLECT_SAMPLES_PROPERTY, 0);
161 
162   private final IntProperty m_ignoreSampleCount =
163     new IntProperty(IGNORE_SAMPLES_PROPERTY, 0);
164 
165   private final IntProperty m_sampleInterval =
166     new IntProperty(SAMPLE_INTERVAL_PROPERTY, 1000);
167 
168   private final IntProperty m_significantFigures =
169     new IntProperty(SIG_FIG_PROPERTY, 3);
170 
171   private final BooleanProperty m_resetConsoleWithProcesses =
172     new BooleanProperty(RESET_CONSOLE_WITH_PROCESSES_PROPERTY, false);
173 
174   private final FileProperty m_propertiesFile =
175     new FileProperty(PROPERTIES_FILE_PROPERTY);
176 
177   private final DirectoryProperty m_distributionDirectory =
178     new DirectoryProperty(DISTRIBUTION_DIRECTORY_PROPERTY);
179 
180   private final PatternProperty m_distributionFileFilterPattern =
181     new PatternProperty(
182       DISTRIBUTION_FILE_FILTER_EXPRESSION_PROPERTY,
183       DEFAULT_DISTRIBUTION_FILE_FILTER_EXPRESSION);
184 
185   private final IntProperty m_scanDistributionFilesPeriod =
186     new IntProperty(SCAN_DISTRIBUTION_FILES_PERIOD_PROPERTY, 6000);
187 
188   private final StringProperty m_lookAndFeel =
189     new StringProperty(LOOK_AND_FEEL_PROPERTY, null);
190 
191   private final FileProperty m_externalEditorCommand =
192     new FileProperty(EXTERNAL_EDITOR_COMMAND_PROPERTY);
193 
194   private final StringProperty m_externalEditorArguments =
195     new StringProperty(EXTERNAL_EDITOR_ARGUMENTS_PROPERTY, null);
196 
197   private final RectangleProperty m_frameBounds =
198     new RectangleProperty(FRAME_BOUNDS_PROPERTY);
199 
200   private final BooleanProperty m_resetConsoleWithProcessesAsk =
201     new BooleanProperty(RESET_CONSOLE_WITH_PROCESSES_ASK_PROPERTY, true);
202 
203   private final BooleanProperty m_propertiesNotSetAsk =
204     new BooleanProperty(PROPERTIES_NOT_SET_ASK_PROPERTY, true);
205 
206   private final BooleanProperty m_startWithUnsavedBuffersAsk =
207     new BooleanProperty(START_WITH_UNSAVED_BUFFERS_ASK_PROPERTY, true);
208 
209   private final BooleanProperty m_stopProcessesAsk =
210     new BooleanProperty(STOP_PROCESSES_ASK_PROPERTY, true);
211 
212   private final BooleanProperty m_distributeOnStartAsk =
213     new BooleanProperty(DISTRIBUTE_ON_START_ASK_PROPERTY, true);
214 
215   private final StringProperty m_consoleHost =
216     new StringProperty(CONSOLE_HOST_PROPERTY,
217                        CommunicationDefaults.CONSOLE_HOST);
218 
219   private final IntProperty m_consolePort =
220     new IntProperty(CONSOLE_PORT_PROPERTY, CommunicationDefaults.CONSOLE_PORT);
221 
222   private final StringProperty m_httpHost =
223       new StringProperty(HTTP_HOST_PROPERTY,
224                          CommunicationDefaults.CONSOLE_HOST);
225 
226   private final IntProperty m_httpPort =
227       new IntProperty(HTTP_PORT_PROPERTY, 6373);
228 
229   private final BooleanProperty m_saveTotalsWithResults =
230     new BooleanProperty(SAVE_TOTALS_WITH_RESULTS_PROPERTY, false);
231 
232   private final Resources m_resources;
233 
234   /**
235    * We delegate to GrinderProperties for storage and the backing file.
236    */
237   private final GrinderProperties m_backingProperties;
238 
239   /**
240    * Construct a ConsoleProperties backed by the given file.
241    *
242    * @param resources Console resources.
243    * @param file The properties file.
244    * @throws ConsoleException If the properties file
245    * cannot be read or the properties file contains invalid data.
246    *
247    */
248   public ConsoleProperties(final Resources resources, final File file)
249     throws ConsoleException {
250 
251     m_resources = resources;
252 
253     try {
254       m_backingProperties = new GrinderProperties(file);
255     }
256     catch (final GrinderProperties.PersistenceException e) {
257       throw new DisplayMessageConsoleException(
258         m_resources, "couldNotLoadOptionsError.text", e);
259     }
260   }
261 
262   /**
263    * Copy constructor. Does not copy property change listeners.
264    *
265    * @param properties The properties to copy.
266    */
267   public ConsoleProperties(final ConsoleProperties properties) {
268     m_resources = properties.m_resources;
269     m_backingProperties = new GrinderProperties();
270     m_backingProperties.setAssociatedFile(
271       properties.m_backingProperties.getAssociatedFile());
272     set(properties);
273   }
274 
275   /**
276    * Assignment. Does not copy property change listeners, nor the
277    * associated file.
278    *
279    * @param properties The properties to copy.
280    */
281   public void set(final ConsoleProperties properties) {
282     m_backingProperties.clear();
283     m_backingProperties.putAll(properties.m_backingProperties);
284   }
285 
286   /**
287    * Add a {@code PropertyChangeListener}.
288    *
289    * @param listener The listener.
290    */
291   public void addPropertyChangeListener(final PropertyChangeListener listener) {
292     m_changeSupport.addPropertyChangeListener(listener);
293   }
294 
295   /**
296    * Add a {@code PropertyChangeListener} which listens to a particular
297    * property.
298    *
299    * @param property
300    *          The property.
301    * @param listener
302    *          The listener.
303    */
304   public void addPropertyChangeListener(
305     final String property, final PropertyChangeListener listener) {
306     m_changeSupport.addPropertyChangeListener(property, listener);
307   }
308 
309   /**
310    * Save to the associated file.
311    *
312    * @throws ConsoleException If an error occurs.
313    */
314   public void save() throws ConsoleException {
315     try {
316       m_backingProperties.save();
317     }
318     catch (final GrinderProperties.PersistenceException e) {
319       throw new DisplayMessageConsoleException(
320           m_resources, "couldNotSaveOptionsError.text", e);
321     }
322   }
323 
324   /**
325    * Get the number of samples to collect.
326    *
327    * @return The number.
328    */
329   public int getCollectSampleCount() {
330     return m_collectSampleCount.get();
331   }
332 
333   /**
334    * Set the number of samples to collect.
335    *
336    * @param n The number. 0 => forever.
337    * @throws ConsoleException If the number is negative.
338    */
339   public void setCollectSampleCount(final int n) throws ConsoleException {
340     if (n < 0) {
341       throw new DisplayMessageConsoleException(
342         m_resources, "collectNegativeError.text");
343     }
344 
345     m_collectSampleCount.set(n);
346   }
347 
348   /**
349    * Get the number of samples to ignore.
350    *
351    * @return The number.
352    */
353   public int getIgnoreSampleCount() {
354     return m_ignoreSampleCount.get();
355   }
356 
357   /**
358    * Set the number of samples to collect.
359    *
360    * @param n The number. Must be positive.
361    * @throws ConsoleException If the number is negative or zero.
362    */
363   public void setIgnoreSampleCount(final int n) throws ConsoleException {
364     if (n < 0) {
365       throw new DisplayMessageConsoleException(
366         m_resources, "ignoreSamplesNegativeError.text");
367     }
368 
369     m_ignoreSampleCount.set(n);
370   }
371 
372   /**
373    * Get the sample interval.
374    *
375    * @return The interval in milliseconds.
376    */
377   public int getSampleInterval() {
378     return m_sampleInterval.get();
379   }
380 
381   /**
382    * Set the sample interval.
383    *
384    * @param interval The interval in milliseconds.
385    * @throws ConsoleException If the number is negative or zero.
386    */
387   public void setSampleInterval(final int interval) throws ConsoleException {
388     if (interval <= 0) {
389       throw new DisplayMessageConsoleException(
390         m_resources, "intervalLessThanOneError.text");
391     }
392 
393     m_sampleInterval.set(interval);
394   }
395 
396   /**
397    * Get the number of significant figures.
398    *
399    * @return The number of significant figures.
400    */
401   public int getSignificantFigures() {
402     return m_significantFigures.get();
403   }
404 
405   /**
406    * Set the number of significant figures.
407    *
408    * @param n The number of significant figures.
409    * @throws ConsoleException If the number is negative.
410    */
411   public void setSignificantFigures(final int n) throws ConsoleException {
412     if (n <= 0) {
413       throw new DisplayMessageConsoleException(
414         m_resources, "significantFiguresNegativeError.text");
415     }
416 
417     m_significantFigures.set(n);
418   }
419 
420   /**
421    * Get the console host as a string.
422    *
423    * @return The address.
424    */
425   public String getConsoleHost() {
426     return m_consoleHost.get();
427   }
428 
429   /**
430    * Validate a unicast IP address.
431    *
432    * <p>
433    * We treat any address that we can look up as valid. I guess we could also
434    * try binding to it to discover whether it is local, but that could take an
435    * indeterminate amount of time.
436    * </p>
437    *
438    * @param address The address, as a string.
439    * @throws ConsoleException If the address is invalid.
440    */
441   private void checkAddress(final String address) throws ConsoleException {
442     if (address.length() > 0) {    // Empty string => all local hosts.
443       final InetAddress newAddress;
444 
445       try {
446         newAddress = InetAddress.getByName(address);
447       }
448       catch (final UnknownHostException e) {
449         throw new DisplayMessageConsoleException(
450           m_resources, "unknownHostError.text");
451       }
452 
453       if (newAddress.isMulticastAddress()) {
454         throw new DisplayMessageConsoleException(
455           m_resources, "invalidHostAddressError.text");
456       }
457     }
458   }
459 
460   /**
461    * Validate a TCP port.
462    *
463    ** @param port The port.
464    * @throws ConsoleException If the port is invalid.
465    */
466   private void checkPort(final int port) throws ConsoleException {
467 
468     if (port < CommunicationDefaults.MIN_PORT ||
469         port > CommunicationDefaults.MAX_PORT) {
470       throw new DisplayMessageConsoleException(
471         m_resources,
472         "invalidPortNumberError.text",
473         new Object[] {
474           CommunicationDefaults.MIN_PORT,
475           CommunicationDefaults.MAX_PORT, }
476         );
477     }
478   }
479 
480   /**
481    * Set the console host.
482    *
483    * @param s Either a machine name or the IP address.
484    * @throws ConsoleException If the address is not
485    * valid.
486    */
487   public void setConsoleHost(final String s) throws ConsoleException {
488     checkAddress(s);
489     m_consoleHost.set(s);
490   }
491 
492   /**
493    * Get the console port.
494    *
495    * @return The port.
496    */
497   public int getConsolePort() {
498     return m_consolePort.get();
499   }
500 
501   /**
502    * Set the console port.
503    *
504    * @param i The port number.
505    * @throws ConsoleException If the port number is not sensible.
506    */
507   public void setConsolePort(final int i) throws ConsoleException {
508     checkPort(i);
509     m_consolePort.set(i);
510   }
511 
512   /**
513    * Get the HTTP host as a string.
514    *
515    * @return The address.
516    */
517   public String getHttpHost() {
518     return m_httpHost.get();
519   }
520 
521   /**
522    * Set the HTTP host.
523    *
524    * @param s Either a machine name or the IP address.
525    * @throws ConsoleException If the address is not
526    * valid.
527    */
528   public void setHttpHost(final String s) throws ConsoleException {
529     checkAddress(s);
530     m_httpHost.set(s);
531   }
532 
533   /**
534    * Get the HTTP port.
535    *
536    * @return The port.
537    */
538   public int getHttpPort() {
539     return m_httpPort.get();
540   }
541 
542   /**
543    * Set the HTTP port.
544    *
545    * @param i The port number.
546    * @throws ConsoleException If the port number is not sensible.
547    */
548   public void setHttpPort(final int i) throws ConsoleException {
549     checkPort(i);
550     m_httpPort.set(i);
551   }
552 
553   /**
554    * Get whether the console should be reset with the worker
555    * processes.
556    *
557    * @return {@code true} => the console should be reset with the
558    * worker processes.
559    */
560   public boolean getResetConsoleWithProcesses() {
561     return m_resetConsoleWithProcesses.get();
562   }
563 
564   /**
565    * Set whether the console should be reset with the worker
566    * processes.
567    *
568    * @param b {@code true} => the console should be reset with
569    * the worker processes.
570    */
571   public void setResetConsoleWithProcesses(final boolean b) {
572     m_resetConsoleWithProcesses.set(b);
573   }
574 
575   /**
576    * Get whether the user wants to be asked if console should be reset
577    * with the worker processes.
578    *
579    * @return {@code true} => the user wants to be asked.
580    */
581   public boolean getResetConsoleWithProcessesAsk() {
582     return m_resetConsoleWithProcessesAsk.get();
583   }
584 
585   /**
586    * Set and save whether the user wants to be asked if console should be reset
587    * with the worker processes.
588    *
589    * @param value
590    *          {@code true} => the user wants to be asked.
591    * @throws ConsoleException
592    *            If the property couldn't be persisted
593    */
594   public void setResetConsoleWithProcessesAsk(final boolean value)
595     throws ConsoleException {
596     m_resetConsoleWithProcessesAsk.set(value);
597     m_resetConsoleWithProcessesAsk.save();
598   }
599 
600   /**
601    * Get whether the user wants to be asked if console should be reset
602    * with the worker processes.
603    *
604    * @return {@code true} => the user wants to be asked.
605    */
606   public boolean getPropertiesNotSetAsk() {
607     return m_propertiesNotSetAsk.get();
608   }
609 
610   /**
611    * Set and save whether the user wants to be asked if console should be reset
612    * with the worker processes.
613    *
614    * @param value
615    *          {@code true} => the user wants to be asked.
616    * @throws ConsoleException
617    *           If the property couldn't be persisted.
618    */
619   public void setPropertiesNotSetAsk(final boolean value)
620       throws ConsoleException {
621     m_propertiesNotSetAsk.set(value);
622     m_propertiesNotSetAsk.save();
623   }
624 
625 
626   /**
627    * Get whether the user wants to be warned when starting processes with
628    * unsaved buffers.
629    *
630    * @return {@code true} => the user wants to be warned.
631    */
632   public boolean getStartWithUnsavedBuffersAsk() {
633     return m_startWithUnsavedBuffersAsk.get();
634   }
635 
636   /**
637    * Set and save whether the user wants to be warned when starting processes
638    * with unsaved buffers.
639    *
640    * @param value
641    *          {@code true} => the user wants to be warned.
642    * @throws ConsoleException
643    *           If the property couldn't be persisted.
644    */
645   public void setStartWithUnsavedBuffersAsk(final boolean value)
646     throws ConsoleException {
647     m_startWithUnsavedBuffersAsk.set(value);
648     m_startWithUnsavedBuffersAsk.save();
649   }
650 
651   /**
652    * Get whether the user wants to be asked to confirm that processes
653    * should be stopped.
654    *
655    * @return {@code true} => the user wants to be asked.
656    */
657   public boolean getStopProcessesAsk() {
658     return m_stopProcessesAsk.get();
659   }
660 
661   /**
662    * Set and save whether the user wants to be asked to confirm that processes
663    * should be stopped.
664    *
665    * @param value
666    *          {@code true} => the user wants to be asked.
667    * @throws ConsoleException If the property couldn't be persisted.
668    */
669   public void setStopProcessesAsk(final boolean value)
670     throws ConsoleException {
671     m_stopProcessesAsk.set(value);
672     m_stopProcessesAsk.save();
673   }
674 
675   /**
676    * Get whether the user wants to distribute files automatically when starting
677    * processes.
678    *
679    * @return {@code true} => the user wants automatic distribution.
680    */
681   public boolean getDistributeOnStartAsk() {
682     return m_distributeOnStartAsk.get();
683   }
684 
685   /**
686    * Set and save whether the user wants to distribute files automatically when
687    * starting processes.
688    *
689    * @param value
690    *          {@code true} => the user wants automatic distribution.
691    * @throws ConsoleException If the property couldn't be persisted.
692    */
693   public void setDistributeOnStartAsk(final boolean value)
694     throws ConsoleException {
695     m_distributeOnStartAsk.set(value);
696     m_distributeOnStartAsk.save();
697   }
698 
699   /**
700    * Get the selected properties file.
701    *
702    * @return The properties file. {@code null} => No file selected.
703    */
704   public File getPropertiesFile() {
705     return m_propertiesFile.get();
706   }
707 
708 
709   /**
710    * Set and save the selected properties file.
711    *
712    * @param propertiesFile
713    *          The properties file. {@code null} => No file selected.
714    */
715   public void setPropertiesFile(final File propertiesFile) {
716     m_propertiesFile.set(propertiesFile);
717   }
718 
719   /**
720    * Set and save the properties file.
721    *
722    * @param propertiesFile The properties file. {@code null} => No file
723    * set.
724    * @throws ConsoleException
725    * @throws ConsoleException If the property could not be saved.
726    */
727   public void setAndSavePropertiesFile(final File propertiesFile)
728     throws ConsoleException {
729     setPropertiesFile(propertiesFile);
730     m_propertiesFile.save();
731   }
732 
733   /**
734    * Get the script distribution directory.
735    *
736    * @return The directory.
737    */
738   public Directory getDistributionDirectory() {
739     return m_distributionDirectory.get();
740   }
741 
742   /**
743    * Set and save the script distribution directory.
744    *
745    * @param distributionDirectory The directory.
746    */
747   public void setDistributionDirectory(final Directory distributionDirectory) {
748     m_distributionDirectory.set(distributionDirectory);
749   }
750 
751   /**
752    * Set and save the script distribution directory.
753    *
754    * @param distributionDirectory The directory.
755    * @throws ConsoleException If the property could not be saved.
756    */
757   public void setAndSaveDistributionDirectory(
758     final Directory distributionDirectory) throws ConsoleException {
759     setDistributionDirectory(distributionDirectory);
760     m_distributionDirectory.save();
761   }
762 
763   /**
764    * Get the distribution file filter pattern.
765    *
766    * <p>The original regular expression can be obtained with
767    * {@link #getDistributionFileFilterPattern()}.</p>
768    *
769    * @return The pattern.
770    * @see #setDistributionFileFilterExpression
771    */
772   public Pattern getDistributionFileFilterPattern() {
773     return m_distributionFileFilterPattern.get();
774   }
775 
776   /**
777    * Get the distribution file filter pattern.
778    *
779    * <p>The original regular expression can be obtained with
780    * {@code getDistributionFileFilterPattern().getPattern()}.</p>
781    *
782    * @return The pattern.
783    * @see #setDistributionFileFilterExpression
784    */
785   public String getDistributionFileFilterExpression() {
786     return getDistributionFileFilterPattern().pattern();
787   }
788 
789   /**
790    * Set the distribution file filter regular expression.
791    *
792    * <p>Files and directory names (not full paths) that match the
793    * regular expression are not distributed. Directory names are
794    * distinguished by a trailing '/'. The expression is in Perl 5
795    * format.</p>
796    *
797    * @param expression A Perl 5 format expression. {@code null}
798    * => use default pattern.
799    * @throws ConsoleException If the pattern is invalid.
800    */
801   public void setDistributionFileFilterExpression(final String expression)
802     throws ConsoleException {
803     m_distributionFileFilterPattern.setExpression(expression);
804   }
805 
806   /**
807    * Get the period at which the distribution files should be scanned.
808    *
809    * @return The period, in milliseconds.
810    */
811   public int getScanDistributionFilesPeriod() {
812     return m_scanDistributionFilesPeriod.get();
813   }
814 
815   /**
816    * Set the period at which the distribution files should be scanned.
817    *
818    * @param i The port number.
819    * @throws ConsoleException If the period is negative.
820    */
821   public void setScanDistributionFilesPeriod(final int i)
822       throws ConsoleException {
823     if (i < 0) {
824       throw new DisplayMessageConsoleException(
825         m_resources, "scanDistributionFilesPeriodNegativeError.text");
826     }
827 
828     m_scanDistributionFilesPeriod.set(i);
829   }
830 
831   /**
832    * Get the name of the Look and Feel. It is up to the UI
833    * implementation how this is interpreted.
834    *
835    * @return The Look and Feel name. {@code null} => use default.
836    */
837   public String getLookAndFeel() {
838     return m_lookAndFeel.get();
839   }
840 
841   /**
842    * Set the name of the Look and Feel.
843    *
844    * @param lookAndFeel The Look and Feel name. {@code null} =>
845    * use default.
846    */
847   public void setLookAndFeel(final String lookAndFeel) {
848     m_lookAndFeel.set(lookAndFeel);
849   }
850 
851   /**
852    * Get the external editor command.
853    *
854    * @return The path to the process to be used for external editing.
855    * {@code null} => no external editor set.
856    */
857   public File getExternalEditorCommand() {
858     return m_externalEditorCommand.get();
859   }
860 
861   /**
862    * Set the external editor command.
863    *
864    * @param command The path to the process to be used for external editing.
865    * {@code null} => no external editor set.
866    */
867   public void setExternalEditorCommand(final File command) {
868     m_externalEditorCommand.set(command);
869   }
870 
871   /**
872    * Get the external editor arguments.
873    *
874    * @return The arguments to be used with the external editor.
875    */
876   public String getExternalEditorArguments() {
877     return m_externalEditorArguments.get();
878   }
879 
880   /**
881    * Set the external editor arguments.
882    *
883    * @param arguments The arguments to be used with the external editor.
884    */
885   public void setExternalEditorArguments(final String arguments) {
886     m_externalEditorArguments.set(arguments);
887   }
888 
889   /**
890    * Get the location and size of the console frame.
891    *
892    * @return The console frame bounds.
893    */
894   public Rectangle getFrameBounds() {
895     return m_frameBounds.get();
896   }
897 
898   /**
899    * Set and save the location and size of the console frame.
900    *
901    * @param bounds The console frame bounds.
902    */
903   public void setFrameBounds(final Rectangle bounds) {
904     m_frameBounds.set(bounds);
905   }
906 
907   /**
908    * Set and save the location and size of the console frame.
909    *
910    * @param bounds The console frame bounds.
911    * @throws ConsoleException If the property couldn't be persisted.
912    */
913   public void setAndSaveFrameBounds(final Rectangle bounds)
914       throws ConsoleException {
915     setFrameBounds(bounds);
916     m_frameBounds.save();
917   }
918 
919   /**
920    * Get whether saved results files should include the Totals line.
921    *
922    * @return {@code true} => results files should include totals.
923    */
924   public boolean getSaveTotalsWithResults() {
925     return m_saveTotalsWithResults.get();
926   }
927 
928   /**
929    * Set whether saved results files should include the Totals line.
930    *
931    * @param b {@code true} => results files should include totals.
932    * @throws ConsoleException If the property couldn't be persisted.
933    */
934   public void setSaveTotalsWithResults(final boolean b)
935       throws ConsoleException {
936     m_saveTotalsWithResults.set(b);
937     m_saveTotalsWithResults.save();
938   }
939 
940   private abstract class Property<T> {
941     private final String m_propertyName;
942     private final T m_defaultValue;
943 
944     Property(final String propertyName, final T defaultValue) {
945       m_propertyName = propertyName;
946       m_defaultValue = defaultValue;
947     }
948 
949     public final void save() throws ConsoleException {
950       try {
951         m_backingProperties.saveSingleProperty(m_propertyName);
952       }
953       catch (final GrinderProperties.PersistenceException e) {
954         throw new DisplayMessageConsoleException(
955           m_resources, "couldNotSaveOptionsError.text", e);
956       }
957     }
958 
959     protected final String getPropertyName() {
960       return m_propertyName;
961     }
962 
963     protected final T getDefaultValue() {
964       return m_defaultValue;
965     }
966 
967     protected abstract T get();
968 
969     protected abstract void setToStorage(T value);
970 
971     public final void set(final T value) {
972       final T old = get();
973 
974       final T defaultValue = getDefaultValue();
975 
976       if (defaultValue == null && value == null ||
977           defaultValue != null && defaultValue.equals(value)) {
978         m_backingProperties.remove(m_propertyName);
979       }
980       else {
981         setToStorage(value);
982       }
983 
984       // For some reason, firePropertyChange only suppresses same value
985       // updates when the value is not null. The default L&F is null,
986       // so this prevents UI flicker on each property change.
987       if (old == null && value == null) {
988         return;
989       }
990 
991       m_changeSupport.firePropertyChange(getPropertyName(), old, value);
992     }
993   }
994 
995   private final class StringProperty extends Property<String> {
996     public StringProperty(final String propertyName,
997                           final String defaultValue) {
998       super(propertyName, defaultValue);
999     }
1000 
1001     @Override
1002     protected String get() {
1003       return m_backingProperties.getProperty(getPropertyName(),
1004                                              getDefaultValue());
1005     }
1006 
1007     @Override
1008     protected void setToStorage(final String value) {
1009       m_backingProperties.setProperty(getPropertyName(), value);
1010     }
1011   }
1012 
1013   private final class PatternProperty extends Property<Pattern> {
1014     public PatternProperty(final String propertyName,
1015                            final String defaultExpression) {
1016       super(propertyName, Pattern.compile(defaultExpression));
1017     }
1018 
1019     @Override
1020     protected Pattern get() {
1021       final String expression =
1022         m_backingProperties.getProperty(getPropertyName());
1023 
1024       if (expression != null) {
1025         try {
1026           return Pattern.compile(expression);
1027         }
1028         catch (final PatternSyntaxException e) {
1029           // Fall through.
1030         }
1031       }
1032 
1033       return getDefaultValue();
1034     }
1035 
1036     @Override
1037     protected void setToStorage(final Pattern value) {
1038       m_backingProperties.put(getPropertyName(), value.pattern());
1039     }
1040 
1041     public void setExpression(final String expression) throws ConsoleException {
1042       if (expression == null) {
1043         m_backingProperties.remove(getPropertyName());
1044       }
1045       else {
1046         try {
1047           set(Pattern.compile(expression));
1048         }
1049         catch (final PatternSyntaxException e) {
1050           throw new DisplayMessageConsoleException(
1051               m_resources,
1052               "regularExpressionError.text",
1053               new Object[] { getPropertyName(), },
1054               e);
1055         }
1056       }
1057     }
1058   }
1059 
1060   private final class IntProperty extends Property<Integer> {
1061     public IntProperty(final String propertyName, final int defaultValue) {
1062       super(propertyName, defaultValue);
1063     }
1064 
1065 
1066     @Override
1067     protected Integer get() {
1068       return m_backingProperties.getInt(getPropertyName(), getDefaultValue());
1069     }
1070 
1071     @Override
1072     protected void setToStorage(final Integer value) {
1073       m_backingProperties.setInt(getPropertyName(), value);
1074     }
1075   }
1076 
1077   private final class FileProperty extends Property<File> {
1078     public FileProperty(final String propertyName) {
1079       super(propertyName, null);
1080     }
1081 
1082     @Override
1083     protected File get() {
1084       return m_backingProperties.getFile(getPropertyName(), getDefaultValue());
1085     }
1086 
1087     @Override
1088     protected void setToStorage(final File value) {
1089       m_backingProperties.setFile(getPropertyName(), value);
1090     }
1091   }
1092 
1093   private final class DirectoryProperty extends Property<Directory> {
1094     public DirectoryProperty(final String propertyName) {
1095       super(propertyName, new Directory());
1096     }
1097 
1098     @Override
1099     protected Directory get() {
1100       final File f = m_backingProperties.getFile(getPropertyName(), null);
1101 
1102       if (f != null) {
1103         try {
1104           return new Directory(f);
1105         }
1106         catch (final Directory.DirectoryException e) {
1107           // fall through.
1108         }
1109       }
1110 
1111       return new Directory();
1112     }
1113 
1114     @Override
1115     protected void setToStorage(final Directory value) {
1116       m_backingProperties.setFile(getPropertyName(), value.getFile());
1117     }
1118   }
1119 
1120   private final class BooleanProperty extends Property<Boolean> {
1121     public BooleanProperty(final String propertyName,
1122                            final boolean defaultValue) {
1123       super(propertyName, defaultValue);
1124     }
1125 
1126     @Override
1127     protected Boolean get() {
1128       return m_backingProperties.getBoolean(getPropertyName(),
1129                                             getDefaultValue());
1130     }
1131 
1132     @Override
1133     protected void setToStorage(final Boolean value) {
1134       m_backingProperties.setBoolean(getPropertyName(), value);
1135     }
1136   }
1137 
1138   private final class RectangleProperty extends Property<Rectangle> {
1139     public RectangleProperty(final String propertyName) {
1140       super(propertyName, null);
1141     }
1142 
1143     @Override
1144     public Rectangle get() {
1145       final String property =
1146         m_backingProperties.getProperty(getPropertyName(), null);
1147 
1148       if (property != null) {
1149         final StringTokenizer tokenizer = new StringTokenizer(property, ",");
1150 
1151         try {
1152           return new Rectangle(Integer.parseInt(tokenizer.nextToken()),
1153                                Integer.parseInt(tokenizer.nextToken()),
1154                                Integer.parseInt(tokenizer.nextToken()),
1155                                Integer.parseInt(tokenizer.nextToken()));
1156         }
1157         catch (final NoSuchElementException e) {
1158           // Ignore.
1159         }
1160         catch (final NumberFormatException e) {
1161           // Ignore.
1162         }
1163       }
1164 
1165       return getDefaultValue();
1166     }
1167 
1168     @Override
1169     public void setToStorage(final Rectangle value) {
1170       m_backingProperties.setProperty(
1171         getPropertyName(),
1172         value.x + "," + value.y + "," + value.width + "," + value.height);
1173     }
1174   }
1175 }