]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
587a41906fffd4cab2dc3b576d3f845648c4149b
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.IntegrationTests.xmlreplay;
2
3 import org.apache.commons.cli.*;
4
5 import org.apache.commons.io.FileUtils;
6 import org.apache.commons.jexl2.JexlContext;
7 import org.apache.commons.jexl2.JexlEngine;
8 import org.collectionspace.services.common.api.Tools;
9 import org.dom4j.*;
10 import org.dom4j.io.SAXReader;
11
12 import java.io.*;
13 import java.util.*;
14
15 /**  This class is used to replay a request to the Services layer, by sending the XML payload
16  *   in an appropriate Multipart request.
17  *   See example usage in calling class XmlReplayTest in services/IntegrationTests, and also in main() in this class.
18  *   @author Laramie Crocker
19  */
20 @SuppressWarnings("unchecked")
21 public class XmlReplay {
22
23     public XmlReplay(String basedir, String reportsDir){
24         this.basedir = basedir;
25         this.serviceResultsMap = createResultsMap();
26         this.reportsList = new ArrayList<String>();
27         this.reportsDir = reportsDir;
28     }
29
30     public static final String DEFAULT_CONTROL = "xml-replay-control.xml";
31     public static final String DEFAULT_MASTER_CONTROL = "xml-replay-master.xml";
32     public static final String DEFAULT_DEV_MASTER_CONTROL = "dev-master.xml";
33         private static final int MAX_REATTEMPTS = 10;
34         private static final String REATTEMPT_KEY = "REATTEMPT_KEY";
35
36     private String reportsDir = "";
37     public String getReportsDir(){
38         return reportsDir;
39     }
40     private String basedir = ".";  //set from constructor.
41     public String getBaseDir(){
42         return basedir;
43     }
44     
45     private String controlFileName = DEFAULT_CONTROL;
46     public String getControlFileName() {
47         return controlFileName;
48     }
49     public void setControlFileName(String controlFileName) {
50         this.controlFileName = controlFileName;
51     }
52
53     private String protoHostPort = "";
54     public String getProtoHostPort() {
55         return protoHostPort;
56     }
57     public void setProtoHostPort(String protoHostPort) {
58         this.protoHostPort = protoHostPort;
59     }
60
61     private boolean autoDeletePOSTS = true;
62     public boolean isAutoDeletePOSTS() {
63         return autoDeletePOSTS;
64     }
65     public void setAutoDeletePOSTS(boolean autoDeletePOSTS) {
66         this.autoDeletePOSTS = autoDeletePOSTS;
67     }
68
69     private Dump dump;
70     public Dump getDump() {
71         return dump;
72     }
73     public void setDump(Dump dump) {
74         this.dump = dump;
75     }
76
77     AuthsMap defaultAuthsMap;
78     public AuthsMap getDefaultAuthsMap(){
79         return defaultAuthsMap;
80     }
81     public void setDefaultAuthsMap(AuthsMap authsMap){
82         defaultAuthsMap = authsMap;
83     }
84
85     private Map<String, ServiceResult> serviceResultsMap;
86     public Map<String, ServiceResult> getServiceResultsMap(){
87         return serviceResultsMap;
88     }
89     public static Map<String, ServiceResult> createResultsMap(){
90         return new HashMap<String, ServiceResult>();
91     }
92
93     private List<String> reportsList;
94     public  List<String> getReportsList(){
95         return reportsList;
96     }
97
98     @Override
99         public String toString(){
100         return "XmlReplay{"+this.basedir+", "+this.defaultAuthsMap+", "+this.dump+", "+this.reportsDir+'}';
101     }
102
103     // ============== METHODS ===========================================================
104
105     /** Optional information method: call this method after instantiating this class using the constructor XmlReplay(String), which sets the basedir.  Then you
106      *   pass in your relative masterFilename to that basedir to this method, which will return true if the file is readable, valid xml, etc.
107      *   Do this in preference to  just seeing if File.exists(), because there are rules to finding the file relative to the maven test dir, yada, yada.
108      *   This method makes it easy to have a development test file that you don't check in, so that dev tests can be missing gracefully, etc.
109      */
110     public boolean masterConfigFileExists(String masterFilename){
111         try {
112             org.dom4j.Document doc = openMasterConfigFile(masterFilename);
113             if (doc == null){
114                 return false;
115             }
116             return true;
117         } catch (Throwable t){
118             return false;
119         }
120     }
121
122     public org.dom4j.Document openMasterConfigFile(String masterFilename) throws FileNotFoundException {
123         String fullPath = Tools.glue(basedir, "/", masterFilename);
124         File f = new File(fullPath);
125         if (!f.exists()){
126             return null;
127         }
128         org.dom4j.Document document = getDocument(fullPath); //will check full path first, then checks relative to PWD.
129         if (document == null){
130             throw new FileNotFoundException("XmlReplay master control file ("+masterFilename+") not found in basedir: "+basedir+". Exiting test.");
131         }
132         return document;
133     }
134
135     /** specify the master config file, relative to getBaseDir(), but ignore any tests or testGroups in the master.
136      *  @return a Document object, which you don't need to use: all options will be stored in XmlReplay instance.
137      */
138     public org.dom4j.Document readOptionsFromMasterConfigFile(String masterFilename) throws FileNotFoundException {
139         org.dom4j.Document document = openMasterConfigFile(masterFilename);
140         if (document == null){
141             throw new FileNotFoundException(masterFilename);
142         }
143         protoHostPort = document.selectSingleNode("/xmlReplayMaster/protoHostPort").getText().trim();
144         AuthsMap authsMap = readAuths(document);
145         setDefaultAuthsMap(authsMap);
146         Dump dump = XmlReplay.readDumpOptions(document);
147         setDump(dump);
148         return document;
149     }
150
151     public List<List<ServiceResult>> runMaster(String masterFilename) throws Exception {
152         return runMaster(masterFilename, true);
153     }
154
155     /** Creates new instances of XmlReplay, one for each controlFile specified in the master,
156      *  and setting defaults from this instance, but not sharing ServiceResult objects or maps. */
157         public List<List<ServiceResult>> runMaster(String masterFilename, boolean readOptionsFromMaster) throws Exception {
158         List<List<ServiceResult>> list = new ArrayList<List<ServiceResult>>();
159         org.dom4j.Document document;
160         if (readOptionsFromMaster){
161             document = readOptionsFromMasterConfigFile(masterFilename);
162         } else {
163             document = openMasterConfigFile(masterFilename);
164         }
165         if (document==null){
166             throw new FileNotFoundException(masterFilename);
167         }
168         String controlFile, testGroup, test;
169         List<Node> runNodes;
170         runNodes = document.selectNodes("/xmlReplayMaster/run");
171         for (Node runNode : runNodes) {
172             controlFile = runNode.valueOf("@controlFile");
173             testGroup = runNode.valueOf("@testGroup");
174             test = runNode.valueOf("@test"); //may be empty
175
176             //Create a new instance and clone only config values, not any results maps.
177             XmlReplay replay = new XmlReplay(basedir, this.reportsDir);
178             replay.setControlFileName(controlFile);
179             replay.setProtoHostPort(protoHostPort);
180             replay.setAutoDeletePOSTS(isAutoDeletePOSTS());
181             replay.setDump(dump);
182             replay.setDefaultAuthsMap(getDefaultAuthsMap());
183
184             //Now run *that* instance.
185             List<ServiceResult> results = replay.runTests(testGroup, test);
186             list.add(results);
187             this.reportsList.addAll(replay.getReportsList());   //Add all the reports from the inner replay, to our master replay's reportsList, to generate the index.html file.
188         }
189         XmlReplayReport.saveIndexForMaster(basedir, reportsDir, masterFilename, this.reportsList);
190         return list;
191     }
192
193     /** Use this if you wish to run named tests within a testGroup, otherwise call runTestGroup(). */
194     public List<ServiceResult>  runTests(String testGroupID, String testID) throws Exception {
195         List<ServiceResult> result = runXmlReplayFile(this.basedir,
196                                 this.controlFileName,
197                                 testGroupID,
198                                 testID,
199                                 this.serviceResultsMap,
200                                 this.autoDeletePOSTS,
201                                 dump,
202                                 this.protoHostPort,
203                                 this.defaultAuthsMap,
204                                 this.reportsList,
205                                 this.reportsDir);
206         return result;
207     }
208
209     /** Use this if you wish to specify just ONE test to run within a testGroup, otherwise call runTestGroup(). */
210     public ServiceResult  runTest(String testGroupID, String testID) throws Exception {
211         List<ServiceResult> result = runXmlReplayFile(this.basedir,
212                                 this.controlFileName,
213                                 testGroupID,
214                                 testID,
215                                 this.serviceResultsMap,
216                                 this.autoDeletePOSTS,
217                                 dump,
218                                 this.protoHostPort,
219                                 this.defaultAuthsMap,
220                                 this.reportsList,
221                                 this.reportsDir);
222         if (result.size()>1){
223             throw new IndexOutOfBoundsException("Multiple ("+result.size()+") tests with ID='"+testID+"' were found within test group '"+testGroupID+"', but there should only be one test per ID attribute.");
224         }
225         return result.get(0);
226     }
227
228     /** Use this if you wish to run all tests within a testGroup.*/
229     public List<ServiceResult> runTestGroup(String testGroupID) throws Exception {
230         //NOTE: calling runTest with empty testID runs all tests in a test group, but don't expose this fact.
231         // Expose this method (runTestGroup) instead.
232         return runTests(testGroupID, "");
233     }
234
235     public List<ServiceResult> autoDelete(String logName){
236         return autoDelete(this.defaultAuthsMap, this.serviceResultsMap, logName, 0);
237     }
238
239     /** Use this method to clean up resources created on the server that returned CSIDs, if you have
240      *  specified autoDeletePOSTS==false, which means you are managing the cleanup yourself.
241      * @param serviceResultsMap a Map of ServiceResult objects, which will contain ServiceResult.deleteURL.
242      * @return a List<String> of debug info about which URLs could not be deleted.
243      */
244     private static List<ServiceResult> autoDelete(AuthsMap defaultAuths, Map<String, ServiceResult> serviceResultsMap, String logName, int reattempt) {
245         List<ServiceResult> results = new ArrayList<ServiceResult>();
246         HashMap<String, ServiceResult> reattemptList = new HashMap<String, ServiceResult>();
247         int deleteFailures = 0;
248         for (ServiceResult pr : serviceResultsMap.values()) {
249             try {
250                 if (pr.autoDelete == true && Tools.notEmpty(pr.deleteURL)){
251                     ServiceResult deleteResult = XmlReplayTransport.doDELETE(pr.deleteURL, pr.adminAuth, pr.testID, "[autodelete:"+logName+"]");
252                     if (deleteResult.gotExpectedResult() == false || deleteResult.responseCode != 200) {
253                         reattemptList.put(REATTEMPT_KEY + deleteFailures++, pr); // We need to try again after our dependents have been deleted. cow()
254                     }
255                     results.add(deleteResult);
256                 } else {
257                     ServiceResult errorResult = new ServiceResult();
258                     errorResult.fullURL = pr.fullURL;
259                     errorResult.testGroupID = pr.testGroupID;
260                     errorResult.fromTestID = pr.fromTestID;
261                     errorResult.overrideGotExpectedResult();
262                     results.add(errorResult);
263                 }
264             } catch (Throwable t){
265                 String s = (pr!=null) ? "ERROR while cleaning up ServiceResult map: "+pr+" for "+pr.deleteURL+" :: "+t
266                                       : "ERROR while cleaning up ServiceResult map (null ServiceResult): "+t;
267                 System.err.println(s);
268                 ServiceResult errorResult = new ServiceResult();
269                 errorResult.fullURL = pr.fullURL;
270                 errorResult.testGroupID = pr.testGroupID;
271                 errorResult.fromTestID = pr.fromTestID;
272                 errorResult.error = s;
273                 results.add(errorResult);
274             }
275         }
276         //
277         // If there were things we had trouble deleting, it might have been because they had dependents that
278         // needed to be deleted first.  Therefore, we're going to try again and again (recursively) up until we reach
279         // our MAX_REATTEMPTS limit.
280         //
281         if (reattemptList.size() > 0 && reattempt < MAX_REATTEMPTS) {
282                 return autoDelete(defaultAuths, reattemptList, logName, ++reattempt); // recursive call
283         }
284         
285         return results;
286     }
287
288     public static class AuthsMap {
289         Map<String,String> map;
290         String defaultID="";
291         public String getDefaultAuth(){
292             return map.get(defaultID);
293         }
294         @Override
295                 public String toString(){
296             return "AuthsMap: {default='"+defaultID+"'; "+map.keySet()+'}';
297         }
298     }
299
300     public static AuthsMap readAuths(org.dom4j.Document document){
301     Map<String, String> map = new HashMap<String, String>();
302         List<Node> authNodes = document.selectNodes("//auths/auth");
303         for (Node auth : authNodes) {
304             map.put(auth.valueOf("@ID"), auth.getStringValue());
305         }
306         AuthsMap authsMap = new AuthsMap();
307         Node auths = document.selectSingleNode("//auths");
308         String defaultID = "";
309         if (auths != null){
310             defaultID = auths.valueOf("@default");
311         }
312         authsMap.map = map;
313         authsMap.defaultID = defaultID;
314         return authsMap;
315     }
316
317     public static class Dump {
318         public boolean payloads = false;
319         //public static final ServiceResult.DUMP_OPTIONS dumpServiceResultOptions = ServiceResult.DUMP_OPTIONS;
320         public ServiceResult.DUMP_OPTIONS dumpServiceResult = ServiceResult.DUMP_OPTIONS.minimal;
321         @Override
322                 public String toString(){
323             return "payloads: "+payloads+" dumpServiceResult: "+dumpServiceResult;
324         }
325     }
326
327     public static Dump getDumpConfig(){
328         return new Dump();
329     }
330
331     public static Dump readDumpOptions(org.dom4j.Document document){
332         Dump dump = getDumpConfig();
333         Node dumpNode = document.selectSingleNode("//dump");
334         if (dumpNode != null){
335             dump.payloads = Tools.isTrue(dumpNode.valueOf("@payloads"));
336             String dumpServiceResultStr = dumpNode.valueOf("@dumpServiceResult");
337             if (Tools.notEmpty(dumpServiceResultStr)){
338                 dump.dumpServiceResult = ServiceResult.DUMP_OPTIONS.valueOf(dumpServiceResultStr);
339             }
340         }
341         return dump;
342     }
343
344     private static class PartsStruct {
345         public List<Map<String,String>> varsList = new ArrayList<Map<String,String>>();
346         String responseFilename = "";
347         String overrideTestID = "";
348         String startElement = "";
349         String label = "";
350
351                 public static PartsStruct readParts(Node testNode, final String testID, String xmlReplayBaseDir) {
352                         PartsStruct resultPartsStruct = new PartsStruct();
353                         resultPartsStruct.responseFilename = testNode.valueOf("filename");
354                         resultPartsStruct.startElement = testNode.valueOf("startElement");
355                         resultPartsStruct.label = testNode.valueOf("label");
356                         String responseFilename = testNode.valueOf("filename");
357                         if (Tools.notEmpty(responseFilename)) {
358                                 resultPartsStruct.responseFilename = xmlReplayBaseDir + '/' + responseFilename;
359                                 List<Node> varNodes = testNode.selectNodes("vars/var");
360                                 readVars(testNode, varNodes, resultPartsStruct);
361                         }
362                         return resultPartsStruct;
363                 }
364
365         private static void readVars(Node testNode, List<Node> varNodes, PartsStruct resultPartsStruct) {
366             Map<String,String> vars = new HashMap<String,String>();
367             resultPartsStruct.varsList.add(vars);
368             //System.out.println("### vars: "+vars.size()+" ########");
369             for (Node var: varNodes){
370                 String ID = var.valueOf("@ID");
371                 String value = var.getText();
372                 //System.out.println("ID: "+ID+" value: "+value);
373                 vars.put(ID, value); //vars is already part of resultPartsStruct.varsList
374             }
375             //System.out.println("### end-vars ########");
376         }
377     }
378
379
380
381     private static String fixupFullURL(String fullURL, String protoHostPort, String uri){
382         if ( ! uri.startsWith(protoHostPort)){
383             fullURL = Tools.glue(protoHostPort, "/", uri);
384         } else {
385             fullURL = uri;
386         }
387         return fullURL;
388     }
389
390     private static String fromTestID(String fullURL, Node testNode, Map<String, ServiceResult> serviceResultsMap){
391         String fromTestID = testNode.valueOf("fromTestID");
392         if (Tools.notEmpty(fromTestID)){
393             ServiceResult getPR = serviceResultsMap.get(fromTestID);
394             if (getPR != null){
395                 fullURL = Tools.glue(fullURL, "/", getPR.location);
396             }
397         }
398         return fullURL;
399     }
400
401     private static String CSIDfromTestID(Node testNode, Map<String, ServiceResult> serviceResultsMap){
402         String result = "";
403         String fromTestID = testNode.valueOf("fromTestID");
404         if (Tools.notEmpty(fromTestID)){
405             ServiceResult getPR = serviceResultsMap.get(fromTestID);
406             if (getPR != null){
407                 result = getPR.location;
408             }
409         }
410         return result;
411     }
412
413     public static org.dom4j.Document getDocument(String xmlFileName) {
414         org.dom4j.Document document = null;
415         SAXReader reader = new SAXReader();
416         try {
417             document = reader.read(xmlFileName);
418         } catch (DocumentException e) {
419             System.out.println("ERROR reading document: "+e);
420             //e.printStackTrace();
421         }
422         return document;
423     }
424
425     protected static String validateResponseSinglePayload(ServiceResult serviceResult,
426                                                  Map<String, ServiceResult> serviceResultsMap,
427                                                  PartsStruct expectedResponseParts,
428                                                  XmlReplayEval evalStruct)
429     throws Exception {
430         String OK = "";
431         byte[] b = FileUtils.readFileToByteArray(new File(expectedResponseParts.responseFilename));
432         String expectedPartContent = new String(b);
433         Map<String,String> vars = expectedResponseParts.varsList.get(0);  //just one part, so just one varsList.  Must be there, even if empty.
434         expectedPartContent = XmlReplayEval.eval(expectedPartContent, serviceResultsMap, vars, evalStruct.jexl, evalStruct.jc);
435         serviceResult.expectedContentExpanded = expectedPartContent;
436         String label = "NOLABEL";
437         String leftID  = "{from expected part, label:"+label+" filename: "+expectedResponseParts.responseFilename+"}";
438         String rightID = "{from server, label:"+label
439                             +" fromTestID: "+serviceResult.fromTestID
440                             +" URL: "+serviceResult.fullURL
441                             +"}";
442         String startElement = expectedResponseParts.startElement;
443         String partLabel = expectedResponseParts.label;
444         if (Tools.isBlank(startElement)){
445             if (Tools.notBlank(partLabel))
446             startElement = "/document/*[local-name()='"+partLabel+"']";
447         }
448         TreeWalkResults.MatchSpec matchSpec = TreeWalkResults.MatchSpec.createDefault();
449         TreeWalkResults list =
450             XmlCompareJdom.compareParts(expectedPartContent,
451                                         leftID,
452                                         serviceResult.result,
453                                         rightID,
454                                         startElement,
455                                         matchSpec);
456         serviceResult.addPartSummary(label, list);
457         return OK;
458     }
459
460     protected static String validateResponse(ServiceResult serviceResult,
461                                              Map<String, ServiceResult> serviceResultsMap,
462                                              PartsStruct expectedResponseParts,
463                                              XmlReplayEval evalStruct){
464         String OK = "";
465         if (expectedResponseParts == null) return OK;
466         if (serviceResult == null) return OK;
467         if (serviceResult.result.length() == 0) return OK;
468         try {
469             return validateResponseSinglePayload(serviceResult, serviceResultsMap, expectedResponseParts, evalStruct);
470         } catch (Exception e){
471             String err = "ERROR in XmlReplay.validateResponse() : "+e;
472             return err  ;
473         }
474     }
475
476     //================= runXmlReplayFile ======================================================
477
478     public static List<ServiceResult> runXmlReplayFile(String xmlReplayBaseDir,
479                                           String controlFileName,
480                                           String targetedTestGroupID,
481                                           String oneTestID,
482                                           Map<String, ServiceResult> serviceResultsMap,
483                                           boolean param_autoDeletePOSTS,
484                                           Dump dump,
485                                           String protoHostPortParam,
486                                           AuthsMap defaultAuths,
487                                           List<String> reportsList,
488                                           String reportsDir)
489                                           throws Exception {
490         //Internally, we maintain two collections of ServiceResult:
491         //  the first is the return value of this method.
492         //  the second is the serviceResultsMap, which is used for keeping track of CSIDs created by POSTs, for later reference by DELETE, etc.
493         List<ServiceResult> results = new ArrayList<ServiceResult>();
494
495         XmlReplayReport report = new XmlReplayReport(reportsDir);
496
497         String controlFile = Tools.glue(xmlReplayBaseDir, "/", controlFileName);
498         org.dom4j.Document document;
499         document = getDocument(controlFile); //will check full path first, then checks relative to PWD.
500         if (document==null){
501             throw new FileNotFoundException("XmlReplay control file ("+controlFileName+") not found in basedir: "+xmlReplayBaseDir+" Exiting test.");
502         }
503         String protoHostPort;
504         if (Tools.isEmpty(protoHostPortParam)){
505             protoHostPort = document.selectSingleNode("/xmlReplay/protoHostPort").getText().trim();
506             System.out.println("DEPRECATED: Using protoHostPort ('"+protoHostPort+"') from xmlReplay file ('"+controlFile+"'), not master.");
507         } else {
508             protoHostPort = protoHostPortParam;
509         }
510         if (Tools.isEmpty(protoHostPort)){
511             throw new Exception("XmlReplay control file must have a protoHostPort element");
512         }
513
514         String authsMapINFO;
515         AuthsMap authsMap = readAuths(document);
516         if (authsMap.map.size()==0){
517             authsMap = defaultAuths;
518             authsMapINFO = "Using defaultAuths from master file: "+defaultAuths;
519         } else {
520             authsMapINFO = "Using AuthsMap from control file: "+authsMap;
521         }
522
523         report.addTestGroup(targetedTestGroupID, controlFileName);   //controlFileName is just the short name, without the full path.
524         String xmlReplayHeader = "========================================================================"
525                           +"\r\nXmlReplay running:"
526                           +"\r\n   controlFile: "+ (new File(controlFile).getCanonicalPath())
527                           +"\r\n   protoHostPort: "+protoHostPort
528                           +"\r\n   testGroup: "+targetedTestGroupID
529                           + (Tools.notEmpty(oneTestID) ? "\r\n   oneTestID: "+oneTestID : "")
530                           +"\r\n   AuthsMap: "+authsMapINFO
531                           +"\r\n   param_autoDeletePOSTS: "+param_autoDeletePOSTS
532                           +"\r\n   Dump info: "+dump
533                           +"\r\n========================================================================"
534                           +"\r\n";
535         report.addRunInfo(xmlReplayHeader);
536
537         System.out.println(xmlReplayHeader);
538
539         String autoDeletePOSTS = "";
540         String authIDForCleanup = "";
541         List<Node> testgroupNodes;
542         if (Tools.notEmpty(targetedTestGroupID)){
543             testgroupNodes = document.selectNodes("//testGroup[@ID='"+targetedTestGroupID+"']");
544         } else {
545             testgroupNodes = document.selectNodes("//testGroup");
546         }
547
548         JexlEngine jexl = new JexlEngine();   // Used for expression language expansion from uri field.
549         XmlReplayEval evalStruct = new XmlReplayEval();
550         evalStruct.serviceResultsMap = serviceResultsMap;
551         evalStruct.jexl = jexl;
552
553         for (Node testgroup : testgroupNodes) {
554                 String testGroupID = testgroup.valueOf("@autoDeletePOSTS");
555             XmlReplayEval.MapContextWKeys jc = new XmlReplayEval.MapContextWKeys();//MapContext();  //Get a new JexlContext for each test group.
556             evalStruct.jc = jc;
557             autoDeletePOSTS = testgroup.valueOf("@autoDeletePOSTS");
558             //
559             // Decide which auth to use for POST request cleanups
560             //
561             String authForCleanup = null;
562             authIDForCleanup = testgroup.valueOf("@authForCleanup");
563             if (Tools.isEmpty(authIDForCleanup) == false) {
564                 authForCleanup = authsMap.map.get(authIDForCleanup);
565                 if (Tools.isEmpty(authForCleanup)) {
566                         String msg = String.format("The 'authForCleanup' attribute value '%s' declared for test group '%s' is not defined.",
567                                         authIDForCleanup, testGroupID);
568                         throw new Exception(msg);
569                 }
570             }
571             authForCleanup = authForCleanup != null ? authForCleanup : defaultAuths.getDefaultAuth();            
572             
573             List<Node> tests;
574             if (Tools.notEmpty(oneTestID)){
575                 tests = testgroup.selectNodes("test[@ID='"+oneTestID+"']");
576             } else {
577                 tests = testgroup.selectNodes("test");
578             }
579             String authForTest = ""; // reset auth for each test group
580             int testElementIndex = -1;
581
582             for (Node testNode : tests) {
583                 long startTime = System.currentTimeMillis();
584                 try {
585                     testElementIndex++;
586                     String testID = testNode.valueOf("@ID");
587                     //
588                     // Figure out if we will auto delete resources
589                     boolean autoDelete = param_autoDeletePOSTS;
590                     String autoDeleteValue = testNode.valueOf("@autoDeletePOSTS");
591                     if (autoDeleteValue != null && !autoDeleteValue.trim().isEmpty()) {
592                         autoDelete = Boolean.valueOf(autoDeleteValue).booleanValue();
593                     }
594                     
595                     String testIDLabel = Tools.notEmpty(testID) ? (testGroupID+'.'+testID) : (testGroupID+'.'+testElementIndex);
596                     String method = testNode.valueOf("method");
597                     String contentType = testNode.valueOf("contentType");
598                     String uri = testNode.valueOf("uri");
599                     String fullURL = Tools.glue(protoHostPort, "/", uri);
600
601                     if (contentType == null || contentType.equals("")) {
602                         contentType = XmlReplayTransport.APPLICATION_XML;
603                     }
604                     
605                     String currentAuthForTest = null;
606                     String authIDForTest = testNode.valueOf("@auth");
607                     if (Tools.notEmpty(authIDForTest)) {
608                         currentAuthForTest = authsMap.map.get(authIDForTest);
609                         if (currentAuthForTest == null) {
610                                 String msg = String.format("The 'auth' attribute value '%s' declared for test '%s' is not defined.",
611                                                 authIDForTest, testIDLabel);
612                                 throw new Exception(msg);
613                         }
614                     } else {
615                         String tokenAuthExpression = testNode.valueOf("@tokenauth");
616                         if (Tools.notEmpty(tokenAuthExpression)){
617                             currentAuthForTest = "Bearer " + XmlReplayEval.eval(tokenAuthExpression, serviceResultsMap, null, jexl, jc);
618                         }
619                     }
620                     
621                     if (Tools.notEmpty(currentAuthForTest)) {
622                         authForTest = currentAuthForTest; //else just run with current from last loop;
623                     }
624                     if (Tools.isEmpty(authForTest)) {
625                         authForTest = defaultAuths.getDefaultAuth();
626                     }                    
627
628                     if (uri.indexOf("$")>-1){
629                         uri = XmlReplayEval.eval(uri, serviceResultsMap, null, jexl, jc);
630                     }
631                     fullURL = fixupFullURL(fullURL, protoHostPort, uri);
632
633                     List<Integer> expectedCodes = new ArrayList<Integer>();
634                     String expectedCodesStr = testNode.valueOf("expectedCodes");
635                     if (Tools.notEmpty(expectedCodesStr)){
636                          String[] codesArray = expectedCodesStr.split(",");
637                          for (String code : codesArray){
638                              expectedCodes.add(new Integer(code.trim()));
639                          }
640                     }
641
642                     Node responseNode = testNode.selectSingleNode("response");
643                     PartsStruct expectedResponseParts = null;
644                     if (responseNode!=null){
645                         expectedResponseParts = PartsStruct.readParts(responseNode, testID, xmlReplayBaseDir);
646                         //System.out.println("reponse parts: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"+expectedResponseParts);
647                     }
648
649                     ServiceResult serviceResult;
650                     boolean isPOST = method.equalsIgnoreCase("POST");
651                     boolean isPUT =  method.equalsIgnoreCase("PUT");
652                     if ( isPOST || isPUT ) {
653                         PartsStruct parts = PartsStruct.readParts(testNode, testID, xmlReplayBaseDir);
654                         if (Tools.notEmpty(parts.overrideTestID)) {
655                             testID = parts.overrideTestID;
656                         }
657                         if (isPOST){
658                             String csid = CSIDfromTestID(testNode, serviceResultsMap);
659                             if (Tools.notEmpty(csid)) uri = Tools.glue(uri, "/", csid+"/items/");
660                         } else if (isPUT) {
661                             uri = fromTestID(uri, testNode, serviceResultsMap);
662                         }
663                         //vars only make sense in two contexts: POST/PUT, because you are submitting another file with internal expressions,
664                         // and in <response> nodes. For GET, DELETE, there is no payload, so all the URLs with potential expressions are right there in the testNode.
665                         Map<String,String> vars = null;
666                         if (parts.varsList.size()>0){
667                             vars = parts.varsList.get(0);
668                         }
669                         serviceResult = XmlReplayTransport.doPOST_PUTFromXML(parts.responseFilename, vars, protoHostPort, uri, method, contentType, evalStruct, authForTest, testIDLabel);
670                         serviceResult.autoDelete = autoDelete;
671                         if (vars!=null) {
672                             serviceResult.addVars(vars);
673                         }
674                         results.add(serviceResult);
675                         //if (isPOST){
676                             serviceResultsMap.put(testID, serviceResult);      //PUTs do not return a Location, so don't add PUTs to serviceResultsMap.
677                         //}
678                         fullURL = fixupFullURL(fullURL, protoHostPort, uri);
679                     } else if (method.equalsIgnoreCase("DELETE")){
680                         String fromTestID = testNode.valueOf("fromTestID");
681                         ServiceResult pr = Tools.notBlank(fromTestID) ? serviceResultsMap.get(fromTestID) : null;
682                         if (pr != null) {
683                             serviceResult = XmlReplayTransport.doDELETE(pr.deleteURL, authForTest, testIDLabel, fromTestID);
684                             serviceResult.fromTestID = fromTestID;
685                             if (expectedCodes.size()>0){
686                                 serviceResult.expectedCodes = expectedCodes;
687                             }
688                             results.add(serviceResult);
689                             if (serviceResult.codeInSuccessRange(serviceResult.responseCode)){  //gotExpectedResult depends on serviceResult.expectedCodes.
690                                 serviceResultsMap.remove(fromTestID);
691                             }
692                         } else {
693                             if (Tools.notEmpty(fromTestID)){
694                                 serviceResult = new ServiceResult();
695                                 serviceResult.responseCode = 0;
696                                 serviceResult.error = "ID not found in element fromTestID: "+fromTestID;
697                                 System.err.println("****\r\nServiceResult: "+serviceResult.error+". SKIPPING TEST. Full URL: "+fullURL);
698                             } else {
699                                 serviceResult = XmlReplayTransport.doDELETE(fullURL, authForTest, testID, fromTestID);
700                             }
701                             serviceResult.fromTestID = fromTestID;
702                             results.add(serviceResult);
703                         }
704                     } else if (method.equalsIgnoreCase("GET")){
705                         fullURL = fromTestID(fullURL, testNode, serviceResultsMap);
706                         serviceResult = XmlReplayTransport.doGET(fullURL, authForTest, testIDLabel);
707                         results.add(serviceResult);
708                         serviceResultsMap.put(testID, serviceResult);
709                     } else if (method.equalsIgnoreCase("LIST")){
710                         fullURL = fixupFullURL(fullURL, protoHostPort, uri);
711                         String listQueryParams = ""; //TODO: empty for now, later may pick up from XML control file.
712                         serviceResult = XmlReplayTransport.doLIST(fullURL, listQueryParams, authForTest, testIDLabel);
713                         results.add(serviceResult);
714                         serviceResultsMap.put(testID, serviceResult);
715                     } else {
716                         throw new Exception("HTTP method not supported by XmlReplay: "+method);
717                     }
718
719                     serviceResult.testID = testID;
720                     serviceResult.fullURL = fullURL;
721                     serviceResult.auth = authForTest;
722                     serviceResult.adminAuth = authForCleanup;
723                     serviceResult.method = method;
724                     if (expectedCodes.size()>0){
725                         serviceResult.expectedCodes = expectedCodes;
726                     }
727                     if (Tools.isEmpty(serviceResult.testID)) serviceResult.testID = testIDLabel;
728                     if (Tools.isEmpty(serviceResult.testGroupID)) serviceResult.testGroupID = testGroupID;
729
730                     Node expectedLevel = testNode.selectSingleNode("response/expected");
731                     if (expectedLevel!=null){
732                         String level = expectedLevel.valueOf("@level");
733                         serviceResult.payloadStrictness = level;
734                     }
735                     //=====================================================
736                     //  ALL VALIDATION FOR ALL REQUESTS IS DONE HERE:
737                     //=====================================================
738                     boolean hasError = false;
739                     String vError = validateResponse(serviceResult, serviceResultsMap, expectedResponseParts, evalStruct);
740                     if (Tools.notEmpty(vError)){
741                         serviceResult.error = vError;
742                         serviceResult.failureReason = " : VALIDATION ERROR; ";
743                         hasError = true;
744                     }
745                     if (hasError == false){
746                         hasError = ! serviceResult.gotExpectedResult();
747                     }
748
749                     report.addTestResult(serviceResult);
750
751                     if ((dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.detailed) || (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.full)) {
752                         System.out.println("\r\n#---------------------#");
753                     }
754                     
755                     String leader = (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.detailed) ? "XmlReplay:" + testIDLabel +": ": "";                    
756                     String serviceResultRow = serviceResult.dump(dump.dumpServiceResult, hasError) + "; time:" + (System.currentTimeMillis()-startTime);                
757                     //System.out.println(timeString() + "\n" + leader + serviceResultRow + "\r\n");
758                     System.out.println(String.format("Time: %s\nLeader: %s\nAuth: %s\nResult: %s\r\n", 
759                                 timeString(), testIDLabel, Tools.notEmpty(authIDForTest) ? authIDForTest : "<inferred>", serviceResultRow));
760                     
761                     boolean doingAuto = (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.auto);                    
762                     if (dump.payloads || (doingAuto&&hasError) ) {
763                         if (Tools.notBlank(serviceResult.requestPayload)){
764                             System.out.println("\r\n========== request payload ===============");
765                             System.out.println(serviceResult.requestPayload);
766                             System.out.println("==========================================\r\n");
767                         }
768                     }
769                     if (dump.payloads || (doingAuto&&hasError)) {
770                         if (Tools.notBlank(serviceResult.result)){
771                             System.out.println("\r\n========== response payload ==============");
772                             System.out.println(serviceResult.result);
773                             System.out.println("==========================================\r\n");
774                         }
775                     }
776                 } catch (Throwable t) {
777                     String msg = "ERROR: XmlReplay experienced an error in a test node: "+testNode+" Throwable: "+t;
778                     System.out.println(msg);
779                     System.out.println(Tools.getStackTrace(t));
780                     ServiceResult serviceResult = new ServiceResult();
781                     serviceResult.error = msg;
782                     serviceResult.failureReason = " : SYSTEM ERROR; ";
783                     results.add(serviceResult);
784                 }
785             }
786             if (Tools.isTrue(autoDeletePOSTS) && param_autoDeletePOSTS){
787                 autoDelete(defaultAuths, serviceResultsMap, "default", 0);
788             }
789         }
790
791         //=== Now spit out the HTML report file ===
792         File m = new File(controlFileName);
793         String localName = m.getName();//don't instantiate, just use File to extract file name without directory.
794         String reportName = localName+'-'+targetedTestGroupID+".html";
795
796         File resultFile = report.saveReport(xmlReplayBaseDir, reportsDir, reportName);
797         if (resultFile!=null) {
798             String toc = report.getTOC(reportName);
799             reportsList.add(toc);
800         }
801
802         return results;
803     }
804
805         private static String timeString() {
806                 java.util.Date date = new java.util.Date();
807                 java.sql.Timestamp ts = new java.sql.Timestamp(date.getTime());
808                 return ts.toString();
809         }
810
811     //======================== MAIN ===================================================================
812
813     private static Options createOptions() {
814         Options options = new Options();
815         options.addOption("xmlReplayBaseDir", true, "default/basedir");
816         return options;
817     }
818
819     public static String usage(){
820         String result = "org.collectionspace.services.IntegrationTests.xmlreplay.XmlReplay {args}\r\n"
821                         +"  -xmlReplayBaseDir <dir> \r\n"
822                         +" You may also override these with system args, e.g.: \r\n"
823                         +"   -DxmlReplayBaseDir=/path/to/dir \r\n"
824                         +" These may also be passed in via the POM.\r\n"
825                         +" You can also set these system args, e.g.: \r\n"
826                         +"  -DtestGroupID=<oneID> \r\n"
827                         +"  -DtestID=<one TestGroup ID>"
828                         +"  -DautoDeletePOSTS=<true|false> \r\n"
829                         +"    (note: -DautoDeletePOSTS won't force deletion if set to false in control file.";
830         return result;
831     }
832
833     private static String opt(CommandLine line, String option){
834         String result;
835         String fromProps = System.getProperty(option);
836         if (Tools.notEmpty(fromProps)){
837             return fromProps;
838         }
839         if (line==null){
840             return "";
841         }
842         result = line.getOptionValue(option);
843         if (result == null){
844             result = "";
845         }
846         return result;
847     }
848
849         public static void main(String[]args) throws Exception {
850         Options options = createOptions();
851         //System.out.println("System CLASSPATH: "+prop.getProperty("java.class.path", null));
852         CommandLineParser parser = new GnuParser();
853         try {
854             // parse the command line arguments
855             CommandLine line = parser.parse(options, args);
856
857             String xmlReplayBaseDir = opt(line, "xmlReplayBaseDir");
858             String reportsDir = opt(line, "reportsDir");
859             String testGroupID      = opt(line, "testGroupID");
860             String testID           = opt(line, "testID");
861             String autoDeletePOSTS  = opt(line, "autoDeletePOSTS");
862             String dumpResults      = opt(line, "dumpResults");
863             String controlFilename   = opt(line, "controlFilename");
864             String xmlReplayMaster  = opt(line, "xmlReplayMaster");
865
866             if (Tools.isBlank(reportsDir)){
867                 reportsDir = xmlReplayBaseDir + XmlReplayTest.REPORTS_DIRNAME;
868             }
869             reportsDir = Tools.fixFilename(reportsDir);
870             xmlReplayBaseDir = Tools.fixFilename(xmlReplayBaseDir);
871             controlFilename = Tools.fixFilename(controlFilename);
872
873             boolean bAutoDeletePOSTS = true;
874             if (Tools.notEmpty(autoDeletePOSTS)) {
875                 bAutoDeletePOSTS = Tools.isTrue(autoDeletePOSTS);
876             }
877             boolean bDumpResults = false;
878             if (Tools.notEmpty(dumpResults)) {
879                 bDumpResults = Tools.isTrue(autoDeletePOSTS);
880             }
881             if (Tools.isEmpty(xmlReplayBaseDir)){
882                 System.err.println("ERROR: xmlReplayBaseDir was not specified.");
883                 return;
884             }
885             File f = new File(Tools.glue(xmlReplayBaseDir, "/", controlFilename));
886             if (Tools.isEmpty(xmlReplayMaster) && !f.exists()){
887                 System.err.println("Control file not found: "+f.getCanonicalPath());
888                 return;
889             }
890             File fMaster = new File(Tools.glue(xmlReplayBaseDir, "/", xmlReplayMaster));
891             if (Tools.notEmpty(xmlReplayMaster)  && !fMaster.exists()){
892                 System.err.println("Master file not found: "+fMaster.getCanonicalPath());
893                 return;
894             }
895
896             String xmlReplayBaseDirResolved = (new File(xmlReplayBaseDir)).getCanonicalPath();
897             System.out.println("XmlReplay ::"
898                             + "\r\n    xmlReplayBaseDir: "+xmlReplayBaseDir
899                             + "\r\n    xmlReplayBaseDir(resolved): "+xmlReplayBaseDirResolved
900                             + "\r\n    controlFilename: "+controlFilename
901                             + "\r\n    xmlReplayMaster: "+xmlReplayMaster
902                             + "\r\n    testGroupID: "+testGroupID
903                             + "\r\n    testID: "+testID
904                             + "\r\n    autoDeletePOSTS: "+bAutoDeletePOSTS
905                             + (Tools.notEmpty(xmlReplayMaster)
906                                        ? ("\r\n    will use master file: "+fMaster.getCanonicalPath())
907                                        : ("\r\n    will use control file: "+f.getCanonicalPath()) )
908                              );
909             
910             if (Tools.notEmpty(xmlReplayMaster)){
911                 if (Tools.notEmpty(controlFilename)){
912                     System.out.println("WARN: controlFilename: "+controlFilename+" will not be used because master was specified.  Running master: "+xmlReplayMaster);
913                 }
914                 XmlReplay replay = new XmlReplay(xmlReplayBaseDirResolved, reportsDir);
915                 replay.readOptionsFromMasterConfigFile(xmlReplayMaster);
916                 replay.setAutoDeletePOSTS(bAutoDeletePOSTS);
917                 Dump dumpFromMaster = replay.getDump();
918                 dumpFromMaster.payloads = Tools.isTrue(dumpResults);
919                 replay.setDump(dumpFromMaster);
920                 replay.runMaster(xmlReplayMaster, false); //false, because we already just read the options, and override a few.
921             } else {
922                 Dump dump = getDumpConfig();
923                 dump.payloads = Tools.isTrue(dumpResults);
924                 List<String> reportsList = new ArrayList<String>();
925                 runXmlReplayFile(xmlReplayBaseDirResolved, controlFilename, testGroupID, testID, createResultsMap(), bAutoDeletePOSTS, dump, "", null, reportsList, reportsDir);
926                 System.out.println("DEPRECATED: reportsList is generated, but not dumped: "+reportsList.toString());
927             }
928         } catch (ParseException exp) {
929             // oops, something went wrong
930             System.err.println("Cmd-line parsing failed.  Reason: " + exp.getMessage());
931             System.err.println(usage());
932         } catch (Exception e) {
933             System.out.println("Error : " + e.getMessage());
934             e.printStackTrace();
935         }
936     }
937
938 }