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