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