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