View Javadoc

1   package eteg.sinon.executor;
2   
3   import eteg.sinon.core.Action;
4   import eteg.sinon.core.Catalog;
5   import eteg.sinon.core.Collector;
6   import eteg.sinon.core.CollectorConfiguration;
7   import eteg.sinon.core.DataExtraction;
8   import eteg.sinon.core.LoopStep;
9   import eteg.sinon.core.Page;
10  import eteg.sinon.core.PageConfiguration;
11  import eteg.sinon.core.Parameter;
12  import eteg.sinon.core.ParameterSet;
13  import eteg.sinon.core.PositioningStep;
14  import eteg.sinon.core.StartPage;
15  import eteg.sinon.core.Step;
16  import eteg.sinon.exception.EvaluationException;
17  import eteg.sinon.exception.SinonException;
18  import eteg.sinon.exception.StepAfterLoopLimitPositionException;
19  import eteg.sinon.exception.StepException;
20  import eteg.sinon.exception.StepWithBeforeException;
21  import eteg.sinon.exception.StepWithFailIfFoundException;
22  import eteg.sinon.exception.UnknownPageException;
23  import eteg.sinon.listener.CatalogStateListener;
24  import eteg.sinon.listener.ErrorListener;
25  import eteg.sinon.listener.PageStateListener;
26  import org.apache.commons.httpclient.Cookie;
27  import org.apache.commons.httpclient.HttpClient;
28  import org.apache.commons.httpclient.HttpMethod;
29  import org.apache.commons.httpclient.URI;
30  import org.apache.commons.httpclient.methods.GetMethod;
31  import org.apache.commons.httpclient.methods.PostMethod;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.log4j.PropertyConfigurator;
35  import org.apache.velocity.VelocityContext;
36  import org.apache.velocity.app.VelocityEngine;
37  
38  import java.io.BufferedReader;
39  import java.io.File;
40  import java.io.FileNotFoundException;
41  import java.io.FileReader;
42  import java.io.FileWriter;
43  import java.io.IOException;
44  import java.io.InputStream;
45  import java.io.StringWriter;
46  import java.io.UnsupportedEncodingException;
47  import java.io.FileOutputStream;
48  import java.io.BufferedOutputStream;
49  import java.lang.reflect.Method;
50  import java.net.MalformedURLException;
51  import java.net.URLEncoder;
52  import java.net.UnknownHostException;
53  import java.text.DecimalFormat;
54  import java.util.ArrayList;
55  import java.util.Date;
56  import java.util.Enumeration;
57  import java.util.HashMap;
58  import java.util.Iterator;
59  import java.util.List;
60  import java.util.Map;
61  import java.util.Properties;
62  
63  /***
64   * Class that executes a {@link eteg.sinon.core.Collector} instance.
65   *
66   * @author <a href="mailto:thiagohp at users.sourceforge.net">Thiago H. de Paula
67   *         Figueiredo </a>
68   * @author Last modified by $Author: thiagohp $
69   * @version $Revision: 1.7 $
70   */
71  public class CollectorExecutor {
72  
73      final private static String HTTP_PREFIX = "http://";
74  
75      final private static String HTTPS_PREFIX = "https://";
76  
77      final private static String VELOCITY_PROPERTIES_PATH =
78              "/velocity.properties";
79  
80      final private static String COLLECTOR_PROPERTY = "collector";
81  
82      final private static PageState BEFORE_DOWNLOAD =
83              PageState.BEFORE_DOWNLOAD;
84  
85      final private static PageState BEFORE_PROCESSING =
86              PageState.BEFORE_PROCESSING;
87  
88      final private static PageState AFTER_PROCESSING =
89              PageState.AFTER_PROCESSING;
90  
91      final private static PageState FINISHED =
92              PageState.FINISHED;
93  
94      final private static String LOG4J_PROPERTIES_FILENAME = "log4j.properties";
95  
96      final private static String TEMPLATE_SUFFIX = "}";
97  
98      final private static String TEMPLATE_PREFIX = "${";
99  
100     /***
101      * Suffix used in URLs that point to local files.
102      */
103     final private static String FILE_PROTOCOL = "file:/";
104 
105     /***
106      * Current directory.
107      */
108     final private static File CURRENT_DIRECTORY = new File(".");
109 
110     /***
111      * Codificação padrão utilizada para se fazer <i>URL enconding </i> de
112      * parâmetros HTTP.
113      */
114     final private static String DEFAULT_URL_ENCODING = "ISO_8859_1";
115 
116     /***
117      * Sinon log filename.
118      */
119     final public static String LOG_FILENAME = "sinon.log";
120 
121     /***
122      * Velocity context.
123      */
124     private VelocityContext context;
125 
126     /***
127      * Velocity engine.
128      */
129     private VelocityEngine engine;
130 
131     /***
132      * Collector being executed.
133      */
134     private Collector collector;
135 
136     /***
137      * Think time instance used by this execution.
138      */
139     private ThinkTime thinkTime;
140 
141     /***
142      * HTTP headers.
143      */
144     private Properties httpHeaders;
145 
146     /***
147      * List containing the names of all properties set.
148      */
149     private ArrayList propertyNames;
150 
151     /***
152      * Hash table that maps multivalued properties (lists) to their
153      * values list.
154      */
155     private HashMap multivaluedProperties;
156 
157     /***
158      * Logger.
159      */
160     private Log log;
161 
162     /***
163      * Catalog instance being executed now.
164      */
165     private Catalog currentCatalog;
166 
167     /***
168      * Page instance being executed now.
169      */
170     private Page currentPage;
171 
172     /***
173      * Data extraction instance being executed now.
174      */
175     private DataExtraction currentDataExtraction;
176 
177     /***
178      * Execution step instance being executed now.
179      */
180     private Step currentStep;
181 
182     /***
183      * Current action text.
184      */
185     private String currentText;
186 
187     /***
188      * Current document bytes.
189      */
190     private byte[] currentBytes;
191 
192     /***
193      * Current position inside the text of the current action.
194      */
195     private int currentPosition;
196 
197     /***
198      * Position at which the desired information starts.
199      */
200     private int from;
201 
202     /***
203      * Position at which the desired information ends.
204      */
205     private int to;
206 
207     /***
208      * Number of downloaded pages..
209      */
210     private int pagesDownloaded;
211 
212     /***
213      * Number of transferred bytes.
214      */
215     private int bytesDownloaded;
216 
217     /***
218      * {@link ErrorListener} of the current action.
219      */
220     private ErrorListener currentErrorListener;
221 
222     /***
223      * {@link PageStateListener} of the current action.
224      */
225     private PageStateListener currentPageStateListener;
226 
227     /***
228      * <code>org.apache.commons.httpclient.HttpClient</code> instance.
229      */
230     private HttpClient httpClient;
231 
232     /***
233      * Action queue.
234      */
235     private ArrayList actionQueue;
236 
237     /***
238      * Current catalog state change listener.
239      */
240     private CatalogStateListener catalogStateListener;
241 
242     /***
243      * Execution start Unix timestamp.
244      */
245     private long start;
246 
247     /***
248      * Execution finish Unix timestamp.
249      */
250     private long finish;
251 
252     /***
253      * Flag used to stop an execution.
254      */
255     private boolean mustStop = false;
256 
257     /***
258      * Single constructor of this class.
259      *
260      * @param collector a {@link Collector} instance.
261      * @throws IllegalArgumentException  if <code>collector</code> is
262      * <code>null</code>.
263      */
264     public CollectorExecutor(Collector collector) {
265 
266         if (collector == null) {
267 
268             throw new IllegalArgumentException(
269                     "Parameter collector can't be null");
270 
271         }
272 
273         this.collector = collector;
274 
275         propertyNames = new ArrayList();
276         pagesDownloaded = 0;
277         bytesDownloaded = 0;
278         currentCatalog = null;
279         currentPage = null;
280         currentDataExtraction = null;
281         currentStep = null;
282         currentPosition = -1;
283         currentText = null;
284         actionQueue = new ArrayList();
285 
286         configureLogger();
287 
288     }
289 
290     /***
291      * Executes all catalogs of this collector in sequence.
292      *
293      * @throws SinonException if some error ocurrs.
294      */
295     public void execute() throws SinonException {
296         execute(collector.getCatalogs());
297     }
298 
299     /***
300      * Executes a list of catalogs of the coletor.
301      *
302      * @param catalogs a {@link Catalog} array..
303      * @throws SinonException if some unhandled error ocurrs.
304      * @todo i18n
305      */
306     public void execute(Catalog[] catalogs) throws SinonException {
307 
308         start = System.currentTimeMillis();
309         finish = -1;
310 
311         if (log.isInfoEnabled()) {
312             log.info("Starting execution of collector " + collector.getId());
313         }
314 
315         try {
316 
317             Catalog[] currentCatalogs = collector.getCatalogs();
318 
319             for (int i = 0; i < catalogs.length; i++) {
320 
321                 boolean ok = false;
322 
323                 for (int j = 0; j < currentCatalogs.length; j++) {
324 
325                     if (catalogs[i] == null) {
326 
327                         throw new IllegalArgumentException(
328                                 "No member of the catalogs vector can be null");
329 
330                     }
331 
332                     if (catalogs[i] == currentCatalogs[j]) {
333 
334                         ok = true;
335                         break;
336 
337                     }
338 
339                 }
340 
341                 if (ok == false) {
342 
343                     throw new SinonException("Catalog "
344                             + catalogs[i].getId()
345                             + " does not belong to this collector.", this);
346 
347                 }
348 
349             }
350 
351             engine = new VelocityEngine();
352             Properties props = new Properties();
353 
354             try {
355 
356                 props.load(this.getClass().getResourceAsStream(
357                         VELOCITY_PROPERTIES_PATH));
358             } catch (RuntimeException e1) {
359                 throw new SinonException("velocity.properties não found.");
360             }
361 
362             try {
363 
364                 engine.init(props);
365 
366             } catch (Exception e) {
367 
368                 final String message = "Error during Velocity initialization";
369 
370                 if (log.isFatalEnabled()) {
371                     log.fatal(message, e);
372                 }
373 
374                 throw new SinonException(message, e, this);
375 
376             }
377 
378             loadCatalogStateListener();
379 
380         } catch (Exception e) {
381 
382             if (log.isFatalEnabled()) {
383 
384                 log.fatal("Fatal error initializing collector "
385                         + collector.getId(), e);
386                 return;
387 
388             }
389 
390         }
391 
392         try {
393 
394             for (int i = 0; i < catalogs.length; i++) {
395 
396                 setup(catalogs[i]);
397 
398                 if (catalogStateListener != null) {
399                     catalogStateListener.onStateChange(catalogs[i],
400                             CatalogState.BEFORE_PROCESSING, this);
401 
402                 }
403 
404                 executeCatalog(catalogs[i]);
405 
406                 if (catalogStateListener != null) {
407 
408                     catalogStateListener.onStateChange(catalogs[i],
409                             CatalogState.AFTER_PROCESSING, this);
410 
411                 }
412 
413             }
414 
415         } catch (SinonException e) {
416 
417             finish = System.currentTimeMillis();
418 
419             if (log.isFatalEnabled()) {
420 
421                 log.fatal("");
422                 log.fatal("Execution state: ");
423                 log.fatal(getExecutionInfo());
424                 log.fatal("");
425                 log.fatal("Properties values: ");
426                 log.fatal(getPropertyInfo());
427                 log.fatal("Fatal error: ", e);
428 
429             }
430 
431             throw e;
432 
433         }
434 
435         finish = System.currentTimeMillis();
436 
437         if (log.isInfoEnabled()) {
438 
439             log.info("Execution finished.");
440             log.info("Information:");
441             log.info(getExecutionInfo());
442 
443         }
444 
445     }
446 
447     /***
448      * Adds all properties of a given <code>catalog</code> to the
449      * Velocity context.
450      *
451      * @param catalog a {@link Catalog} instance.
452      */
453     private void addCatalogProperties(final Catalog catalog)
454             throws SinonException {
455 
456         Properties properties = catalog.getProperties();
457 
458         Enumeration keys = properties.keys();
459 
460         while (keys.hasMoreElements()) {
461 
462             String name = (String) keys.nextElement();
463             String value = properties.getProperty(name);
464             setProperty(name, value);
465 
466         }
467 
468     }
469 
470     /***
471      * Loads this collector's {@link CatalogStateListener} if it exists.
472      *
473      * @throws SinonException if some error ocurrs.
474      */
475     private void loadCatalogStateListener() throws SinonException {
476 
477         String className = collector.getCatalogStateListenerClassName();
478 
479         if (className != null) {
480 
481             Object instance = invokeGetInstanceMethod(className);
482 
483             try {
484 
485                 catalogStateListener = (CatalogStateListener) instance;
486 
487             } catch (ClassCastException e) {
488 
489                 final String message = "Object returned by static method " +
490                         "getInstance() of class" + className
491                         + " is not a  CatalogStateListener instance.";
492 
493                 throw new SinonException(message, e, this);
494 
495             }
496 
497         } else {
498             catalogStateListener = null;
499         }
500 
501     }
502 
503     /***
504      * Sets up this {@link CollectorExecutor} for the execution of
505      * <code>catalog</code>
506      *
507      * @param catalog a {@link Catalog} instance.
508      * @throws SinonException if some error ocurrs.
509      * @todo i18n
510      */
511     private void setup(Catalog catalog) throws SinonException {
512 
513         if (log.isDebugEnabled()) {
514             log.debug("Initializing the context of this execution.");
515         }
516 
517         context = new VelocityContext();
518         context.put(COLLECTOR_PROPERTY, this);
519 
520         httpClient = new HttpClient();
521         httpClient.setConnectionTimeout(
522                 collector.getConfiguration().getTimeout());
523         httpClient.setTimeout(collector.getConfiguration().getTimeout());
524 
525         context = new VelocityContext();
526 
527         configureThinkTime();
528 
529         httpHeaders = collector.getConfiguration().getHttpHeaders();
530 
531         propertyNames = new ArrayList();
532         multivaluedProperties = new HashMap();
533 
534         addCatalogProperties(catalog);
535 
536     }
537 
538     /***
539      * Retuns the {@link Collector} to be executed by this executor.
540      *
541      * @return a {@link Collector} instance.
542      */
543     public Collector getCollector() {
544         return collector;
545     }
546 
547     /***
548      * Returns the {@link Catalog} being executed.
549      * @return a {@link Catalog} instance.
550      */
551     public Catalog getCurrentCatalog() {
552         return currentCatalog;
553     }
554 
555     /***
556      * Returns the {@link Page} being executed.
557      * @return a {@link Page} instance.
558      */
559     public Page getCurrentPage() {
560         return currentPage;
561     }
562 
563     /***
564      * Returns the {@link DataExtraction} being executed.
565      * @return a {@link DataExtraction} instance.
566      */
567     public DataExtraction getCurrentDataExtraction() {
568         return currentDataExtraction;
569     }
570 
571     /***
572      * Returns the {@link Step} being executed.
573      * @return a {@link Step} instance.
574      */
575     public Step getCurrentStep() {
576         return currentStep;
577     }
578 
579     /***
580      * Retorns a <code>String</code> array containing the name of all
581      * properties set in this executor.
582      *
583      * @return a <code>String[]</code>.
584      */
585     public String[] getPropertyNames() {
586 
587         String[] propertyArray = new String[propertyNames.size()];
588 
589         propertyNames.toArray(propertyArray);
590 
591         return propertyArray;
592 
593     }
594 
595     /***
596      * Retorns the last downloaded page text.
597      *
598      * @return a <code>String</code>.
599      */
600     public String getCurrentText() {
601         return currentText;
602     }
603 
604     /***
605      * Retorns the last downloaded document as a byte array.
606      *
607      * @return a <code>byte[]</code>.
608      */
609     public byte[] getCurrentBytes() {
610         return currentBytes;
611     }
612 
613     /***
614      * Retorns the current position in the current page.
615      *
616      * @return an <code>int</code>.
617      */
618     public int getCurrentPosition() {
619         return currentPosition;
620     }
621 
622     /***
623      * Returns the number of pages already downloaded.
624      *
625      * @return an <code>int</code>.
626      */
627     public int getPagesDownloaded() {
628         return pagesDownloaded;
629     }
630 
631     /***
632      * Return the number of bytes already downloaded.
633      *
634      * @return an <code>int</code>.
635      */
636     public int getBytesDownloaded() {
637         return bytesDownloaded;
638     }
639 
640     /***
641      * Retorns the moment this executor started in Unix timestamp format.
642      * Returns -1 if this executor has not started yet.
643      *
644      * @return a <code>long</code>.
645      */
646     public long getStartTime() {
647         return start;
648     }
649 
650     /***
651      * Retorns the moment this executor finished in Unix timestamp format.
652      * Returns -1 if this executor has not finished yet.
653      *
654      * @return a <code>long</code>.
655      */
656     public long getFinishTime() {
657         return finish;
658     }
659 
660     /***
661      * Returns a <code>String</code> containing some execution information.
662      *
663      * @return a <code>String</code>.
664      */
665     public String getExecutionInfo() {
666 
667         StringBuffer buffer = new StringBuffer();
668 
669         buffer.append("Collector       : " + collector.getId());
670         buffer.append("\n");
671 
672         buffer.append("Catalog         : ");
673         if (currentCatalog == null) {
674             buffer.append("none");
675         } else {
676             buffer.append(currentCatalog.getId());
677         }
678         buffer.append("\n");
679 
680         buffer.append("Page            : ");
681         if (currentPage == null) {
682             buffer.append("none");
683         } else {
684             buffer.append(currentPage.getId());
685         }
686         buffer.append("\n");
687 
688         buffer.append("Data extraction : ");
689         if (currentDataExtraction == null) {
690             buffer.append("none");
691         } else {
692             buffer.append(currentDataExtraction.getId());
693         }
694         buffer.append("\n");
695 
696         buffer.append("Step type       : ");
697         if (currentStep == null) {
698             buffer.append("none");
699         } else {
700             buffer.append(currentStep.getClass().getName());
701         }
702         buffer.append("\n");
703 
704         buffer.append("Page downloads  : ");
705         buffer.append(getPagesDownloaded());
706         buffer.append("\n");
707 
708         buffer.append("Bytes downloaded: ");
709         buffer.append(getBytesDownloaded());
710         buffer.append("\n");
711 
712         buffer.append("Start time      : ");
713         buffer.append(new Date(getStartTime()));
714         buffer.append("\n");
715 
716         buffer.append("Finish time     : ");
717         buffer.append(new Date(getFinishTime()));
718         buffer.append("\n");
719 
720         if (finish == -1) {
721             finish = System.currentTimeMillis();
722         }
723 
724         DecimalFormat decimalFormat = new DecimalFormat("0.00");
725         String elapsed = decimalFormat.format((finish - start) / 1000.0);
726 
727         buffer.append("Elapsed time    : ");
728         buffer.append(elapsed);
729         buffer.append(" seconds");
730         buffer.append("\n");
731 
732         return buffer.toString();
733 
734     }
735 
736     /***
737      * Retorns a <code>String</code> containing name and value of all properties
738      * set.
739      *
740      * @return a <code>String</code>.
741      */
742     public String getPropertyInfo() {
743 
744         StringBuffer buffer = new StringBuffer();
745 
746         String[] propertyNames = getPropertyNames();
747 
748         for (int i = 0; i < propertyNames.length; i++) {
749 
750             buffer.append("\t");
751             buffer.append(propertyNames[i]);
752             buffer.append(" = ");
753 
754             final Object propertyValue = getPropertyValue(propertyNames[i]);
755 
756             if (propertyValue instanceof List) {
757                 buffer.append("list : ");
758             }
759 
760             buffer.append(propertyValue);
761             buffer.append("\n");
762 
763         }
764 
765         return buffer.toString();
766 
767     }
768 
769     /***
770      * Returns the <i>logger</i> used by this executor.
771      *
772      * @return a <code>org.apache.commons.logging.Log</code> instance.
773      */
774     public Log getLog() {
775         return log;
776     }
777 
778     /***
779      * Returns the value of the property which name is
780      * <code>propertyName</code>. If this property is a simple value, a
781      * <code>String</code> is returned. Otherwise, a <code>List</code>
782      * containing <code>String</code>s is returned.
783      *
784      * @param propertyName a <code>String</code>.
785      * @return the value of <code>propertyName</code>.
786      * @throws IllegalArgumentException if <code>propertyName</code> is
787      * <code>null</code>.
788      */
789     final public Object getPropertyValue(String propertyName) {
790 
791         if (propertyName == null) {
792 
793             throw new IllegalArgumentException(
794                     "Parameter propertyName can't be null.");
795 
796         }
797 
798         if (multivaluedProperties.containsKey(propertyName)) {
799 
800             return context.get(propertyName);
801 
802         } else {
803 
804             String value = null;
805             final String template =
806                     TEMPLATE_PREFIX + propertyName + TEMPLATE_SUFFIX;
807 
808             try {
809                 value = evaluate(template);
810             } catch (EvaluationException e) {
811 
812                 if (log.isErrorEnabled()) {
813                     log.error(e);
814                 }
815 
816                 return null;
817 
818             }
819 
820             if (template.equals(value)) {
821                 return null;
822             } else {
823                 return value;
824             }
825 
826         }
827 
828     }
829 
830     /***
831      * Evaluates the Velocity template contained in <code>template</code>.
832      *
833      * @param template a <code>String</code> contendo um Velocity template.
834      */
835     private String evaluate(String template) throws EvaluationException {
836 
837         StringWriter sw = new StringWriter();
838 
839         try {
840 
841             engine.evaluate(context, sw, template, template);
842 
843         } catch (Exception e) {
844 
845             final String message = "Problems evaluation template : "
846                     + template;
847 
848             if (log.isFatalEnabled()) {
849                 log.fatal(message);
850             }
851 
852             throw new EvaluationException(e, this, template);
853 
854         }
855 
856         return sw.toString();
857 
858     }
859 
860     /***
861      * Adds a value to a multivalued property (list). If the property does not
862      * exists yet, it is created. To set simple properties, use
863      * {@link #setProperty}.
864      *
865      * @param propertyId a <code>String</code> containing the property name.
866      * @param value a <code>String</code> containing the value to be added.
867      * @param allowDuplicates a <code>boolean</code> telling if duplicate values
868      * can be added or not.
869      * @throws SinonException if <code>propertyId</code> is not a multivalued
870      * property.
871      * @throws IllegalArgumentException if <code>propertyId</code> is
872      * <code>null</code> or if <code>value</code> is <code>null</code>.
873      * @todo this method cannot be public.
874      */
875     final public void addValueToProperty(String propertyId, String value,
876                                          boolean allowDuplicates)
877             throws SinonException {
878 
879         if (propertyId == null) {
880 
881             throw new IllegalArgumentException(
882                     "Parameter propertyId can't be null");
883 
884         }
885 
886         if (value == null) {
887 
888             throw new IllegalArgumentException(
889                     "Parameter value can't be null");
890 
891         }
892 
893         if (multivaluedProperties.containsKey(propertyId) == false) {
894 
895             if (propertyNames.contains(propertyId)) {
896 
897                 throw new SinonException("Attempt to add a value to a " +
898                         "property that is not multivalued : "
899                         + propertyId);
900 
901             }
902 
903             ArrayList values = new ArrayList();
904             values.add(value);
905 
906             propertyNames.add(propertyId);
907             multivaluedProperties.put(propertyId, values);
908             context.put(propertyId, values);
909 
910             if (log.isDebugEnabled()) {
911 
912                 log.debug("Creating multivalued property " + propertyId
913                         + " with value " + value);
914 
915             }
916 
917         } else {
918 
919             if (propertyNames.contains(propertyId) == false) {
920                 propertyNames.add(propertyId);
921             }
922 
923             List values = (List) multivaluedProperties.get(propertyId);
924 
925             if (allowDuplicates == false) {
926 
927                 if (values.contains(value)) {
928 
929                     log.debug("Attempt to add value to multivalued property " +
930                             "that does not accept duplicate values: "
931                             + propertyId + " value: "
932                             + value);
933 
934                     return;
935 
936                 }
937 
938             }
939 
940             values.add(value);
941 
942             if (log.isDebugEnabled()) {
943 
944                 log.debug("Added value to property " + propertyId
945                         + " : " + value);
946 
947             }
948 
949         }
950 
951     }
952 
953     /***
954      * Sets a property. This method can only be used with simple properties.
955      * To add a value to a multivalued property, use
956      * {@link #addValueToProperty}.
957      *
958      * @param propertyId a <code>String</code> containing the property name.
959      * @param value a <code>String</code> containing the property value.
960      * @throws SinonException if <code>propertyId</code> for o nome de uma propriedade
961      *             multivalorada.
962      * @throws IllegalArgumentException
963      *             se <code>propertyId</code> é <code>null</code> ou se
964      *             <code>value</code> é <code>null</code>.
965      */
966     final public void setProperty(String propertyId, String value)
967             throws SinonException {
968 
969         if (propertyId == null) {
970 
971             throw new IllegalArgumentException(
972                     "Parameter propertyId cannot be null");
973 
974         }
975 
976         if (value == null) {
977 
978             throw new IllegalArgumentException(
979                     "Parameter value cannot be null");
980 
981         }
982 
983         if (propertyNames.contains(propertyId) == false) {
984 
985             if (multivaluedProperties.containsKey(propertyId)) {
986 
987                 throw new SinonException("Attempt to set a value for a "
988                         + "multivalued property: " + propertyId);
989 
990             }
991 
992             propertyNames.add(propertyId);
993 
994         }
995 
996         if (log.isDebugEnabled()) {
997 
998             log.debug("Setting property : " + propertyId + " = " + value
999                     + " (previous value : " + context.get(propertyId) + ")");
1000 
1001         }
1002 
1003         context.put(propertyId, value);
1004 
1005     }
1006 
1007     /***
1008      * Executes a catalog.
1009      *
1010      * @param catalog a {@link Catalog} instance.
1011      * @throws SinonException if some error ocurrs.
1012      */
1013     private void executeCatalog(Catalog catalog) throws SinonException {
1014 
1015         currentCatalog = catalog;
1016         StartPage[] startPages = catalog.getStartPages();
1017         Page page;
1018 
1019         if (mustStop == false) {
1020 
1021             if (log.isInfoEnabled()) {
1022 
1023                 log.info("Starting processing of catalog "
1024                         + catalog.getId());
1025 
1026             }
1027 
1028             for (int i = 0; i < startPages.length; i++) {
1029 
1030                 page = catalog.getPage(startPages[i].getRefid());
1031 
1032                 if (page == null) {
1033 
1034                     final UnknownPageException exception =
1035                             new UnknownPageException(
1036                                     startPages[i].getRefid(), this);
1037 
1038                     throw exception;
1039 
1040                 }
1041 
1042                 // the Velocity context does not need to be restarted at the
1043                 // first time, as the method that calls this method already
1044                 // called setup
1045                 if (i > 0) {
1046                     setup(catalog);
1047                 }
1048 
1049                 setProperties(startPages[i].getProperties());
1050 
1051                 if (log.isInfoEnabled()) {
1052 
1053                     log.info("Starting download and processing of initial " +
1054                             "action " + page.getId() + " (" + (i + 1)
1055                             + " out of " + startPages.length + ")");
1056 
1057                 }
1058 
1059                 actionQueue.clear();
1060 
1061                 executePage(page);
1062 
1063                 while (actionQueue.isEmpty() == false) {
1064 
1065                     // removing the ActionContextPair at the top of the
1066                     // stack and executing it.
1067                     final int size = actionQueue.size();
1068                     ActionContextPair pair =
1069                             (ActionContextPair) actionQueue.get(size - 1);
1070 
1071                     actionQueue.remove(pair);
1072 
1073                     final Action action = pair.getAction();
1074 
1075                     Page thisPage = catalog.getPage(action.getPageId());
1076 
1077                     // note that the context being used in the execution
1078                     // was changed to the one popped from the top of the
1079                     // stack.
1080                     context = pair.getContext();
1081 
1082                     executePage(thisPage, action);
1083 
1084                 }
1085 
1086             }
1087 
1088         }
1089 
1090         if (log.isInfoEnabled()) {
1091 
1092             log.info("Finished execution of catalog " + catalog.getId());
1093             log.info("Execution information up to now:");
1094             log.info(getExecutionInfo());
1095 
1096         }
1097 
1098     }
1099 
1100     /***
1101      * Executes a action. If <code>action</code> is not <code>null</code>,
1102      * the action download will follow the <code>action</code>.
1103      * Otherwise, the action download will use the configurations of
1104      * <code>action</code>.
1105      *
1106      * @param page a {@link Page} instance.
1107      * @param action an {@link Action} instance.
1108      * @throws SinonException if some error ocurrs.
1109      */
1110     private void executePageAndItsActions(Page page, Action action)
1111             throws SinonException {
1112 
1113         executePage(page, action);
1114 
1115     }
1116 
1117     /***
1118      * Calls {@link #setProperty} for each property in
1119      * <code>properties</code>.
1120      *
1121      * @param properties a <code>java.util.Properties</code> instance.
1122      */
1123     private void setProperties(Properties properties) throws SinonException {
1124 
1125         Enumeration keys = properties.keys();
1126 
1127         while (keys.hasMoreElements()) {
1128 
1129             String key = (String) keys.nextElement();
1130             String value = properties.getProperty(key);
1131             setProperty(key, value);
1132 
1133         }
1134 
1135     }
1136 
1137     /***
1138      * Executes a action. The same as <code>executeCatalog(action, null)</code>.
1139      *
1140      * @param page a {@link Page} instance.
1141      * @throws SinonException if some error ocurrs.
1142      */
1143     private void executePage(Page page) throws SinonException {
1144         executePage(page, null);
1145     }
1146 
1147     /***
1148      * Executes a pages. If <code>action</code> is not <code>null</code>,
1149      * the action download will follow the <code>action</code>.
1150      * Otherwise, the action download will use the configurations of
1151      * <code>action</code>.
1152      *
1153      * @param page a {@link Page} instance.
1154      * @param page an {@link Action} instance.
1155      * @throws SinonException if some error ocurrs.
1156      */
1157     private void executePage(Page page, Action action) throws SinonException {
1158 
1159         // if nobody asked this CollectorExecutor to not execute anymore . . .
1160         if (mustStop == false) {
1161 
1162             currentPage = page;
1163 
1164             loadListeners(page);
1165 
1166             currentText = download(page, action);
1167 
1168             // if there was some problem during the action download, we notify
1169             // the action listener.
1170             if (currentText == null) {
1171 
1172                 notifyPageListener(FINISHED);
1173                 return;
1174 
1175             }
1176 
1177             final String filename =
1178                     page.getStorageConfiguration().getFilename();
1179 
1180             if (filename != null) {
1181                 writePageToFile(filename);
1182             }
1183 
1184             processPage(page);
1185 
1186             notifyPageListener(AFTER_PROCESSING);
1187 
1188             Action[] actions = page.getActions();
1189 
1190             for (int i = 0; i < actions.length; i++) {
1191                 executeActionContextPairs(actions[i]);
1192             }
1193 
1194             currentPage = page;
1195 
1196             notifyPageListener(FINISHED);
1197 
1198         }
1199 
1200     }
1201 
1202     /***
1203      * Writes {@link #currentText} to a file named <code>filename</code>.
1204      * If it already exists, it will be overwritten.
1205      *
1206      * @param filename a <code>String</code>.
1207      */
1208     private void writePageToFile(final String filename) throws SinonException {
1209 
1210         String pathname = evaluate(filename);
1211         File destination = new File(pathname);
1212 
1213         try {
1214 
1215             if (destination.isAbsolute() == false) {
1216                 destination = new File(CURRENT_DIRECTORY, pathname);
1217             }
1218 
1219             BufferedOutputStream outputStream =
1220                     new BufferedOutputStream(new FileOutputStream(destination));
1221 
1222             outputStream.write(currentBytes);
1223             outputStream.close();
1224 
1225         } catch (IOException e) {
1226 
1227             throw new SinonException("Error writing page "
1228                     + currentPage.getId()
1229                     + " to " + destination.getAbsolutePath(), e, this);
1230 
1231         }
1232 
1233     }
1234 
1235     /***
1236      * Loads the listeners of a given action.
1237      */
1238     private void loadListeners(Page page) throws SinonException {
1239 
1240         String className;
1241         Object instance;
1242 
1243         className = page.getErrorListenerClassName();
1244 
1245         if (className != null) {
1246 
1247             instance = invokeGetInstanceMethod(className);
1248 
1249             try {
1250 
1251                 currentErrorListener = (ErrorListener) instance;
1252 
1253             } catch (ClassCastException e) {
1254 
1255                 final String message = "The object returned by the static "
1256                         + "method getInstance() of class " + className
1257                         + " is not an ErrorListener instance.";
1258 
1259                 throw new SinonException(message, e, this);
1260 
1261             }
1262 
1263         } else {
1264             currentErrorListener = null;
1265         }
1266 
1267         className = currentPage.getPageStateListenerClassName();
1268 
1269         if (className != null) {
1270 
1271             instance = invokeGetInstanceMethod(className);
1272 
1273             try {
1274 
1275                 currentPageStateListener = (PageStateListener) instance;
1276 
1277             } catch (ClassCastException e) {
1278 
1279                 final String message = "The object returned by the static "
1280                         + "method getInstance() of class " + className
1281                         + " is not an PageStateListener instance.";
1282 
1283                 throw new SinonException(message, e, this);
1284 
1285             }
1286 
1287         } else {
1288             currentPageStateListener = null;
1289         }
1290 
1291 //		className = currentPage.getPromptListenerClassName();
1292 //
1293 //		if (className != null) {
1294 //
1295 //			instance = invokeGetInstanceMethod(className);
1296 //
1297 //			try {
1298 //
1299 //				currentPromptListener = (PromptListener) instance;
1300 //
1301 //			} catch (ClassCastException e) {
1302 //
1303 //				final String message = "O objeto retornado pelo método "
1304 //						+ "estático getInstance() da classe " + className
1305 //						+ " não é uma instância de PromptListener.";
1306 //
1307 //				throw new SinonException(message, e, this);
1308 //
1309 //			}
1310 //
1311 //		} else {
1312 //			currentPromptListener = null;
1313 //		}
1314 
1315     }
1316 
1317     /***
1318      * Invokes static method <code>getInstance</code> of the class whose
1319      * fully qualified name is <code>className</code>.
1320      *
1321      * @param className a <code>String</code>.
1322      * @return the object returned by static method <code>getInstance</code>
1323      * of the class whose fully qualified name is <code>className</code>.
1324      * @throws SinonException if some error ocurrs.
1325      */
1326     private Object invokeGetInstanceMethod(String className)
1327             throws SinonException {
1328 
1329         Object instance;
1330         Class someClass = null;
1331 
1332         try {
1333 
1334             someClass = Class.forName(className);
1335 
1336         } catch (ClassNotFoundException e) {
1337 
1338             final String message = "Class " + className
1339                     + " was not found.";
1340 
1341             throw new SinonException(message, e, this);
1342 
1343         }
1344 
1345         try {
1346 
1347             Method method = someClass.getMethod("getInstance", null);
1348             instance = method.invoke(null, null);
1349 
1350         } catch (Exception e) {
1351 
1352             final String message = "Could not create a " + className +
1353                     "instance. Check if it has a public static getInstance() " +
1354                     "method that returns one instance of this class.";
1355 
1356             throw new SinonException(message, e, this);
1357 
1358         }
1359 
1360         return instance;
1361 
1362     }
1363 
1364     /***
1365      * Notifies the current action {@link PageStateListener} that its
1366      * state changed to <code>newPageState</code>.
1367      *
1368      * @param newPageState a {@link PageState} instance.
1369      */
1370     private void notifyPageListener(PageState newPageState) {
1371 
1372         if (currentPageStateListener != null) {
1373 
1374             currentPageStateListener.onStateChange(currentPage, newPageState,
1375                     catalogStateListener, this);
1376 
1377         }
1378 
1379     }
1380 
1381     /***
1382      * Processes a action.
1383      *
1384      * @param page a {@link Page} instance.
1385      */
1386     private void processPage(Page page) throws SinonException {
1387 
1388         Step[] steps = page.getSteps();
1389         currentPosition = 0;
1390         from = -1;
1391         to = -1;
1392 
1393         for (int i = 0; i < steps.length; i++) {
1394 
1395             Step step = steps[i];
1396 
1397             try {
1398 
1399                 executeStep(step, Integer.MAX_VALUE);
1400 
1401             } catch (SinonException e) {
1402 
1403                 ErrorResponse response = notifyErrorListener(e);
1404 
1405                 if (response.isResumeNext()) {
1406                     continue;
1407                 } else if (response.isRetry()) {
1408                     executeStep(step, Integer.MAX_VALUE);
1409                 } else if (response.isStop()) {
1410                     throw e;
1411                 }
1412 
1413             }
1414 
1415         }
1416 
1417     }
1418 
1419     /***
1420      * Executes an extraction step (action, data extraction, loop, jump,
1421      * from or to).
1422      *
1423      * @param step a {@link Step} instance.
1424      * @param limitPosition a <code>int</code> containing a position that
1425      * this step cannot go further.
1426      * @throws StepException if some error ocurrs.
1427      */
1428     private void executeStep(Step step, int limitPosition)
1429             throws SinonException {
1430 
1431         currentStep = step;
1432 
1433         if (step.isPositioningStep()) {
1434 
1435             PositioningStep positioningStep = (PositioningStep) step;
1436             executePositioningStep(positioningStep, limitPosition);
1437 
1438         } else if (step.isDataExtraction()) {
1439 
1440             DataExtraction dataExtraction = (DataExtraction) step;
1441             executeDataExtraction(dataExtraction, limitPosition);
1442 
1443         } else if (step.isAction()) {
1444 
1445             log.fatal("This could not happen!!!");
1446             System.exit(1);
1447 
1448         } else if (step.isLoop()) {
1449 
1450             LoopStep loopStep = (LoopStep) step;
1451             executeLoop(loopStep, limitPosition);
1452 
1453         } else {
1454 
1455             throw new StepException("Unknown step type: " + step + " : "
1456                     + step.getClass(), this);
1457 
1458         }
1459 
1460     }
1461 
1462     /***
1463      * Executes a loop.
1464      *
1465      * @param loop a {@link LoopStep} instance.
1466      * @param limitPosition a <code>int</code> containing a position that
1467      * this step cannot go further.
1468      * @throws StepException if some error ocurrs.
1469      */
1470     private void executeLoop(LoopStep loop, int limitPosition)
1471             throws SinonException {
1472 
1473         int iterations = 0;
1474         int loopReferencePosition = getLoopReferencePosition(loop);
1475 
1476         if (limitPosition < loopReferencePosition) {
1477             loopReferencePosition = limitPosition;
1478         }
1479 
1480         while (evaluateLoopCondition(loop, loopReferencePosition)) {
1481 
1482             Step[] steps = loop.getSteps();
1483             int oldCurrentPosition = currentPosition;
1484 
1485             try {
1486 
1487                 for (int i = 0; i < steps.length; i++) {
1488 
1489                     if (oldCurrentPosition > currentPosition) {
1490                         break;
1491                     }
1492 
1493                     oldCurrentPosition = currentPosition;
1494                     executeStep(steps[i], loopReferencePosition);
1495 
1496                     if (oldCurrentPosition > currentPosition) {
1497                         break;
1498                     }
1499 
1500                 }
1501 
1502 // expected failure . . .
1503             } catch (StepAfterLoopLimitPositionException afterLimitPosition) {
1504 
1505                 if (log.isDebugEnabled()) {
1506                     log.debug("Iterations executed : " + iterations);
1507                 }
1508 
1509                 return;
1510 
1511             } catch (StepWithBeforeException beforeException) {
1512                 throw beforeException;
1513             } catch (SinonException sinonException) {
1514 
1515                 if (loop.getCondition().equals("notFails")) {
1516 
1517                     currentPosition = oldCurrentPosition;
1518 
1519                     if (log.isDebugEnabled()) {
1520 
1521                         log.debug("Expected failure in loop in action "
1522                                 + "identificador " + currentPage.getId());
1523 
1524                         if (currentStep.isPositioningStep()) {
1525 
1526                             PositioningStep positioningStep =
1527                                     (PositioningStep) currentStep;
1528 
1529                             log.debug("Reference : "
1530                                     + positioningStep.getReference());
1531 
1532                         }
1533 
1534                         log.debug("Iterations executed : " + iterations);
1535 
1536                     }
1537 
1538                     return;
1539 
1540                 } else {
1541                     throw sinonException;
1542                 }
1543 
1544             }
1545 
1546             iterations++;
1547 
1548             if (evaluateLoopCondition(loop, loopReferencePosition) == false) {
1549                 return;
1550             }
1551 
1552         }
1553 
1554     }
1555 
1556     /***
1557      * Retruns the reference position of the current loop.
1558      *
1559      * @param loop a {@link LoopStep} instance.
1560      * @throws StepException if some error ocurrs.
1561      */
1562     private int getLoopReferencePosition(LoopStep loop) throws SinonException {
1563 
1564         int loopReferencePosition;
1565 
1566         if (loop.getCondition().equals("before")) {
1567 
1568             loopReferencePosition = currentText.indexOf(
1569                     loop.getValue(), currentPosition);
1570 
1571             if (loopReferencePosition < 0) {
1572 
1573                 throw new SinonException("Loop reference not found : "
1574                         + loop.getValue());
1575 
1576             }
1577 
1578         } else {
1579             loopReferencePosition = Integer.MAX_VALUE;
1580         }
1581 
1582         return loopReferencePosition;
1583 
1584     }
1585 
1586     /***
1587      * Returns <code>true</code> if this loop must continue, <code>false</code>
1588      * otherwise.
1589      *
1590      * @param loop a {@link LoopStep} instance.
1591      * @return <code>true</code> if this loop must continue, <code>false</code>
1592      * otherwise.
1593      * @throws StepException if some error ocurrs.
1594      */
1595     private boolean evaluateLoopCondition(LoopStep loop,
1596                                           int loopReferencePosition) throws SinonException {
1597 
1598         if (loop.getCondition().equals("notFails")) {
1599 
1600             return (true);
1601 
1602         } else if (loop.getCondition().equals("before")) {
1603 
1604             return (currentPosition < loopReferencePosition);
1605 
1606         } else {
1607 
1608             throw new SinonException("Unknown loop stop condition : "
1609                     + loop.getCondition());
1610 
1611         }
1612 
1613     }
1614 
1615     /***
1616      * Enqueues one or more {@link ActionContextPair} to execute
1617      * this <code>action</code>.
1618      *
1619      * @param action an {@link Action} instance.
1620      * @throws SinonException if some error ocurrs.
1621      */
1622     private void executeActionContextPairs(Action action)
1623             throws SinonException {
1624 
1625         String condition = action.getCondition();
1626 
1627         // if we have a condition in this action . . .
1628         if (condition != null) {
1629 
1630             final boolean mustExecute = evaluateActionCondition(condition,
1631                     action.getPropertyId());
1632 
1633             if (mustExecute == false) {
1634                 return;
1635             }
1636 
1637         }
1638 
1639         Page page = currentCatalog.getPage(action.getPageId());
1640 
1641         if (page == null) {
1642 
1643             if (log.isFatalEnabled()) {
1644 
1645                 final String message = "Page " + action.getPageId()
1646                         + ", referenced in a " + currentPage.getId() +
1647                         " action, was not found.";
1648 
1649                 log.fatal(message);
1650 
1651                 throw new UnknownPageException(action.getPageId(), this);
1652 
1653             }
1654 
1655         }
1656 
1657         String foreach = action.getForeach();
1658 
1659         // if this action has a loop . . .
1660         if (foreach != null) {
1661 
1662             String index = action.getIndex();
1663             String variable = action.getVariable();
1664             List values = (List) getPropertyValue(foreach);
1665 
1666             if (values == null) {
1667 
1668                 throw new SinonException("Property " + foreach
1669                         + " does not exists or no value was added to it.");
1670 
1671             }
1672 
1673             final int size = values.size();
1674 
1675             // we are enqueuing actions to be executed, se we must
1676             // do this in reverse order in order to have them executed
1677             // in the right order.
1678             for (int i = size - 1; i >= 0; i--) {
1679 
1680                 String value = (String) values.get(i);
1681 
1682                 if (index != null) {
1683                     setProperty(index, String.valueOf(i + 1));
1684                 }
1685 
1686                 if (variable != null) {
1687                     setProperty(variable, value);
1688                 }
1689 
1690                 enqueueAction(action);
1691 
1692             }
1693 
1694         } else {
1695 
1696             page = currentCatalog.getPage(action.getPageId());
1697 
1698             if (page == null) {
1699 
1700                 if (log.isFatalEnabled()) {
1701 
1702                     final String message = "Page " + action.getPageId()
1703                             + ", referenced by a "
1704                             + currentPage.getId() + " action, was not found.";
1705 
1706                     log.fatal(message);
1707 
1708                     throw new UnknownPageException(action.getPageId(), this);
1709 
1710                 }
1711 
1712             }
1713 
1714             enqueueAction(action);
1715 
1716         }
1717 
1718     }
1719 
1720     /***
1721      * Enqueues an action.
1722      *
1723      * @param action an {@link Action} instance.
1724      */
1725     private void enqueueAction(Action action) {
1726 
1727         if (action == null) {
1728 
1729             throw new IllegalArgumentException(
1730                     "Parameter action can't be null");
1731 
1732         }
1733 
1734         final VelocityContext contextClone = (VelocityContext) context.clone();
1735 
1736         actionQueue.add(new ActionContextPair(action, contextClone));
1737 
1738     }
1739 
1740     /***
1741      * Evaluates if a given {@link Action} condition is true or not.
1742      *
1743      * @param condition a <code>String</code> containing the name of the
1744      * condition.
1745      * @param propertyId a <code>String</code> containing the name of
1746      * the variable used to evaluate the condition.
1747      * @return <code>true</code> if the condition is true,
1748      *         <code>false</code> otherwise.
1749      * @throws IllegalArgumentException if <code>propertyId</code> is
1750      * <code>null</code>.
1751      * @throws SinonException if some error ocurrs.
1752      */
1753     private boolean evaluateActionCondition(String condition, String propertyId)
1754             throws SinonException {
1755 
1756         String value = (String) getPropertyValue(propertyId);
1757 
1758         if (value == null) {
1759             throw new SinonException("The property " + propertyId + " does " +
1760                     "not exists.");
1761         }
1762 
1763         if (condition.equals("notEmpty")) {
1764 
1765             if (value.trim().length() == 0) {
1766                 return false;
1767             } else {
1768                 return true;
1769             }
1770 
1771         } else {
1772 
1773             throw new IllegalArgumentException("The condition " + condition
1774                     + " is not valid.");
1775 
1776         }
1777 
1778     }
1779 
1780     /***
1781      * Executes a data extraction. The action current position is changed.
1782      *
1783      * @param dataExtraction a {@link PositioningStep} instance.
1784      * @param limitPosition a <code>int</code> containing a position that
1785      * this step cannot go further.
1786      * @throws StepException if some error ocurrs.
1787      */
1788     private void executeDataExtraction(DataExtraction dataExtraction,
1789                                        int limitPosition)
1790             throws SinonException {
1791 
1792         currentDataExtraction = dataExtraction;
1793 
1794         if (dataExtraction.isResetPositionNeeded()) {
1795             currentPosition = 0;
1796         }
1797 
1798         Step[] steps = dataExtraction.getSteps();
1799         int oldCurrentPosition = currentPosition;
1800 
1801         try {
1802 
1803             for (int i = 0; i < steps.length; i++) {
1804                 executeStep(steps[i], limitPosition);
1805             }
1806 
1807         } catch (StepAfterLoopLimitPositionException e) {
1808 
1809             throw e;
1810 
1811         } catch (StepException e) {
1812 
1813             if (dataExtraction.isFailOnError()) {
1814                 throw e;
1815             } else {
1816 
1817                 currentPosition = oldCurrentPosition;
1818 
1819                 if (log.isDebugEnabled()) {
1820 
1821                     if (e instanceof StepWithFailIfFoundException
1822                             || e instanceof StepWithBeforeException) {
1823 
1824                         log.debug(e.getClass() + " : " + e.getMessage());
1825 
1826                     } else {
1827 
1828                         log.debug("Data extraction " + dataExtraction.getId()
1829                                 + " had an expected failure, so everything " +
1830                                 "OK.");
1831 
1832                     }
1833 
1834                     if (currentStep.isPositioningStep()) {
1835 
1836                         PositioningStep positioningStep = (PositioningStep) currentStep;
1837 
1838                         log.debug("Reference : "
1839                                 + positioningStep.getReference());
1840 
1841                     }
1842 
1843                 }
1844 
1845                 if (dataExtraction.isMultivalued()) {
1846                     addValueToProperty(dataExtraction.getId(),
1847                             dataExtraction.getValueOnError(), true);
1848                 } else {
1849                     setProperty(dataExtraction.getId(),
1850                             dataExtraction.getValueOnError());
1851                 }
1852 
1853                 return;
1854 
1855             }
1856 
1857         }
1858 
1859         String value = extractFromToData(dataExtraction);
1860 
1861         if (dataExtraction.isTrimValueNeeded()) {
1862             value = value.trim();
1863         }
1864 
1865         if (dataExtraction.isMultivalued() == false) {
1866             setProperty(dataExtraction.getId(), value);
1867         } else {
1868 
1869             addValueToProperty(dataExtraction.getId(), value,
1870                     dataExtraction.isAllowDuplicates());
1871 
1872         }
1873 
1874     }
1875 
1876     /***
1877      * Extracts a piece of data after <code>from</code> and <code>to</code>
1878      * steps were already done. The extracted string is returned.
1879      *
1880      * @param dataExtraction the {@link DataExtraction} being executed.
1881      * @throws SinonException if the <code>from</code> step was not
1882      * executed succesfully of if the <code>to</code> step was not
1883      * executed succesfully of if the <code>from</code> position is
1884      * higher than the <code>to</code> one.
1885      */
1886     private String extractFromToData(DataExtraction dataExtraction)
1887             throws SinonException {
1888 
1889         if (from < 0) {
1890 
1891             throw new SinonException("Initial position (from) of data " +
1892                     " extraction " + dataExtraction.getId()
1893                     + " was not found or was not set in action "
1894                     + currentPage.getId());
1895 
1896         }
1897 
1898         if (to < 0) {
1899 
1900             throw new SinonException("End position (to) of data " +
1901                     " extraction " + dataExtraction.getId()
1902                     + " was not found or was not set in action "
1903                     + currentPage.getId());
1904 
1905         }
1906 
1907         if (from > to) {
1908 
1909             throw new SinonException("Initial position (from) of data "
1910                     + "extraction " + dataExtraction.getId() + " is higher "
1911                     + "than the end positioin (to) in action " +
1912                     currentPage.getId());
1913 
1914         }
1915 
1916         String value = currentText.substring(from, to);
1917         return value;
1918 
1919     }
1920 
1921     /***
1922      * Executes a positioning step. The current position is changed.
1923      *
1924      * @param step a {@link PositioningStep} instance.
1925      * @param limitPosition a <code>int</code> containing a position that
1926      * this step cannot go further.
1927      * @throws StepException if the positioning step's reference was not found.
1928      */
1929     private void executePositioningStep(PositioningStep step, int limitPosition)
1930             throws StepException {
1931 
1932         currentStep = step;
1933 
1934         int oldCurrentPosition = currentPosition;
1935 
1936         final String originalReference = step.getReference();
1937         String reference = null;
1938 
1939         try {
1940             reference = evaluate(originalReference);
1941         } catch (EvaluationException e) {
1942 
1943             throw new StepException("Could not evaluate reference "
1944                     + originalReference + " : " + e.getClass() + " : "
1945                     + e.getMessage(), this);
1946 
1947         }
1948 
1949         int beforePosition = Integer.MAX_VALUE;
1950 
1951         String before = step.getBefore();
1952 
1953         if (before != null) {
1954 
1955             beforePosition = currentText.indexOf(before, currentPosition);
1956 
1957             if (beforePosition == -1) {
1958 
1959                 throw new StepWithBeforeException(
1960                         "Failure executing positioning step: the reference " +
1961                         "string " + before + ", used as maximum position, " +
1962                         "was not found.", this);
1963 
1964             }
1965 
1966         }
1967 
1968         if (step.getDirection() == PositioningStep.FORWARD) {
1969 
1970             currentPosition = currentText.indexOf(reference, currentPosition);
1971 
1972         } else {
1973 
1974             currentPosition = currentText.lastIndexOf(reference,
1975                     currentPosition);
1976 
1977         }
1978 
1979         if (step.isFailIfFound()) {
1980 
1981             if (currentPosition >= 0) {
1982 
1983                 throw new StepWithFailIfFoundException("Reference string "
1984                         + originalReference + " (" + reference
1985                         + "), that means an expected failure, was found" +
1986                         "and everything is OK.", this);
1987 
1988             } else {
1989 
1990                 currentPosition = oldCurrentPosition;
1991                 return;
1992 
1993             }
1994 
1995         }
1996 
1997         if (currentPosition > limitPosition) {
1998 
1999             throw new StepAfterLoopLimitPositionException("The positioning " +
2000                     "step with reference " + originalReference +
2001                     " (" + reference + ") went further the loop limit.", this);
2002 
2003         }
2004 
2005         if (currentPosition == -1) {
2006 
2007             currentPosition = oldCurrentPosition;
2008 
2009             throw new StepException("Failure during execution of positioning " +
2010                     "step: the reference string " + originalReference +
2011                     " (" + reference + ") "
2012                     + " was not found.", this);
2013 
2014         }
2015 
2016         if (currentPosition >= beforePosition) {
2017 
2018             throw new StepException("Failure during execution of positioning " +
2019                     "step: the reference string " + originalReference +
2020                     " (" + reference + ") "
2021                     + " was found after string " + before, this);
2022 
2023         }
2024 
2025         if (step.isFrom() || step.isJump()) {
2026             currentPosition += reference.length();
2027         }
2028 
2029         if (step.isFrom()) {
2030             from = currentPosition;
2031         }
2032 
2033         if (step.isTo()) {
2034             to = currentPosition;
2035         }
2036 
2037     }
2038 
2039     /***
2040      * Downloads a action and returns it in a <code>String</code>.
2041      * The configurations contained in <code>actioni</code>, if it is not
2042      * <code>null</code>, are used instead of the configurations contained in
2043      * <code>action</code>.
2044      *
2045      * @param page a {@link Page} instance.
2046      * @param action an {@link Action} instance.
2047      * @return a <code>String</code>.
2048      * @throws SinonException if some unhandled error ocurrs.
2049      *             se algum erro não tratado ocorrer.
2050      */
2051     private String download(Page page, Action action) throws SinonException {
2052 
2053         HttpMethod method;
2054         String url = null;
2055         PageConfiguration configuration = null;
2056         currentText = null;
2057         currentBytes = null;
2058 
2059         if (action != null) {
2060 
2061             url = action.getUrl();
2062 
2063             if (url != null) {
2064 
2065                 configuration = action.getConfiguration();
2066 
2067             } else {
2068 
2069                 url = page.getUrl();
2070                 configuration = page.getConfiguration();
2071 
2072             }
2073 
2074         } else {
2075 
2076             url = page.getUrl();
2077             configuration = page.getConfiguration();
2078 
2079         }
2080 
2081         url = url.trim();
2082 
2083         if (url == null || url.length() == 0) {
2084 
2085             throw new SinonException("No URL was given to action "
2086                     + page.getId() + " in catalog " + currentCatalog.getId());
2087 
2088         }
2089 
2090         url = evaluate(url);
2091 
2092         if (url.startsWith(FILE_PROTOCOL)) {
2093 
2094             String filename = url.substring(
2095                     FILE_PROTOCOL.length(), url.length());
2096 
2097             currentText = readFile(filename);
2098 
2099             return currentText;
2100 
2101         } else if (url.startsWith(HTTP_PREFIX) == false
2102                 && url.startsWith(HTTPS_PREFIX) == false) {
2103 
2104             throw new SinonException(
2105                     "Toda URL must begin with " + HTTP_PREFIX + ", " +
2106                     HTTPS_PREFIX + ", or " + FILE_PROTOCOL +
2107                     "(action: " + page + ", url : " + url + ")");
2108 
2109         }
2110 
2111         method = createHttpMethod(page, url, configuration);
2112 
2113         final boolean followRedirects = page.getFollowRedirects();
2114 
2115         setHttpHeaders(method);
2116 
2117         if (log.isDebugEnabled()) {
2118             logDebuggingMessages(page, url, method);
2119         }
2120 
2121         notifyPageListener(BEFORE_DOWNLOAD);
2122 
2123         int response = -1;
2124         int retries = collector.getConfiguration().getRetries();
2125         Exception exception = null;
2126         GetMethod redirMethod = null;
2127         String exceptionMessage = null;
2128         boolean ok = false;
2129 
2130         for (int i = 0; i < retries + 1; i++) {
2131 
2132             if (log.isDebugEnabled() && i > 0) {
2133 
2134                 log.debug("");
2135                 log.debug("Attempt " + i + " of " + retries);
2136 
2137             }
2138 
2139             // se esta não é a primeira vez que baixamos alguma coisa,
2140             // temos que observar o think time.
2141             if (pagesDownloaded > 0) {
2142                 waitForThinkTime();
2143             }
2144 
2145             long start = System.currentTimeMillis();
2146             long end;
2147 
2148             try {
2149 
2150                 response = httpClient.executeMethod(method);
2151                 boolean wasRedirected = false;
2152 
2153                 if (followRedirects && response >= 300 && response < 400) {
2154 
2155                     URI location = new URI(method.getURI(),
2156                             method.getResponseHeader("location").getValue());
2157 
2158                     if (log.isDebugEnabled()) {
2159                         log.debug("Redirecting to " + location.getURI());
2160                     }
2161 
2162                     redirMethod = new GetMethod(location.getURI());
2163                     response = httpClient.executeMethod(redirMethod);
2164                     wasRedirected = true;
2165 
2166                 }
2167 
2168                 if (wasRedirected) {
2169                     currentBytes = redirMethod.getResponseBody();
2170                 } else {
2171                     currentBytes = method.getResponseBody();
2172                 }
2173 
2174                 currentText = new String(currentBytes);
2175 
2176                 if (response == 200 && currentText == null) {
2177 
2178                     if (log.isDebugEnabled()) {
2179                         log.debug("Connection timeout expired.");
2180                     }
2181 
2182                     method.releaseConnection();
2183 
2184                     if (redirMethod != null) {
2185                         redirMethod.releaseConnection();
2186                     }
2187 
2188                     method = createHttpMethod(page, url, configuration);
2189 
2190                     continue;
2191 
2192                 }
2193 
2194             } catch (MalformedURLException e) {
2195 
2196                 exception = e;
2197                 exceptionMessage = "Invalid URL : " + url;
2198 
2199                 if (log.isDebugEnabled()) {
2200 
2201                     log.debug("HTTP response : " + response);
2202                     log.debug(exceptionMessage);
2203 
2204                 }
2205 
2206             } catch (UnknownHostException e) {
2207 
2208                 exception = e;
2209                 exceptionMessage = "URL not found : " + url;
2210 
2211                 if (log.isDebugEnabled()) {
2212 
2213                     log.debug("HTTP response : " + response);
2214                     log.debug(exceptionMessage);
2215 
2216                 }
2217 
2218                 method = recreateMethodObject(method, redirMethod, page, url,
2219                         configuration);
2220 
2221                 continue;
2222 
2223             } catch (IOException e) {
2224 
2225                 exceptionMessage = "Problems transferring action "
2226                         + page.getId() + " : " + e.getClass() + " : "
2227                         + e.getMessage();
2228 
2229                 if (log.isDebugEnabled()) {
2230 
2231                     log.debug("HTTP response : " + response);
2232                     log.debug(exceptionMessage);
2233 
2234                 }
2235 
2236                 exception = e;
2237 
2238                 method = recreateMethodObject(method, redirMethod, page, url,
2239                         configuration);
2240 
2241                 continue;
2242 
2243             }
2244 
2245             ok = true;
2246 
2247             end = System.currentTimeMillis();
2248 
2249             if (log.isDebugEnabled()) {
2250                 log.debug("Download elapsed time : " + (end - start) + " ms.");
2251             }
2252 
2253             break;
2254 
2255         }
2256 
2257         if (log.isDebugEnabled()) {
2258             log.debug("HTTP response : " + response);
2259         }
2260 
2261         if (response == -1 || response >= 400
2262                 || (followRedirects && response >= 300) || ok == false) {
2263 
2264             if (exceptionMessage == null && response == -1) {
2265                 exceptionMessage = "Connection timeout expired";
2266             } else {
2267                 exceptionMessage = "HTTP response : " + response;
2268             }
2269 
2270             final SinonException sinonException = new SinonException(
2271                     "Page " + page.getId()
2272                     + " could not be downloaded : "
2273                     + exceptionMessage, exception, this);
2274 
2275             ErrorResponse errorResponse = notifyErrorListener(sinonException);
2276 
2277             if (errorResponse.isStop()) {
2278                 throw sinonException;
2279             } else if (errorResponse.isResumeNext()) {
2280 
2281                 currentText = null;
2282                 return null;
2283 
2284             } else if (errorResponse.isRetry()) {
2285                 return download(page, action);
2286             }
2287 
2288         }
2289 
2290         if (currentText == null) {
2291 
2292             final String message = "Page " + page.getId()
2293                     + " could not be downloaded within timeout.";
2294 
2295             if (log.isErrorEnabled()) {
2296                 log.error(message);
2297             }
2298 
2299             throw new SinonException(message);
2300 
2301         }
2302 
2303         notifyPageListener(BEFORE_PROCESSING);
2304 
2305         bytesDownloaded += currentText.length();
2306         pagesDownloaded++;
2307 
2308         return currentText;
2309 
2310     }
2311 
2312     /***
2313      * Recreates a <code>Method</code> object.
2314      *
2315      * @param method
2316      * @param redirMethod
2317      * @param page
2318      * @param url
2319      * @param configuration
2320      * @return
2321      * @throws SinonException
2322      */
2323     private HttpMethod recreateMethodObject(HttpMethod method,
2324                                             GetMethod redirMethod,
2325                                             Page page,
2326                                             String url,
2327                                             PageConfiguration configuration)
2328             throws SinonException {
2329 
2330         method.releaseConnection();
2331 
2332         if (redirMethod != null) {
2333             redirMethod.releaseConnection();
2334         }
2335 
2336         method = createHttpMethod(page, url, configuration);
2337         return method;
2338 
2339     }
2340 
2341     /***
2342      * Halts the collector execution to simulate a think time.
2343      */
2344     private void waitForThinkTime() {
2345 
2346         try {
2347 
2348             final long millis = thinkTime.nextThinkTime();
2349             Thread.sleep(millis);
2350 
2351             if (log.isDebugEnabled()) {
2352                 log.debug("Think time interval: " + millis + " ms.");
2353             }
2354 
2355         } catch (InterruptedException e) {
2356 
2357             if (log.isWarnEnabled()) {
2358                 log.warn(e.getClass() + " : " + e.getMessage());
2359             }
2360 
2361         }
2362 
2363     }
2364 
2365     /***
2366      * Creates a <code>Method</code> instance to download a action.
2367      *
2368      * @param page a {@link Page} instance.
2369      * @param url a <code>String</code>.
2370      * @param configuration a {@link PageConfiguration} instance.
2371      * @return a <code>HttpMethod</code> instance.
2372      * @throws SinonException if some error ocurrs.
2373      */
2374     private HttpMethod createHttpMethod(Page page,
2375                                         String url,
2376                                         PageConfiguration configuration)
2377             throws SinonException {
2378 
2379         HttpMethod method;
2380         ParameterSet parameterSet;
2381 
2382         try {
2383 
2384             if (page.getMethod() == Page.POST_METHOD) {
2385                 method = new PostMethod(url);
2386             } else {
2387                 method = new GetMethod(url);
2388             }
2389 
2390         } catch (IllegalArgumentException e) {
2391             throw new SinonException("Invalid URL : " + url, e, this);
2392         }
2393 
2394         if (configuration != null) {
2395             parameterSet = configuration.getParameterSet();
2396         } else {
2397             parameterSet = null;
2398         }
2399 
2400         //NameValuePair[] parameters;
2401 
2402         if (parameterSet != null && parameterSet.getParameters().size() > 0) {
2403 
2404             String queryString = getQueryString(parameterSet);
2405 
2406             // parameters = getNameValuePairArray(parameterSet);
2407 
2408             if (page.getMethod() == Page.POST_METHOD) {
2409 
2410                 PostMethod postMethod = (PostMethod) method;
2411                 // postMethod.setRequestBody(parameters);
2412                 postMethod.setRequestBody(queryString);
2413 
2414             } else {
2415                 // method.setQueryString(parameters);
2416                 method.setQueryString(queryString);
2417             }
2418 
2419         }
2420 
2421         return method;
2422 
2423     }
2424 
2425     /***
2426      * Returns a query string representing <code>parameterSet</code>.
2427      *
2428      * @param parameterSet a {@link ParameterSet} instance.
2429      * @return a <code>String</code>.
2430      * @throws SinonException if some error ocurrs.
2431      */
2432     private String getQueryString(ParameterSet parameterSet)
2433             throws SinonException {
2434 
2435         StringBuffer buffer = new StringBuffer();
2436         Map parameters = parameterSet.getParameters();
2437         Iterator iterator = parameters.keySet().iterator();
2438 
2439         while (iterator.hasNext()) {
2440 
2441             String name = (String) iterator.next();
2442             Parameter parameter = (Parameter) parameters.get(name);
2443             String value = evaluate(parameter.getValue());
2444             String encoding = parameter.getEncoding();
2445 
2446             try {
2447 
2448                 buffer.append(URLEncoder.encode(name, encoding));
2449                 buffer.append("=");
2450                 buffer.append(URLEncoder.encode(value, encoding));
2451                 buffer.append("&");
2452 
2453             } catch (UnsupportedEncodingException e) {
2454 
2455                 throw new SinonException("Unsupported character set : "
2456                         + encoding, e, this);
2457 
2458             }
2459 
2460         }
2461 
2462         // deletes the last &, as it is not useful.
2463         buffer.deleteCharAt(buffer.length() - 1);
2464 
2465         return buffer.toString();
2466 
2467     }
2468 
2469     /***
2470      * Logs some debug messages about the {@link Page} about to be downloaded.
2471      *
2472      * @param page a {@link Page} instance.
2473      * @param url a <code>String</code>.
2474      * @param method a <code>HttpMethod</code> instance.
2475      */
2476     private void logDebuggingMessages(Page page,
2477                                       String url,
2478                                       HttpMethod method) {
2479 
2480         log.debug("");
2481         log.debug("Starting download of action " + page.getId());
2482         log.debug("URL : " + url);
2483 
2484         if (page.getMethod() == Page.POST_METHOD) {
2485 
2486             PostMethod postMethod = (PostMethod) method;
2487 
2488             try {
2489 
2490                 log.debug("Query string: "
2491                         + postMethod.getRequestBodyAsString());
2492 
2493             } catch (Exception e) {
2494                 log.debug(e);
2495             }
2496 
2497             log.debug("Using POST method to send parameters.");
2498 
2499         } else {
2500 
2501             log.debug("Query string: " + method.getQueryString());
2502             log.debug("Using GET method to send parameters.");
2503 
2504         }
2505 
2506         Cookie[] cookies = httpClient.getState().getCookies();
2507 
2508         if (cookies.length > 0) {
2509 
2510             log.debug("Cookies:");
2511 
2512             for (int i = 0; i < cookies.length; i++) {
2513 
2514                 log.debug("\t" + cookies[i].getName() + " : "
2515                         + cookies[i].getValue());
2516 
2517             }
2518 
2519         }
2520 
2521     }
2522 
2523     /***
2524      * Notifies the current action's {@link ErrorListener}, if it has one,
2525      * that some exception ocurred and asks what to do.
2526      *
2527      * @param exception a {@link SinonException} instance.
2528      * @return a {@link ErrorResponse} instance..
2529      */
2530     private ErrorResponse notifyErrorListener(SinonException exception) {
2531 
2532         if (currentErrorListener != null) {
2533             return currentErrorListener.onError(exception);
2534         } else {
2535             return ErrorResponse.STOP;
2536         }
2537 
2538     }
2539 
2540     /***
2541      * Reads a text file and returns int in a <code>String</code>.
2542      *
2543      * @param filename a <code>String</code> containing the file name.
2544      * @return uma <code>String</code> containing the file contents.
2545      * @throws SinonException if some error ocurrs.
2546      */
2547     final private String readFile(String filename) throws SinonException {
2548 
2549         StringBuffer buffer = null;
2550 
2551         try {
2552 
2553             FileReader fileReader = new FileReader(filename);
2554             BufferedReader reader = new BufferedReader(fileReader);
2555 
2556             buffer = new StringBuffer();
2557 
2558             String line = reader.readLine();
2559 
2560             while (line != null) {
2561 
2562                 buffer.append(line);
2563                 line = reader.readLine();
2564 
2565             }
2566 
2567         } catch (FileNotFoundException e) {
2568 
2569             throw new SinonException("File not found : " + filename, e, this);
2570 
2571         } catch (IOException e) {
2572 
2573             throw new SinonException("Error reading file " + filename,
2574                     e, this);
2575 
2576         }
2577 
2578         return buffer.toString();
2579 
2580     }
2581 
2582     /***
2583      * Configures the logger object and deletes the current directory's
2584      * <code>sinon.log</code> file if it already exists.
2585      */
2586     private void configureLogger() {
2587 
2588         File file = new File(LOG_FILENAME);
2589 
2590         if (file.exists()) {
2591             file.delete();
2592         }
2593 
2594         Properties props = new Properties();
2595 
2596         InputStream is = this.getClass().getResourceAsStream(
2597                 "/" + LOG4J_PROPERTIES_FILENAME);
2598 
2599         try {
2600             props.load(is);
2601             PropertyConfigurator.configure(props);
2602             log = LogFactory.getLog(CollectorExecutor.class);
2603             log.info("Loaded log4j properties");
2604         } catch (RuntimeException e1) {
2605             System.out.println("log4J.properties not found. " +
2606                     "The logger will use default settings.");
2607             e1.printStackTrace();
2608         } catch (IOException e2) {
2609             System.out.println("Error loading log4j.properties");
2610             e2.printStackTrace();
2611         }
2612 
2613 
2614     }
2615 
2616     /***
2617      * Configures the think time.
2618      */
2619     private void configureThinkTime() throws SinonException {
2620 
2621         final CollectorConfiguration configuration =
2622                 collector.getConfiguration();
2623 
2624         String className = configuration.getThinkTimeImplementationClassName();
2625 
2626         Class thinkTimeClass = null;
2627 
2628         try {
2629 
2630             thinkTimeClass = Class.forName(className);
2631 
2632         } catch (ClassNotFoundException e) {
2633 
2634             final String message = "Class " + className
2635                     + ", eteg.sinon.executor.ThinkTime implementation, "
2636                     + "was not found.";
2637 
2638             throw new SinonException(message, e, this);
2639 
2640         }
2641 
2642         try {
2643 
2644             thinkTime = (ThinkTime) thinkTimeClass.newInstance();
2645 
2646         } catch (InstantiationException e) {
2647 
2648             final String message = "Could not create instance of class " +
2649                     className + ". Check if it is concrete and if it has "
2650                     + "a public constructor with no parameters.";
2651 
2652             throw new SinonException(message, e, this);
2653 
2654         } catch (IllegalAccessException e) {
2655 
2656             final String message = "Could not create instance of class " +
2657                     className + ". Check if it is concrete and if it has "
2658                     + "a public constructor with no parameters.";
2659 
2660             throw new SinonException(message, e, this);
2661 
2662         }
2663 
2664         thinkTime.setConfiguration(configuration.getThinkTimeProperties());
2665 
2666     }
2667 
2668     /***
2669      * Sets the HTTP headers of <code>method</code> according to the
2670      * collector's configuration.
2671      *
2672      * @param method a <code>HttpMethod</code> instance.
2673      */
2674     private void setHttpHeaders(HttpMethod method) {
2675 
2676         Enumeration keys = httpHeaders.keys();
2677 
2678         while (keys.hasMoreElements()) {
2679 
2680             String name = (String) keys.nextElement();
2681             String value = httpHeaders.getProperty(name);
2682             method.setRequestHeader(name, value);
2683 
2684         }
2685 
2686     }
2687 
2688     /***
2689      * Stops the execution of this collector.
2690      */
2691     public void stop() {
2692 
2693         if (log != null && log.isInfoEnabled()) {
2694             log.info("Stopping collector execution.");
2695         }
2696 
2697         this.mustStop = true;
2698 
2699     }
2700 
2701     /***
2702      * Returns <code>true</code> if the execution of this collector
2703      * was stopped and <code>false</code> otherwise.
2704      * @return <code>true</code> if the execution of this collector
2705      * was stopped and <code>false</code> otherwise.
2706      */
2707     public boolean isStopped() {
2708         return this.mustStop;
2709     }
2710 
2711     /***
2712      * Prints the content of <code>someContext</code>
2713      * to a String and returns it.
2714      *
2715      * @param someContext a <code>VelocityContext</code>.
2716      */
2717     private static String printContext(VelocityContext someContext) {
2718 
2719         if (someContext == null) {
2720 
2721             throw new IllegalArgumentException(
2722                     "Parameter someContext can't be null");
2723 
2724         }
2725 
2726         StringBuffer buffer = new StringBuffer();
2727 
2728         Object[] keys = someContext.getKeys();
2729         final int length = keys.length;
2730 
2731         for (int i = 0; i < length; i++) {
2732 
2733             final Object key = keys[i];
2734             buffer.append(key.toString().trim());
2735             buffer.append("=");
2736             buffer.append(someContext.get((String) key).toString().trim());
2737             buffer.append("");
2738 
2739             if (i < length - 1) {
2740                 buffer.append(", ");
2741             }
2742 
2743         }
2744 
2745         return buffer.toString();
2746 
2747     }
2748 
2749     /***
2750      * (Action, Velocity context) pair.
2751      */
2752     private final static class ActionContextPair {
2753 
2754         /***
2755          * Action to be executed.
2756          */
2757         private Action action;
2758 
2759         /***
2760          * Context under which the action will be executed.
2761          */
2762         private VelocityContext context;
2763 
2764         /***
2765          * Single constructor.
2766          *
2767          * @param action an {@link Action} instance.
2768          * @param context a {@link VelocityContext} instance.
2769          */
2770         public ActionContextPair(Action action, VelocityContext context) {
2771 
2772             if (action == null) {
2773 
2774                 throw new IllegalArgumentException(
2775                         "Parameter action can't be null");
2776 
2777             }
2778 
2779             if (context == null) {
2780 
2781                 throw new IllegalArgumentException(
2782                         "Parameter context can't be null");
2783 
2784             }
2785 
2786             this.action = action;
2787             this.context = context;
2788 
2789         }
2790 
2791         /***
2792          * Returns the context.
2793          *
2794          * @return a {@link VelocityContext} instance.
2795          */
2796         public VelocityContext getContext() {
2797             return context;
2798         }
2799 
2800         /***
2801          * Returns the action.
2802          *
2803          * @return an {@link Action} instance.
2804          */
2805         public Action getAction() {
2806             return action;
2807         }
2808 
2809         /***
2810          * Returns the action pageId and the contents of the context.
2811          *
2812          * @return a <code>String</code>.
2813          */
2814         public String toString() {
2815 
2816             StringBuffer buffer = new StringBuffer();
2817 
2818             buffer.append("Stack: page ");
2819             buffer.append(action.getPageId());
2820             buffer.append(", context ");
2821             buffer.append(printContext(context));
2822             buffer.append("");
2823 
2824             return buffer.toString();
2825 
2826         }
2827 
2828     }
2829 
2830 }