]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
NOJIRA: merging from branch sprint
authorLaramie Crocker <laramie@berkeley.edu>
Tue, 9 Nov 2010 20:51:09 +0000 (20:51 +0000)
committerLaramie Crocker <laramie@berkeley.edu>
Tue, 9 Nov 2010 20:51:09 +0000 (20:51 +0000)
services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/ServiceResult.java [new file with mode: 0755]
services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/Tools.java [new file with mode: 0755]
services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplay.java [new file with mode: 0755]
services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayEval.java [new file with mode: 0755]
services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayTest.java [new file with mode: 0755]
services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayTransport.java [new file with mode: 0755]

diff --git a/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/ServiceResult.java b/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/ServiceResult.java
new file mode 100755 (executable)
index 0000000..04f8f55
--- /dev/null
@@ -0,0 +1,124 @@
+/**\r
+ * This document is a part of the source code and related artifacts\r
+ * for CollectionSpace, an open source collections management system\r
+ * for museums and related institutions:\r
+ *\r
+ * http://www.collectionspace.org\r
+ * http://wiki.collectionspace.org\r
+ *\r
+ * Copyright (c) 2009 Regents of the University of California\r
+ *\r
+ * Licensed under the Educational Community License (ECL), Version 2.0.\r
+ * You may not use this file except in compliance with this License.\r
+ *\r
+ * You may obtain a copy of the ECL 2.0 License at\r
+ * https://source.collectionspace.org/collection-space/LICENSE.txt\r
+ *\r
+ *  Unless required by applicable law or agreed to in writing, software\r
+ *  distributed under the License is distributed on an "AS IS" BASIS,\r
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ *  See the License for the specific language governing permissions and\r
+ *  limitations under the License.\r
+ */\r
+\r
+package org.collectionspace.services.IntegrationTests.xmlreplay;\r
+\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+\r
+/**\r
+ * User: laramie\r
+ * $LastChangedRevision:  $\r
+ * $LastChangedDate:  $\r
+ */\r
+public class ServiceResult {\r
+    public String testID = "";\r
+    public String testGroupID = "";\r
+    public String fullURL = "";\r
+    public String deleteURL = "";\r
+    public String location = "";\r
+    public String CSID = "";\r
+    public String subresourceCSID = "";\r
+    public String result = "";\r
+    public int responseCode = 0;\r
+    public String responseMessage = "";\r
+    public String method = "";\r
+    public String error = "";\r
+    public String fromTestID = "";\r
+    public String auth = "";\r
+    public List<Integer> expectedCodes = new ArrayList<Integer>();\r
+    public boolean codeInSuccessRange(int code){\r
+        if (0<=code && code<200){\r
+            return false;\r
+        } else if (400<=code) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
+    public boolean gotExpectedResult(){\r
+        for (Integer oneExpected : expectedCodes){\r
+            if (responseCode == oneExpected){\r
+                return true;\r
+            }\r
+        }\r
+        if (expectedCodes.size()>0 && codeInSuccessRange(responseCode)){ //none found, but result expected.\r
+            for (Integer oneExpected : expectedCodes){\r
+                if ( ! codeInSuccessRange(oneExpected)){\r
+                    return false;\r
+                }\r
+            }\r
+        }\r
+        return codeInSuccessRange(responseCode);\r
+    }\r
+    //public static final String[] DUMP_OPTIONS = {"minimal", "detailed", "full"};\r
+    public static enum DUMP_OPTIONS {minimal, detailed, full};\r
+\r
+    public String toString(){\r
+        return detail(true);\r
+\r
+    }\r
+    public String detail(boolean includePayloads){\r
+        return "{ServiceResult: "\r
+                + ( Tools.notEmpty(testID) ? " testID:"+testID : "" )\r
+                + ( Tools.notEmpty(testGroupID) ? "; testGroupID:"+testGroupID : "" )\r
+                + ( Tools.notEmpty(fromTestID) ? "; fromTestID:"+fromTestID : "" )\r
+                +"; "+method\r
+                +"; "+responseCode\r
+                + ( Tools.notEmpty(responseMessage) ? "; msg:"+responseMessage : "" )\r
+                +"; URL:"+fullURL\r
+                +"; auth: "+auth\r
+                + ( Tools.notEmpty(deleteURL) ? "; deleteURL:"+deleteURL : "" )\r
+                + ( Tools.notEmpty(location) ? "; location.CSID:"+location : "" )\r
+                + ( Tools.notEmpty(error) ? "; ERROR:"+error : "" )\r
+                + ( (expectedCodes.size()>0) ? "; expectedCodes:"+expectedCodes : "" )\r
+                + "; gotExpected:"+gotExpectedResult()\r
+                + ( includePayloads && Tools.notEmpty(result) ? "; result:"+result : "" )\r
+                +"}";\r
+    }\r
+    public String minimal(){\r
+        return "{"\r
+                + ( gotExpectedResult() ? "SUCCESS" : "FAILURE"  )\r
+\r
+                + ( Tools.notEmpty(testID) ? "; "+testID : "" )\r
+                +"; "+method\r
+                +"; "+responseCode\r
+                + (expectedCodes.size()>0 ? "; expected:"+expectedCodes : "")\r
+                + ( Tools.notEmpty(responseMessage) ? "; msg:"+responseMessage : "" )\r
+                +"; URL:"+fullURL\r
+                +"; auth: "+auth\r
+                + ( Tools.notEmpty(error) ? "; ERROR:"+error : "" )\r
+                +"}";\r
+    }\r
+    public String dump(ServiceResult.DUMP_OPTIONS opt){\r
+        switch (opt){\r
+            case minimal:\r
+                return minimal();\r
+            case detailed:\r
+                return detail(false);\r
+            case full:\r
+                return detail(true);\r
+            default:\r
+                return toString();\r
+        }\r
+    }\r
+}\r
diff --git a/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/Tools.java b/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/Tools.java
new file mode 100755 (executable)
index 0000000..bfd7ebf
--- /dev/null
@@ -0,0 +1,126 @@
+/**\r
+ * This document is a part of the source code and related artifacts\r
+ * for CollectionSpace, an open source collections management system\r
+ * for museums and related institutions:\r
+ *\r
+ * http://www.collectionspace.org\r
+ * http://wiki.collectionspace.org\r
+ *\r
+ * Copyright (c) 2009 Regents of the University of California\r
+ *\r
+ * Licensed under the Educational Community License (ECL), Version 2.0.\r
+ * You may not use this file except in compliance with this License.\r
+ *\r
+ * You may obtain a copy of the ECL 2.0 License at\r
+ * https://source.collectionspace.org/collection-space/LICENSE.txt\r
+ *\r
+ *  Unless required by applicable law or agreed to in writing, software\r
+ *  distributed under the License is distributed on an "AS IS" BASIS,\r
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ *  See the License for the specific language governing permissions and\r
+ *  limitations under the License.\r
+ */\r
+\r
+package org.collectionspace.services.IntegrationTests.xmlreplay;\r
+\r
+import org.apache.commons.jexl2.Expression;\r
+import org.apache.commons.jexl2.JexlContext;\r
+import org.apache.commons.jexl2.JexlEngine;\r
+import org.apache.commons.jexl2.MapContext;\r
+\r
+import java.io.File;\r
+import  java.util.regex.Pattern;\r
+import java.util.regex.Matcher;\r
+\r
+/** General utility methods.\r
+ *   @author Laramie Crocker\r
+ */\r
+public class Tools {\r
+    /** @return first glued to second with the separator string, at most one time - useful for appending paths.\r
+     */\r
+    public static String glue(String first, String separator, String second){\r
+        if (first==null) { first = ""; }\r
+        if (second==null) { second = ""; }\r
+        if (separator==null) { separator = ""; }\r
+        if (first.startsWith(separator) && second.startsWith(separator)){\r
+            return first.substring(0, first.length()-separator.length()) + second;\r
+        }\r
+        if (first.endsWith(separator) || second.startsWith(separator)){\r
+            return first+second;\r
+        }\r
+        return first+separator+second;\r
+    }\r
+\r
+    /** Handles null strings as empty.  */\r
+    public static boolean isEmpty(String str){\r
+        return !notEmpty(str);\r
+    }\r
+\r
+    /** Handles null strings as empty.  */\r
+        public static boolean notEmpty(String str){\r
+        if (str==null) return false;\r
+        if (str.length()==0) return false;\r
+        return true;\r
+    }\r
+\r
+    /** Handles null strings as false.  */\r
+    public static boolean isTrue(String test){\r
+        return notEmpty(test) && (new Boolean(test)).booleanValue();\r
+    }\r
+\r
+                    /*  Example usage of searchAndReplace:\r
+                        for (Map.Entry<String,String> entry : variablesMap.entrySet()){\r
+                            String key = entry.getKey();\r
+                            String replace = entry.getValue();\r
+                            String find = "\\$\\{"+key+"\\}";   //must add expression escapes\r
+                                                                //because $ and braces are "special", and we want to find "${object.CSID}"\r
+                            uri = Tools.searchAndReplace(uri, find, replace);\r
+                            System.out.println("---- REPLACE.uri:        "+initURI);\r
+                            System.out.println("---- REPLACE.find:       "+find);\r
+                            System.out.println("---- REPLACE.replace:    "+replace);\r
+                            System.out.println("---- REPLACE.uri result: "+uri);\r
+                        }\r
+                    */\r
+    public static String  searchAndReplace(String source, String find, String replace){\r
+        Pattern pattern = Pattern.compile(find);\r
+        Matcher matcher = pattern.matcher(source);\r
+        String output = matcher.replaceAll(replace);\r
+        return output;\r
+    }\r
+\r
+    static boolean m_fileSystemIsDOS = "\\".equals(File.separator);\r
+    static boolean m_fileSystemIsMac = ":".equals(File.separator);\r
+\r
+    public static boolean fileSystemIsDOS(){return m_fileSystemIsDOS;}\r
+    public static boolean fileSystemIsMac(){return m_fileSystemIsMac;}\r
+\r
+    public static String fixFilename(String filename){\r
+        if ( m_fileSystemIsDOS ) {\r
+            return filename.replace('/', '\\');\r
+        }\r
+        if ( m_fileSystemIsMac ) {\r
+            String t = filename.replace('/', ':');\r
+            t = t.replace('\\', ':');\r
+            return t;\r
+        }\r
+        return filename.replace('\\','/');\r
+    }\r
+\r
+    public static String join(String dir, String file){\r
+        if ( dir.length() == 0 ) {\r
+            return file;\r
+        }\r
+        dir = Tools.fixFilename(dir);\r
+        file = Tools.fixFilename(file);\r
+        if ( ! dir.endsWith(File.separator) ) {\r
+            dir += File.separator;\r
+        }\r
+        if ( file.startsWith(File.separator) ) {\r
+            file = file.substring(1);\r
+        }\r
+        return dir + file;\r
+    }\r
+\r
+\r
+\r
+}\r
diff --git a/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplay.java b/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplay.java
new file mode 100755 (executable)
index 0000000..c12b3fa
--- /dev/null
@@ -0,0 +1,668 @@
+package org.collectionspace.services.IntegrationTests.xmlreplay;\r
+\r
+import org.apache.commons.cli.*;\r
+\r
+import org.apache.commons.jexl2.JexlContext;\r
+import org.apache.commons.jexl2.JexlEngine;\r
+import org.apache.commons.jexl2.MapContext;\r
+import org.dom4j.Document;\r
+import org.dom4j.DocumentException;\r
+import org.dom4j.Node;\r
+import org.dom4j.io.SAXReader;\r
+\r
+import java.io.*;\r
+import java.util.*;\r
+\r
+/**  This class is used to replay a request to the Services layer, by sending the XML payload\r
+ *   in an appropriate Multipart request.\r
+ *   See example usage in calling class XmlReplayTest in services/IntegrationTests, and also in main() in this class.\r
+ *   @author Laramie Crocker\r
+ */\r
+public class XmlReplay {\r
+\r
+    public XmlReplay(String basedir){\r
+        this.basedir = basedir;\r
+        this.serviceResultsMap = createResultsMap();\r
+    }\r
+\r
+    public static final String DEFAULT_CONTROL = "xml-replay-control.xml";\r
+    public static final String DEFAULT_MASTER_CONTROL = "xml-replay-master.xml";\r
+\r
+    private String basedir = ".";  //set from constructor.\r
+    public String getBaseDir(){\r
+        return basedir;\r
+    }\r
+    \r
+    private String controlFileName = DEFAULT_CONTROL;\r
+    public String getControlFileName() {\r
+        return controlFileName;\r
+    }\r
+    public void setControlFileName(String controlFileName) {\r
+        this.controlFileName = controlFileName;\r
+    }\r
+\r
+    private String protoHostPort = "";\r
+    public String getProtoHostPort() {\r
+        return protoHostPort;\r
+    }\r
+    public void setProtoHostPort(String protoHostPort) {\r
+        this.protoHostPort = protoHostPort;\r
+    }\r
+\r
+    private boolean autoDeletePOSTS = true;\r
+    public boolean isAutoDeletePOSTS() {\r
+        return autoDeletePOSTS;\r
+    }\r
+    public void setAutoDeletePOSTS(boolean autoDeletePOSTS) {\r
+        this.autoDeletePOSTS = autoDeletePOSTS;\r
+    }\r
+\r
+    private Dump dump;\r
+    public Dump getDump() {\r
+        return dump;\r
+    }\r
+    public void setDump(Dump dump) {\r
+        this.dump = dump;\r
+    }\r
+\r
+    AuthsMap defaultAuthsMap;\r
+    public AuthsMap getDefaultAuthsMap(){\r
+        return defaultAuthsMap;\r
+    }\r
+    public void setDefaultAuthsMap(AuthsMap authsMap){\r
+        defaultAuthsMap = authsMap;\r
+    }\r
+\r
+    private Map<String, ServiceResult> serviceResultsMap;\r
+    public Map<String, ServiceResult> getServiceResultsMap(){\r
+        return serviceResultsMap;\r
+    }\r
+    public static Map<String, ServiceResult> createResultsMap(){\r
+        return new HashMap<String, ServiceResult>();\r
+    }\r
+\r
+\r
+    public String toString(){\r
+        return "XmlReplay{"+this.basedir+", "+this.controlFileName+", "+this.defaultAuthsMap+", "+this.dump+'}';\r
+    }\r
+\r
+    // ============== METHODS ===========================================================\r
+\r
+    public Document openMasterConfigFile(String masterFilename) throws FileNotFoundException {\r
+        Document document = getDocument(Tools.glue(basedir, "/", masterFilename)); //will check full path first, then checks relative to PWD.\r
+        if (document == null){\r
+            throw new FileNotFoundException("XmlReplay master control file ("+masterFilename+") not found in basedir: "+basedir+". Exiting test.");\r
+        }\r
+        return document;\r
+    }\r
+\r
+    /** specify the master config file, relative to getBaseDir(), but ignore any tests or testGroups in the master.\r
+     *  @return a Document object, which you don't need to use: all options will be stored in XmlReplay instance.\r
+     */\r
+    public Document readOptionsFromMasterConfigFile(String masterFilename) throws FileNotFoundException {\r
+        Document document = openMasterConfigFile(masterFilename);\r
+        protoHostPort = document.selectSingleNode("/xmlReplayMaster/protoHostPort").getText().trim();\r
+        AuthsMap authsMap = readAuths(document);\r
+        setDefaultAuthsMap(authsMap);\r
+        Dump dump = XmlReplay.readDumpOptions(document);\r
+        setDump(dump);\r
+        return document;\r
+    }\r
+\r
+    public List<List<ServiceResult>> runMaster(String masterFilename) throws Exception {\r
+        return runMaster(masterFilename, true);\r
+    }\r
+\r
+    /** Creates new instances of XmlReplay, one for each controlFile specified in the master,\r
+     *  and setting defaults from this instance, but not sharing ServiceResult objects or maps. */\r
+    public List<List<ServiceResult>> runMaster(String masterFilename, boolean readOptionsFromMaster) throws Exception {\r
+        List<List<ServiceResult>> list = new ArrayList<List<ServiceResult>>();\r
+        Document document;\r
+        if (readOptionsFromMaster){\r
+            document = readOptionsFromMasterConfigFile(masterFilename);\r
+        } else {\r
+            document = openMasterConfigFile(masterFilename);\r
+        }\r
+        String controlFile, testGroup, test;\r
+        List<Node> runNodes;\r
+        runNodes = document.selectNodes("/xmlReplayMaster/run");\r
+        for (Node runNode : runNodes) {\r
+            controlFile = runNode.valueOf("@controlFile");\r
+            testGroup = runNode.valueOf("@testGroup");\r
+            test = runNode.valueOf("@test"); //may be empty\r
+\r
+            //Create a new instance and clone only config values, not any results maps.\r
+            XmlReplay replay = new XmlReplay(basedir);\r
+            replay.setControlFileName(controlFile);\r
+            replay.setProtoHostPort(protoHostPort);\r
+            replay.setAutoDeletePOSTS(isAutoDeletePOSTS());\r
+            replay.setDump(dump);\r
+            replay.setDefaultAuthsMap(getDefaultAuthsMap());\r
+\r
+            //Now run *that* instance.\r
+            List<ServiceResult> results = replay.runTests(testGroup, test);\r
+            list.add(results);\r
+        }\r
+        return list;\r
+    }\r
+\r
+    /** Use this if you wish to named tests within a testGroup, otherwise call runTestGroup(). */\r
+    public List<ServiceResult>  runTests(String testGroupID, String testID) throws Exception {\r
+        List<ServiceResult> result = runXmlReplayFile(this.basedir,\r
+                                this.controlFileName,\r
+                                testGroupID,\r
+                                testID,\r
+                                this.serviceResultsMap,\r
+                                this.autoDeletePOSTS,\r
+                                dump,\r
+                                this.protoHostPort,\r
+                                this.defaultAuthsMap);\r
+        return result;\r
+    }\r
+\r
+    /** Use this if you wish to specify just ONE test to run within a testGroup, otherwise call runTestGroup(). */\r
+    public ServiceResult  runTest(String testGroupID, String testID) throws Exception {\r
+        List<ServiceResult> result = runXmlReplayFile(this.basedir,\r
+                                this.controlFileName,\r
+                                testGroupID,\r
+                                testID,\r
+                                this.serviceResultsMap,\r
+                                this.autoDeletePOSTS,\r
+                                dump,\r
+                                this.protoHostPort,\r
+                                this.defaultAuthsMap);\r
+        if (result.size()>1){\r
+            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.");\r
+        }\r
+        return result.get(0);\r
+    }\r
+\r
+    /** Use this if you wish to run all tests within a testGroup.*/\r
+    public List<ServiceResult> runTestGroup(String testGroupID) throws Exception {\r
+        //NOTE: calling runTest with empty testID runs all tests in a test group, but don't expose this fact.\r
+        // Expose this method (runTestGroup) instead.\r
+        return runTests(testGroupID, "");\r
+    }\r
+\r
+    public List<ServiceResult>  autoDelete(String logName){\r
+        return autoDelete(this.serviceResultsMap, logName);\r
+    }\r
+\r
+    /** Use this method to clean up resources created on the server that returned CSIDs, if you have\r
+     *  specified autoDeletePOSTS==false, which means you are managing the cleanup yourself.\r
+     * @param serviceResultsMap a Map of ServiceResult objects, which will contain ServiceResult.deleteURL.\r
+     * @return a List<String> of debug info about which URLs could not be deleted.\r
+     */\r
+    public static List<ServiceResult> autoDelete(Map<String, ServiceResult> serviceResultsMap, String logName){\r
+        List<ServiceResult> results = new ArrayList<ServiceResult>();\r
+        for (ServiceResult pr : serviceResultsMap.values()){\r
+            try {\r
+                ServiceResult deleteResult = XmlReplayTransport.doDELETE(pr.deleteURL, pr.auth, pr.testID, "[autodelete:"+logName+"]");\r
+                results.add(deleteResult);\r
+            } catch (Throwable t){\r
+                String s = (pr!=null) ? "ERROR while cleaning up ServiceResult map: "+pr+" for "+pr.deleteURL+" :: "+t\r
+                                      : "ERROR while cleaning up ServiceResult map (null ServiceResult): "+t;\r
+                System.err.println(s);\r
+                ServiceResult errorResult = new ServiceResult();\r
+                errorResult.fullURL = pr.fullURL;\r
+                errorResult.testGroupID = pr.testGroupID;\r
+                errorResult.fromTestID = pr.fromTestID;\r
+                errorResult.error = s;\r
+                results.add(errorResult);\r
+            }\r
+        }\r
+        return results;\r
+    }\r
+\r
+    public static class AuthsMap {\r
+        Map<String,String> map;\r
+        String defaultID="";\r
+        public String getDefaultAuth(){\r
+            return map.get(defaultID);\r
+        }\r
+        public String toString(){\r
+            return "AuthsMap: {default='"+defaultID+"'; "+map.keySet()+'}';\r
+        }\r
+    }\r
+\r
+    public static AuthsMap readAuths(Document document){\r
+    Map<String, String> map = new HashMap<String, String>();\r
+        List<Node> authNodes = document.selectNodes("//auths/auth");\r
+        for (Node auth : authNodes) {\r
+            map.put(auth.valueOf("@ID"), auth.getStringValue());\r
+        }\r
+        AuthsMap authsMap = new AuthsMap();\r
+        Node auths = document.selectSingleNode("//auths");\r
+        String defaultID = "";\r
+        if (auths != null){\r
+            defaultID = auths.valueOf("@default");\r
+        }\r
+        authsMap.map = map;\r
+        authsMap.defaultID = defaultID;\r
+        return authsMap;\r
+    }\r
+\r
+    public static class Dump {\r
+        public boolean payloads = false;\r
+        //public static final ServiceResult.DUMP_OPTIONS dumpServiceResultOptions = ServiceResult.DUMP_OPTIONS;\r
+        public ServiceResult.DUMP_OPTIONS dumpServiceResult = ServiceResult.DUMP_OPTIONS.minimal;\r
+        public String toString(){\r
+            return "payloads: "+payloads+" dumpServiceResult: "+dumpServiceResult;\r
+        }\r
+    }\r
+\r
+    public static Dump getDumpConfig(){\r
+        return new Dump();\r
+    }\r
+\r
+    public static Dump readDumpOptions(Document document){\r
+        Dump dump = getDumpConfig();\r
+        Node dumpNode = document.selectSingleNode("//dump");\r
+        if (dumpNode != null){\r
+            dump.payloads = Tools.isTrue(dumpNode.valueOf("@payloads"));\r
+            String dumpServiceResultStr = dumpNode.valueOf("@dumpServiceResult");\r
+            if (Tools.notEmpty(dumpServiceResultStr)){\r
+                dump.dumpServiceResult = ServiceResult.DUMP_OPTIONS.valueOf(dumpServiceResultStr);\r
+            }\r
+        }\r
+        return dump;\r
+    }\r
+\r
+    private static class PartsStruct {\r
+        public List<String> partsList = new ArrayList<String>();\r
+        public List<String> filesList = new ArrayList<String>();\r
+        boolean bDoingSinglePartPayload = false;\r
+        String singlePartPayloadFilename = "";\r
+        String overrideTestID = "";\r
+        public static PartsStruct readParts(Node testNode, final String testID, String xmlReplayBaseDir){\r
+            PartsStruct result = new PartsStruct();\r
+            result.singlePartPayloadFilename = testNode.valueOf("filename");\r
+            String singlePartPayloadFilename = testNode.valueOf("filename");\r
+            if (Tools.notEmpty(singlePartPayloadFilename)){\r
+                result.bDoingSinglePartPayload = true;\r
+                result.singlePartPayloadFilename = xmlReplayBaseDir + '/' + singlePartPayloadFilename;\r
+            } else {\r
+                result.bDoingSinglePartPayload = false;\r
+                List<Node> parts = testNode.selectNodes("parts/part");\r
+                if (parts == null || parts.size()==0){  //path is just /testGroup/test/part/\r
+                    String commonPartName = testNode.valueOf("part/label");\r
+                    String testfile = testNode.valueOf("part/filename");\r
+                    String fullTestFilename = xmlReplayBaseDir + '/' + testfile;\r
+                    if ( Tools.isEmpty(testID) ){\r
+                        result.overrideTestID = testfile; //It is legal to have a missing ID attribute, and rely on a unique filename.\r
+                    }\r
+                    result.partsList.add(commonPartName);\r
+                    result.filesList.add(fullTestFilename);\r
+                } else { // path is /testGroup/test/parts/part/\r
+                    for (Node part : parts){\r
+                        String commonPartName = part.valueOf("label");\r
+                        String filename = part.valueOf("filename");\r
+                        String fullTestFilename = xmlReplayBaseDir + '/' + filename;\r
+                        if ( Tools.isEmpty(testID) ){  //if testID is empty, we'll use the *first*  filename as ID.\r
+                            result.overrideTestID = filename; //It is legal to have a missing ID attribute, and rely on a unique filename.\r
+                        }\r
+                        result.partsList.add(commonPartName);\r
+                        result.filesList.add(fullTestFilename);\r
+                    }\r
+                }\r
+            }\r
+            return result;\r
+        }\r
+    }\r
+\r
+    private static String fixupFullURL(String fullURL, String protoHostPort, String uri){\r
+        if ( ! uri.startsWith(protoHostPort)){\r
+            fullURL = Tools.glue(protoHostPort, "/", uri);\r
+        } else {\r
+            fullURL = uri;\r
+        }\r
+        return fullURL;\r
+    }\r
+\r
+    private static String fromTestID(String fullURL, Node testNode, Map<String, ServiceResult> serviceResultsMap){\r
+        String fromTestID = testNode.valueOf("fromTestID");\r
+        if (Tools.notEmpty(fromTestID)){\r
+            ServiceResult getPR = serviceResultsMap.get(fromTestID);\r
+            if (getPR != null){\r
+                fullURL = Tools.glue(fullURL, "/", getPR.location);\r
+            }\r
+        }\r
+        return fullURL;\r
+    }\r
+\r
+    private static String CSIDfromTestID(Node testNode, Map<String, ServiceResult> serviceResultsMap){\r
+        String result = "";\r
+        String fromTestID = testNode.valueOf("fromTestID");\r
+        if (Tools.notEmpty(fromTestID)){\r
+            ServiceResult getPR = serviceResultsMap.get(fromTestID);\r
+            if (getPR != null){\r
+                result = getPR.location;\r
+            }\r
+        }\r
+        return result;\r
+    }\r
+\r
+\r
+    public static org.dom4j.Document getDocument(String xmlFileName) {\r
+        Document document = null;\r
+        SAXReader reader = new SAXReader();\r
+        try {\r
+            document = reader.read(xmlFileName);\r
+        } catch (DocumentException e) {\r
+            //e.printStackTrace();\r
+        }\r
+        return document;\r
+    }\r
+\r
+\r
+    //================= runXmlReplayFile ======================================================\r
+\r
+    public static List<ServiceResult> runXmlReplayFile(String xmlReplayBaseDir,\r
+                                          String controlFileName,\r
+                                          String testGroupID,\r
+                                          String oneTestID,\r
+                                          Map<String, ServiceResult> serviceResultsMap,\r
+                                          boolean param_autoDeletePOSTS,\r
+                                          Dump dump,\r
+                                          String protoHostPortParam,\r
+                                          AuthsMap defaultAuths)\r
+                                          throws Exception {\r
+        //Internally, we maintain two collections of ServiceResult:\r
+        //  the first is the return value of this method.\r
+        //  the second is the serviceResultsMap, which is used for keeping track of CSIDs created by POSTs, for later reference by DELETE, etc.\r
+        List<ServiceResult> results = new ArrayList<ServiceResult>();\r
+\r
+        String controlFile = Tools.glue(xmlReplayBaseDir, "/", controlFileName);\r
+        Document document;\r
+        document = getDocument(controlFile); //will check full path first, then checks relative to PWD.\r
+        if (document==null){\r
+            throw new FileNotFoundException("XmlReplay control file ("+controlFileName+") not found in basedir: "+xmlReplayBaseDir+" Exiting test.");\r
+        }\r
+        String protoHostPort;\r
+        if (Tools.isEmpty(protoHostPortParam)){\r
+            protoHostPort = document.selectSingleNode("/xmlReplay/protoHostPort").getText().trim();\r
+            System.out.println("DEPRECATED: Using protoHostPort ('"+protoHostPort+"') from xmlReplay file ('"+controlFile+"'), not master.");\r
+        } else {\r
+            protoHostPort = protoHostPortParam;\r
+        }\r
+        if (Tools.isEmpty(protoHostPort)){\r
+            throw new Exception("XmlReplay control file must have a protoHostPort element");\r
+        }\r
+\r
+        String authsMapINFO;\r
+        AuthsMap authsMap = readAuths(document);\r
+        if (authsMap.map.size()==0){\r
+            authsMap = defaultAuths;\r
+            authsMapINFO = "Using defaultAuths from master file: "+defaultAuths;\r
+        } else {\r
+            authsMapINFO = "Using AuthsMap from control file: "+authsMap;\r
+        }\r
+        System.out.println("XmlReplay running:"\r
+                          +"\r\n   controlFile: "+ (new File(controlFile).getCanonicalPath())\r
+                          +"\r\n   protoHostPort: "+protoHostPort\r
+                          +"\r\n   testGroup: "+testGroupID\r
+                          + (Tools.notEmpty(oneTestID) ? "\r\n   oneTestID: "+oneTestID : "")\r
+                          +"\r\n   AuthsMap: "+authsMapINFO\r
+                          +"\r\n   param_autoDeletePOSTS: "+param_autoDeletePOSTS\r
+                          +"\r\n   Dump info: "+dump\r
+                          +"\r\n");\r
+\r
+        String autoDeletePOSTS = "";\r
+        List<Node> testgroupNodes;\r
+        if (Tools.notEmpty(testGroupID)){\r
+            testgroupNodes = document.selectNodes("//testGroup[@ID='"+testGroupID+"']");\r
+        } else {\r
+            testgroupNodes = document.selectNodes("//testGroup");\r
+        }\r
+\r
+        JexlEngine jexl = new JexlEngine();   // Used for expression language expansion from uri field.\r
+        XmlReplayEval evalStruct = new XmlReplayEval();\r
+        evalStruct.serviceResultsMap = serviceResultsMap;\r
+        evalStruct.jexl = jexl;\r
+\r
+        for (Node testgroup : testgroupNodes) {\r
+            JexlContext jc = new MapContext();  //Get a new JexlContext for each test group.\r
+            evalStruct.jc = jc;\r
+\r
+            autoDeletePOSTS = testgroup.valueOf("@autoDeletePOSTS");\r
+            List<Node> tests;\r
+            if (Tools.notEmpty(oneTestID)){\r
+                tests = testgroup.selectNodes("test[@ID='"+oneTestID+"']");\r
+            } else {\r
+                tests = testgroup.selectNodes("test");\r
+            }\r
+            String authForTest = "";\r
+            int testElementIndex = -1;\r
+\r
+            for (Node testNode : tests) {\r
+                testElementIndex++;\r
+                String testID = testNode.valueOf("@ID");\r
+                String testIDLabel = Tools.notEmpty(testID) ? (testGroupID+'.'+testID) : (testGroupID+'.'+testElementIndex);\r
+                String method = testNode.valueOf("method");\r
+                String uri = testNode.valueOf("uri");\r
+                String fullURL = Tools.glue(protoHostPort, "/", uri);\r
+                String initURI = uri;\r
+\r
+                String authIDForTest = testNode.valueOf("@auth");\r
+                String currentAuthForTest = authsMap.map.get(authIDForTest);\r
+                if (Tools.notEmpty(currentAuthForTest)){\r
+                    authForTest = currentAuthForTest; //else just run with current from last loop;\r
+                }\r
+                if (Tools.isEmpty(authForTest)){\r
+                    authForTest = defaultAuths.getDefaultAuth();\r
+                }\r
+\r
+                if (uri.indexOf("$")>-1){\r
+                    uri = evalStruct.eval(uri, serviceResultsMap, jexl, jc);\r
+                }\r
+                fullURL = fixupFullURL(fullURL, protoHostPort, uri);\r
+\r
+                List<Integer> expectedCodes = new ArrayList<Integer>();\r
+                String expectedCodesStr = testNode.valueOf("expectedCodes");\r
+                if (Tools.notEmpty(expectedCodesStr)){\r
+                     String[] codesArray = expectedCodesStr.split(",");\r
+                     for (String code : codesArray){\r
+                         expectedCodes.add(new Integer(code));\r
+                     }\r
+                }\r
+\r
+                ServiceResult serviceResult;\r
+                boolean isPOST = method.equalsIgnoreCase("POST");\r
+                boolean isPUT =  method.equalsIgnoreCase("PUT");\r
+                if ( isPOST || isPUT ) {\r
+                    PartsStruct parts = PartsStruct.readParts(testNode, testID, xmlReplayBaseDir);\r
+                    if (Tools.notEmpty(parts.overrideTestID)) {\r
+                        testID = parts.overrideTestID;\r
+                    }\r
+                    if (isPOST){\r
+                        String csid = CSIDfromTestID(testNode, serviceResultsMap);\r
+                        if (Tools.notEmpty(csid)) uri = Tools.glue(uri, "/", csid+"/items/");\r
+                    } else if (isPUT) {\r
+                        uri = fromTestID(uri, testNode, serviceResultsMap);\r
+                    }\r
+                    if (parts.bDoingSinglePartPayload){\r
+                        serviceResult = XmlReplayTransport.doPOST_PUTFromXML(parts.singlePartPayloadFilename, protoHostPort, uri, "POST", XmlReplayTransport.APPLICATION_XML, evalStruct, authForTest, testIDLabel);\r
+                    } else {\r
+                        serviceResult = XmlReplayTransport.doPOST_PUTFromXML_Multipart(parts.filesList, parts.partsList, protoHostPort, uri, "POST", evalStruct, authForTest, testIDLabel);\r
+                    }\r
+                    results.add(serviceResult);\r
+                    if (isPOST){\r
+                        serviceResultsMap.put(testID, serviceResult);      //PUTs do not return a Location, so don't add PUTs to serviceResultsMap.\r
+                    }\r
+                    fullURL = fixupFullURL(fullURL, protoHostPort, uri);\r
+                } else if (method.equalsIgnoreCase("DELETE")){\r
+                    String fromTestID = testNode.valueOf("fromTestID");\r
+                    ServiceResult pr = serviceResultsMap.get(fromTestID);\r
+                    if (pr!=null){\r
+                        serviceResult = XmlReplayTransport.doDELETE(pr.deleteURL, authForTest, testIDLabel, fromTestID);\r
+                        serviceResult.fromTestID = fromTestID;\r
+                        results.add(serviceResult);\r
+                        if (serviceResult.gotExpectedResult()){\r
+                            serviceResultsMap.remove(fromTestID);\r
+                        }\r
+                    } else {\r
+                        if (Tools.notEmpty(fromTestID)){\r
+                            serviceResult = new ServiceResult();\r
+                            serviceResult.responseCode = 0;\r
+                            serviceResult.error = "ID not found in element fromTestID: "+fromTestID;\r
+                            System.err.println("****\r\nServiceResult: "+serviceResult.error+". SKIPPING TEST. Full URL: "+fullURL);\r
+                        } else {\r
+                            serviceResult = XmlReplayTransport.doDELETE(fullURL, authForTest, testID, fromTestID);\r
+                        }\r
+                        serviceResult.fromTestID = fromTestID;\r
+                        results.add(serviceResult);\r
+                    }\r
+                } else if (method.equalsIgnoreCase("GET")){\r
+                    fullURL = fromTestID(fullURL, testNode, serviceResultsMap);\r
+                    serviceResult = XmlReplayTransport.doGET(fullURL, authForTest, testIDLabel);\r
+                    results.add(serviceResult);\r
+                } else if (method.equalsIgnoreCase("LIST")){\r
+                    fullURL = fixupFullURL(fullURL, protoHostPort, uri);\r
+                    String listQueryParams = ""; //TODO: empty for now, later may pick up from XML control file.\r
+                    serviceResult = XmlReplayTransport.doLIST(fullURL, listQueryParams, authForTest, testIDLabel);\r
+                    results.add(serviceResult);\r
+                } else {\r
+                    throw new Exception("HTTP method not supported by XmlReplay: "+method);\r
+                }\r
+\r
+                serviceResult.testID = testID;\r
+                serviceResult.fullURL = fullURL;\r
+                serviceResult.auth = authForTest;\r
+                serviceResult.method = method;\r
+                if (expectedCodes.size()>0){\r
+                    serviceResult.expectedCodes = expectedCodes;\r
+                }\r
+                if (Tools.isEmpty(serviceResult.testID)) serviceResult.testID = testIDLabel;\r
+                if (Tools.isEmpty(serviceResult.testGroupID)) serviceResult.testGroupID = testGroupID;\r
+\r
+                String serviceResultRow = serviceResult.dump(dump.dumpServiceResult);\r
+                String leader = (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.detailed) ? "XmlReplay:"+testIDLabel+": ": "";\r
+                System.out.println(leader+serviceResultRow+"\r\n");\r
+                if (dump.payloads) System.out.println(serviceResult.result);\r
+            }\r
+            if (Tools.isTrue(autoDeletePOSTS)&&param_autoDeletePOSTS){\r
+                autoDelete(serviceResultsMap, "default");\r
+            }\r
+        }\r
+        return results;\r
+    }\r
+\r
+\r
+    //======================== MAIN ===================================================================\r
+\r
+    private static Options createOptions() {\r
+        Options options = new Options();\r
+        options.addOption("xmlReplayBaseDir", true, "default/basedir");\r
+        return options;\r
+    }\r
+\r
+    public static String usage(){\r
+        String result = "org.collectionspace.services.IntegrationTests.xmlreplay.XmlReplay {args}\r\n"\r
+                        +"  -xmlReplayBaseDir <dir> \r\n"\r
+                        +" You may also override these with system args, e.g.: \r\n"\r
+                        +"   -DxmlReplayBaseDir=/path/to/dir \r\n"\r
+                        +" These may also be passed in via the POM.\r\n"\r
+                        +" You can also set these system args, e.g.: \r\n"\r
+                        +"  -DtestGroupID=<oneID> \r\n"\r
+                        +"  -DtestID=<one TestGroup ID>"\r
+                        +"  -DautoDeletePOSTS=<true|false> \r\n"\r
+                        +"    (note: -DautoDeletePOSTS won't force deletion if set to false in control file.";\r
+        return result;\r
+    }\r
+\r
+    private static String opt(CommandLine line, String option){\r
+        String result;\r
+        String fromProps = System.getProperty(option);\r
+        if (Tools.notEmpty(fromProps)){\r
+            return fromProps;\r
+        }\r
+        result = line.getOptionValue(option);\r
+        if (result == null){\r
+            result = "";\r
+        }\r
+        return result;\r
+    }\r
+\r
+    public static void main(String[]args) throws Exception {\r
+        Options options = createOptions();\r
+        //System.out.println("System CLASSPATH: "+prop.getProperty("java.class.path", null));\r
+        CommandLineParser parser = new GnuParser();\r
+        try {\r
+            // parse the command line arguments\r
+            CommandLine line = parser.parse(options, args);\r
+\r
+            String xmlReplayBaseDir = opt(line, "xmlReplayBaseDir");\r
+            String testGroupID      = opt(line, "testGroupID");\r
+            String testID           = opt(line, "testID");\r
+            String autoDeletePOSTS  = opt(line, "autoDeletePOSTS");\r
+            String dumpResults      = opt(line, "dumpResults");\r
+            String controlFilename   = opt(line, "controlFilename");\r
+            String xmlReplayMaster  = opt(line, "xmlReplayMaster");\r
+\r
+            xmlReplayBaseDir = Tools.fixFilename(xmlReplayBaseDir);\r
+            controlFilename = Tools.fixFilename(controlFilename);\r
+\r
+            boolean bAutoDeletePOSTS = true;\r
+            if (Tools.notEmpty(autoDeletePOSTS)) {\r
+                bAutoDeletePOSTS = Tools.isTrue(autoDeletePOSTS);\r
+            }\r
+            boolean bDumpResults = false;\r
+            if (Tools.notEmpty(dumpResults)) {\r
+                bDumpResults = Tools.isTrue(autoDeletePOSTS);\r
+            }\r
+            if (Tools.isEmpty(xmlReplayBaseDir)){\r
+                System.err.println("ERROR: xmlReplayBaseDir was not specified.");\r
+                return;\r
+            }\r
+            File f = new File(Tools.glue(xmlReplayBaseDir, "/", controlFilename));\r
+            if (Tools.isEmpty(xmlReplayMaster) && !f.exists()){\r
+                System.err.println("Control file not found: "+f.getCanonicalPath());\r
+                return;\r
+            }\r
+            File fMaster = new File(Tools.glue(xmlReplayBaseDir, "/", xmlReplayMaster));\r
+            if (Tools.notEmpty(xmlReplayMaster)  && !fMaster.exists()){\r
+                System.err.println("Master file not found: "+fMaster.getCanonicalPath());\r
+                return;\r
+            }\r
+\r
+            String xmlReplayBaseDirResolved = (new File(xmlReplayBaseDir)).getCanonicalPath();\r
+            System.out.println("XmlReplay ::"\r
+                            + "\r\n    xmlReplayBaseDir: "+xmlReplayBaseDir\r
+                            + "\r\n    xmlReplayBaseDir(resolved): "+xmlReplayBaseDirResolved\r
+                            + "\r\n    controlFilename: "+controlFilename\r
+                            + "\r\n    xmlReplayMaster: "+xmlReplayMaster\r
+                            + "\r\n    testGroupID: "+testGroupID\r
+                            + "\r\n    testID: "+testID\r
+                            + "\r\n    autoDeletePOSTS: "+bAutoDeletePOSTS\r
+                            + (Tools.notEmpty(xmlReplayMaster)\r
+                                       ? ("\r\n    will use master file: "+fMaster.getCanonicalPath())\r
+                                       : ("\r\n    will use control file: "+f.getCanonicalPath()) )\r
+                             );\r
+            \r
+            if (Tools.notEmpty(xmlReplayMaster)){\r
+                if (Tools.notEmpty(controlFilename)){\r
+                    System.out.println("WARN: controlFilename: "+controlFilename+" will not be used because master was specified.  Running master: "+xmlReplayMaster);\r
+                }\r
+                XmlReplay replay = new XmlReplay(xmlReplayBaseDirResolved);\r
+                replay.readOptionsFromMasterConfigFile(xmlReplayMaster);\r
+                replay.setAutoDeletePOSTS(bAutoDeletePOSTS);\r
+                Dump dumpFromMaster = replay.getDump();\r
+                dumpFromMaster.payloads = Tools.isTrue(dumpResults);\r
+                replay.setDump(dumpFromMaster);\r
+                replay.runMaster(xmlReplayMaster, false); //false, because we already just read the options, and override a few.\r
+            } else {\r
+                Dump dump = getDumpConfig();\r
+                dump.payloads = Tools.isTrue(dumpResults);\r
+                runXmlReplayFile(xmlReplayBaseDirResolved, controlFilename, testGroupID, testID, createResultsMap(), bAutoDeletePOSTS, dump, "", null);\r
+            }\r
+        } catch (ParseException exp) {\r
+            // oops, something went wrong\r
+            System.err.println("Cmd-line parsing failed.  Reason: " + exp.getMessage());\r
+            System.err.println(usage());\r
+        } catch (Exception e) {\r
+            System.out.println("Error : " + e.getMessage());\r
+            e.printStackTrace();\r
+        }\r
+    }\r
+\r
+}\r
diff --git a/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayEval.java b/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayEval.java
new file mode 100755 (executable)
index 0000000..5723f5b
--- /dev/null
@@ -0,0 +1,114 @@
+/**\r
+ * This document is a part of the source code and related artifacts\r
+ * for CollectionSpace, an open source collections management system\r
+ * for museums and related institutions:\r
+ *\r
+ * http://www.collectionspace.org\r
+ * http://wiki.collectionspace.org\r
+ *\r
+ * Copyright (c) 2009 Regents of the University of California\r
+ *\r
+ * Licensed under the Educational Community License (ECL), Version 2.0.\r
+ * You may not use this file except in compliance with this License.\r
+ *\r
+ * You may obtain a copy of the ECL 2.0 License at\r
+ * https://source.collectionspace.org/collection-space/LICENSE.txt\r
+ *\r
+ *  Unless required by applicable law or agreed to in writing, software\r
+ *  distributed under the License is distributed on an "AS IS" BASIS,\r
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ *  See the License for the specific language governing permissions and\r
+ *  limitations under the License.\r
+ */\r
+package org.collectionspace.services.IntegrationTests.xmlreplay;\r
+\r
+import org.apache.commons.jexl2.Expression;\r
+import org.apache.commons.jexl2.JexlContext;\r
+import org.apache.commons.jexl2.JexlEngine;\r
+\r
+import java.util.Map;\r
+\r
+/**\r
+ * User: laramie\r
+ * $LastChangedRevision:  $\r
+ * $LastChangedDate:  $\r
+ */\r
+public class XmlReplayEval {\r
+    public Map<String, ServiceResult> serviceResultsMap;\r
+    public JexlEngine jexl;\r
+    public JexlContext jc;\r
+\r
+    /**\r
+     * You may pass in a Jexl 2 expression, e.g. ${foo.bar} and it will be eval'd for you.\r
+     * We are looking at some URI like so: ${newOrgAuthority.CSID}\r
+     * The idea here is that the XML control file may bind to this namespace, and\r
+     * this module may find those values and any future extensions, specifically\r
+     * when someone says "I want to bind to ${CSID} and ${SUBRESOURCE.CSID}\r
+     * The code here is easy to extend, but the test cases build up, so you don't\r
+     * want to break all the config files by not being backward compatible.  Binding\r
+     * to context variables like this makes it easy.\r
+     * EXAMPLE USAGE: <br />\r
+     * String uri = "/cspace-services/orgauthorities/${OrgAuth1.CSID}/items/${Org1.CSID}";   <br />\r
+     * uri = eval(uri, serviceResultsMap, jexl, jc);  <br />\r
+     * RESULT:    "/cspace-services/orgauthorities/43a2739c-4f40-49c8-a6d5/items/"\r
+     */\r
+    public static String eval(String inputJexlExpression, Map<String, ServiceResult> serviceResultsMap, JexlEngine jexl, JexlContext jc) {\r
+        //System.out.println("\r\n---- REPLACE.init-uri:        "+inputJexlExpression);\r
+        String result;\r
+        try {\r
+            for (ServiceResult postResult : serviceResultsMap.values()) {\r
+                jc.set(postResult.testID, postResult);\r
+                //System.out.println("eval :: "+postResult.testID+"==>"+postResult);\r
+            }\r
+            result = parse(inputJexlExpression, jexl, jc);\r
+        } catch (Throwable t) {\r
+            System.err.println("ERROR: " + t);\r
+            result = "ERROR";\r
+        }\r
+        //System.out.println("---- REPLACE.uri:        "+result+"\r\n");\r
+        return result;\r
+    }\r
+\r
+    private static String parse(String in, JexlEngine jexl, JexlContext jc) {\r
+        StringBuffer result = new StringBuffer();\r
+        String s = in;\r
+        String var = "";\r
+        int start, end, len;\r
+        len = in.length();\r
+        start = 0;\r
+        int cursor = 0;\r
+        String front = "";\r
+        while (start < len) {\r
+            end = in.indexOf("}", start);\r
+            start = in.indexOf("${", start);\r
+            if (start < 0) {\r
+                String tail = in.substring(cursor);\r
+                result.append(tail);\r
+                break;\r
+            }\r
+            if (end < 0) {\r
+                return "ERROR: unbalanced ${} braces";\r
+            }\r
+            front = in.substring(cursor, start);\r
+            result.append(front);\r
+            cursor = end + 1;                   //bump past close brace\r
+            var = in.substring(start + 2, end);  //+2 bump past open brace ${ and then "end" is indexed just before the close brace }\r
+            //s   = s.substring(end+1);         //bump past close brace\r
+            start = cursor;\r
+\r
+            Expression expr = jexl.createExpression(var);\r
+            Object resultObj = expr.evaluate(jc);\r
+            String resultStr;\r
+            if (null == resultObj){\r
+                resultStr = "ERROR";\r
+                System.out.println("Jexl context: "+jc.toString());\r
+            } else {\r
+                resultStr = resultObj.toString();\r
+\r
+            }\r
+            result.append(resultStr);\r
+        }\r
+        return result.toString();\r
+    }\r
+\r
+}\r
diff --git a/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayTest.java b/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayTest.java
new file mode 100755 (executable)
index 0000000..f05ab13
--- /dev/null
@@ -0,0 +1,179 @@
+/**\r
+ * This document is a part of the source code and related artifacts\r
+ * for CollectionSpace, an open source collections management system\r
+ * for museums and related institutions:\r
+ *\r
+ * http://www.collectionspace.org\r
+ * http://wiki.collectionspace.org\r
+ *\r
+ * Copyright (c) 2009 Regents of the University of California\r
+ *\r
+ * Licensed under the Educational Community License (ECL), Version 2.0.\r
+ * You may not use this file except in compliance with this License.\r
+ *\r
+ * You may obtain a copy of the ECL 2.0 License at\r
+ * https://source.collectionspace.org/collection-space/LICENSE.txt\r
+ *\r
+ *  Unless required by applicable law or agreed to in writing, software\r
+ *  distributed under the License is distributed on an "AS IS" BASIS,\r
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ *  See the License for the specific language governing permissions and\r
+ *  limitations under the License.\r
+ */\r
+\r
+package org.collectionspace.services.IntegrationTests.xmlreplay;\r
+\r
+import org.testng.Assert;\r
+\r
+import java.io.File;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+\r
+/** Subclass this test to programmatically control XmlReplay from a surefire test.  See example in IntegrationTests :: XmlReplaySelfTest\r
+ * User: laramie\r
+ * $LastChangedRevision:  $\r
+ * $LastChangedDate:  $\r
+ */\r
+public class XmlReplayTest {\r
+\r
+    public static final String XMLREPLAY_REL_DIR_TO_MODULE = "/src/test/resources/test-data/xmlreplay";\r
+\r
+    /** To use this method, you should have a test repository of xml files in the path\r
+     *  defined by XMLREPLAY_REL_DIR_TO_MODULE, relative to your pom.xml file, but normally\r
+     *  you would use the central repository of tests, which live in services/IntegrationTests,\r
+     *  and which you can use by calling createXmlReplay() which calls createXmlReplayUsingIntegrationTestsModule() for you.\r
+     */\r
+    public static XmlReplay createXmlReplayForModule() throws Exception {\r
+        String pwd = (new File(".")).getCanonicalPath();\r
+        System.out.println("createXmlReplayForModule.pwd: "+pwd);\r
+        XmlReplay replay = new XmlReplay(pwd+XMLREPLAY_REL_DIR_TO_MODULE);\r
+        System.out.println("XmlReplay: "+replay);\r
+        return replay;\r
+    }\r
+\r
+    /** Use this method if your test xml files are stored in the central repository,\r
+     *   which is "services/IntegrationTests" + XMLREPLAY_REL_DIR_TO_MODULE\r
+     */\r
+    public static XmlReplay createXmlReplay() throws Exception {\r
+        return createXmlReplayUsingIntegrationTestsModule("../..");\r
+    }\r
+\r
+    /**\r
+     * @param relToServicesRoot is a Unix-like path from the calling module to the services root,\r
+     *        so if  if you are in services/dimension/client/\r
+     *        then relToServicesRoot is "../.." which is how most of the client tests are set up, or if you\r
+     *        are setting up your test repository relative to the main service, e.g. you are in\r
+     *        services/dimension/, then relToServicesRoot is ".."\r
+     */\r
+    public static XmlReplay createXmlReplayUsingIntegrationTestsModule(String relToServicesRoot) throws Exception {\r
+        String thisDir = Tools.glue(relToServicesRoot, "/", "IntegrationTests");\r
+        String pwd = (new File(thisDir)).getCanonicalPath();\r
+        System.out.println("createXmlReplayUsingIntegrationTestsModule.pwd: "+pwd);\r
+        XmlReplay replay = new XmlReplay(pwd+XMLREPLAY_REL_DIR_TO_MODULE);\r
+        System.out.println("XmlReplay: "+replay);\r
+        return replay;\r
+    }\r
+\r
+    public static void logTest(ServiceResult sresult, String testname){\r
+        ResultSummary summary = resultSummary(sresult);\r
+        org.testng.Reporter.log(summary.table);\r
+        Assert.assertEquals(summary.oks, summary.total, "Expected all "+summary.total+ " XmlReplay tests to pass.  See Output from test '"+testname+"'.");\r
+    }\r
+\r
+    public static void logTest(List<ServiceResult> list, String testname){\r
+        ResultSummary summary = resultSummary(list);\r
+        org.testng.Reporter.log(summary.table);\r
+        Assert.assertEquals(summary.oks, summary.total, "Expected all "+summary.total+ " XmlReplay tests to pass.  See Output from test '"+testname+"'.");\r
+    }\r
+\r
+    public static void logTestForGroup(List<List<ServiceResult>> list, String testname){\r
+        ResultSummary summary = resultSummaryForGroup(list);\r
+        org.testng.Reporter.log(summary.table);\r
+        Assert.assertEquals(summary.oks, summary.total, "Expected all "+summary.total+ " XmlReplay tests to pass.  See Output from test '"+testname+"'.");\r
+    }\r
+\r
+\r
+    //============== HELPERS AND FORMATTING =====================================================\r
+\r
+    private static final String TBLSTART = "<table border='1'>";\r
+    private static final String ROWSTART = "<tr><td bgcolor='white'>";\r
+    private static final String ROWSTARTRED = "<tr><td bgcolor='red'><b>";\r
+    private static final String SEP = "</td><td>";\r
+    private static final String ROWEND = "</td></tr>";\r
+    private static final String ROWENDRED = "</b></td></tr>";\r
+    private static final String TBLEND = "</table>";\r
+\r
+    public static class ResultSummary {\r
+        public long oks = 0;\r
+        public long total = 0;\r
+        public String table = "";\r
+        public List<String> groups = new ArrayList<String>();\r
+    }\r
+\r
+    public static ResultSummary resultSummaryForGroup(List<List<ServiceResult>> list){\r
+        ResultSummary summary = new ResultSummary();\r
+        summary.oks = 0;\r
+        summary.total = 0;\r
+        StringBuffer buff = new StringBuffer();\r
+        buff.append(TBLSTART);\r
+        for (List<ServiceResult> serviceResults : list){\r
+            String groupID = "";\r
+            if (serviceResults.size()>0){\r
+                groupID = serviceResults.get(0).testGroupID;\r
+                summary.groups.add(groupID);\r
+            }\r
+            buff.append(ROWSTART+"XmlReplay testGroup "+groupID+ROWEND);\r
+            for (ServiceResult serviceResult : serviceResults){\r
+                summary.total++;\r
+                if (serviceResult.gotExpectedResult()){\r
+                    summary.oks++;\r
+                    buff.append(ROWSTART+serviceResult.minimal()+ROWEND);\r
+                } else {\r
+                    buff.append(ROWSTARTRED+serviceResult.minimal()+ROWENDRED);\r
+                }\r
+            }\r
+\r
+        }\r
+        buff.append(TBLEND);\r
+        summary.table = buff.toString();\r
+        return summary;\r
+    }\r
+\r
+    public static ResultSummary resultSummary(List<ServiceResult> serviceResults){\r
+        ResultSummary summary = new ResultSummary();\r
+        summary.oks = 0;\r
+        summary.total = 0;\r
+        StringBuffer buff = new StringBuffer();\r
+        buff.append(TBLSTART);\r
+        for (ServiceResult serviceResult : serviceResults){\r
+            summary.total++;\r
+            if (serviceResult.gotExpectedResult()){\r
+                summary.oks++;\r
+                buff.append(ROWSTART+serviceResult.minimal()+ROWEND);\r
+            } else {\r
+                buff.append(ROWSTARTRED+serviceResult.minimal()+ROWENDRED);\r
+            }\r
+        }\r
+        buff.append(TBLEND);\r
+        summary.table = buff.toString();\r
+        return summary;\r
+    }\r
+\r
+    public static ResultSummary resultSummary(ServiceResult serviceResult){\r
+        ResultSummary summary = new ResultSummary();\r
+        summary.oks = 0;\r
+        summary.total = 1;\r
+        StringBuffer buff = new StringBuffer();\r
+        buff.append(TBLSTART);\r
+        if (serviceResult.gotExpectedResult()){\r
+            summary.oks = 1;\r
+            buff.append(ROWSTART+serviceResult.minimal()+ROWEND);\r
+        } else {\r
+            buff.append(ROWSTARTRED+serviceResult.minimal()+ROWENDRED);\r
+        }\r
+        buff.append(TBLEND);\r
+        summary.table = buff.toString();\r
+        return summary;\r
+    }\r
+\r
+}\r
diff --git a/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayTransport.java b/services/IntegrationTests/src/main/java/org/collectionspace/services/IntegrationTests/xmlreplay/XmlReplayTransport.java
new file mode 100755 (executable)
index 0000000..b865787
--- /dev/null
@@ -0,0 +1,237 @@
+/**\r
+ * This document is a part of the source code and related artifacts\r
+ * for CollectionSpace, an open source collections management system\r
+ * for museums and related institutions:\r
+ *\r
+ * http://www.collectionspace.org\r
+ * http://wiki.collectionspace.org\r
+ *\r
+ * Copyright (c) 2009 Regents of the University of California\r
+ *\r
+ * Licensed under the Educational Community License (ECL), Version 2.0.\r
+ * You may not use this file except in compliance with this License.\r
+ *\r
+ * You may obtain a copy of the ECL 2.0 License at\r
+ * https://source.collectionspace.org/collection-space/LICENSE.txt\r
+ *\r
+ *  Unless required by applicable law or agreed to in writing, software\r
+ *  distributed under the License is distributed on an "AS IS" BASIS,\r
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ *  See the License for the specific language governing permissions and\r
+ *  limitations under the License.\r
+ */\r
+\r
+package org.collectionspace.services.IntegrationTests.xmlreplay;\r
+\r
+import org.apache.commons.httpclient.HttpClient;\r
+import org.apache.commons.httpclient.methods.DeleteMethod;\r
+import org.apache.commons.httpclient.methods.GetMethod;\r
+import org.apache.commons.io.FileUtils;\r
+\r
+import java.io.BufferedReader;\r
+import java.io.File;\r
+import java.io.InputStreamReader;\r
+import java.io.OutputStreamWriter;\r
+import java.net.HttpURLConnection;\r
+import java.net.URL;\r
+import java.util.List;\r
+import java.util.Map;\r
+\r
+import org.collectionspace.services.IntegrationTests.xmlreplay.ServiceResult;\r
+\r
+/**\r
+ *   @author Laramie Crocker\r
+ */\r
+public class XmlReplayTransport {\r
+\r
+    private static String BOUNDARY = "34d97c83-0d61-4958-80ab-6bf8d362290f";\r
+        private static String DD = "--";\r
+        private static String CRLF = "\r\n";\r
+\r
+    public static ServiceResult doGET(String urlString, String authForTest, String fromTestID) throws Exception {\r
+        HttpClient client = new HttpClient();\r
+        GetMethod getMethod = new GetMethod(urlString);\r
+        getMethod.addRequestHeader("Accept", "multipart/mixed");\r
+        getMethod.addRequestHeader("Accept", "application/xml");\r
+        getMethod.setRequestHeader("Authorization", "Basic " + authForTest); //"dGVzdDp0ZXN0");\r
+        getMethod.setRequestHeader("X-XmlReplay-fromTestID", fromTestID);\r
+        ServiceResult pr = new ServiceResult();\r
+\r
+        int statusCode1 = client.executeMethod(getMethod);\r
+        pr.responseCode = statusCode1;\r
+        pr.method = "GET";\r
+        try {\r
+            pr.result = getMethod.getResponseBodyAsString();\r
+            pr.responseMessage = getMethod.getStatusText();\r
+        } catch (Throwable t){\r
+            //System.err.println("ERROR getting content from response: "+t);\r
+            pr.error = t.toString();\r
+        }\r
+\r
+\r
+        getMethod.releaseConnection();\r
+        return pr;\r
+    }\r
+\r
+    public static ServiceResult doDELETE(String urlString, String authForTest, String testID, String fromTestID) throws Exception {\r
+        ServiceResult pr = new ServiceResult();\r
+        pr.method = "DELETE";\r
+        pr.fullURL = urlString;\r
+        if (Tools.isEmpty(urlString)){\r
+            pr.error = "url was empty.  Check the result for fromTestID: "+fromTestID+". currentTest: "+testID;\r
+            return pr;\r
+        }\r
+        HttpClient client = new HttpClient();\r
+        DeleteMethod deleteMethod = new DeleteMethod(urlString);\r
+        deleteMethod.setRequestHeader("Accept", "multipart/mixed");\r
+        deleteMethod.addRequestHeader("Accept", "application/xml");\r
+        deleteMethod.setRequestHeader("Authorization", "Basic " + authForTest);\r
+        deleteMethod.setRequestHeader("X-XmlReplay-fromTestID", fromTestID);\r
+        int statusCode1 = 0;\r
+        String res = "";\r
+        try {\r
+            statusCode1 = client.executeMethod(deleteMethod);\r
+            pr.responseCode = statusCode1;\r
+            //System.out.println("statusCode: "+statusCode1+" statusLine ==>" + deleteMethod.getStatusLine());\r
+            pr.responseMessage = deleteMethod.getStatusText();\r
+            res = deleteMethod.getResponseBodyAsString();\r
+            deleteMethod.releaseConnection();\r
+        } catch (Throwable t){\r
+            pr.error = t.toString();\r
+        }\r
+        pr.result = res;\r
+        pr.responseCode = statusCode1;\r
+        return pr;\r
+    }\r
+\r
+    public static ServiceResult doLIST(String urlString, String listQueryParams, String authForTest, String fromTestID) throws Exception {\r
+        //String u = Tools.glue(urlString, "/", "items/");\r
+        if (Tools.notEmpty(listQueryParams)){\r
+            urlString = Tools.glue(urlString, "?", listQueryParams);\r
+        }\r
+        return doGET(urlString, authForTest, fromTestID);\r
+    }\r
+\r
+    public static final String MULTIPART_MIXED = "multipart/mixed";\r
+    public static final String APPLICATION_XML = "application/xml";\r
+\r
+    /** Use this overload for multipart messages. */\r
+    public static ServiceResult doPOST_PUTFromXML_Multipart(List<String> filesList,\r
+                                                                      List<String> partsList,\r
+                                                                      String protoHostPort,\r
+                                                                      String uri,\r
+                                                                      String method,\r
+                                                                      XmlReplayEval evalStruct,\r
+                                                                      String authForTest,\r
+                                                                      String fromTestID)\r
+                                                                      throws Exception {\r
+        if (  filesList==null||filesList.size()==0\r
+            ||partsList==null||partsList.size()==0\r
+            ||(partsList.size() != filesList.size())){\r
+            throw new Exception("filesList and partsList must not be empty and must have the same number of items each.");\r
+        }\r
+        String content = DD + BOUNDARY;\r
+\r
+        for (int i=0; i<partsList.size(); i++){\r
+            String fileName = filesList.get(i);\r
+            String commonPartName = partsList.get(i);\r
+            byte[] b = FileUtils.readFileToByteArray(new File(fileName));\r
+            String xmlString = new String(b);\r
+\r
+            xmlString = evalStruct.eval(xmlString, evalStruct.serviceResultsMap, evalStruct.jexl, evalStruct.jc);\r
+\r
+            content = content + CRLF + "label: "+commonPartName + CRLF\r
+                              + "Content-Type: application/xml" + CRLF\r
+                              + CRLF\r
+                              + xmlString + CRLF\r
+                              + DD + BOUNDARY;\r
+        }\r
+        content = content + DD;\r
+        String urlString = protoHostPort+uri;\r
+        return doPOST_PUT(urlString, content, BOUNDARY, method, MULTIPART_MIXED, authForTest, fromTestID); //method is POST or PUT.\r
+    }\r
+\r
+    /** Use this overload for NON-multipart messages, that is, regular POSTs. */\r
+        public static ServiceResult doPOST_PUTFromXML(String fileName,\r
+                                                                String protoHostPort,\r
+                                                                String uri,\r
+                                                                String method,\r
+                                                                String contentType,\r
+                                                                XmlReplayEval evalStruct,\r
+                                                                String authForTest,\r
+                                                                String fromTestID)\r
+    throws Exception {\r
+        byte[] b = FileUtils.readFileToByteArray(new File(fileName));\r
+        String xmlString = new String(b);\r
+        xmlString = evalStruct.eval(xmlString, evalStruct.serviceResultsMap, evalStruct.jexl, evalStruct.jc);\r
+        String urlString = protoHostPort+uri;\r
+        return doPOST_PUT(urlString, xmlString, BOUNDARY, method, contentType, authForTest, fromTestID); //method is POST or PUT.\r
+    }\r
+\r
+\r
+    public static ServiceResult doPOST_PUT(String urlString, String content, String boundary, String method, String contentType,\r
+                                           String authForTest, String fromTestID) throws Exception {\r
+        URL url = new URL(urlString);\r
+        HttpURLConnection conn;\r
+        conn = (HttpURLConnection) url.openConnection();\r
+\r
+        if (MULTIPART_MIXED.equalsIgnoreCase(contentType)){\r
+            conn.setRequestProperty("Accept", "multipart/mixed");\r
+            conn.setRequestProperty("content-type", "multipart/mixed; boundary=" + boundary);\r
+        } else {\r
+            conn.setRequestProperty("Accept", "application/xml");\r
+            conn.setRequestProperty("content-type", contentType);\r
+        }\r
+        conn.setRequestProperty("Authorization", "Basic " + authForTest);  //TODO: remove test user : hard-coded as "dGVzdDp0ZXN0"\r
+        conn.setRequestProperty("Connection", "close");\r
+        conn.setRequestProperty("X-XmlReplay-fromTestID", fromTestID);\r
+        conn.setDoOutput(true);\r
+        conn.setDoInput(true);\r
+        conn.setRequestMethod(method); // "POST" or "PUT"\r
+        OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());\r
+        wr.write(content);\r
+        wr.flush();\r
+\r
+        ServiceResult result = new ServiceResult();\r
+        try {\r
+            result.responseCode = conn.getResponseCode();\r
+            //System.out.println("responseCode: "+result.responseCode);\r
+            if (400 <= result.responseCode && result.responseCode <= 499){\r
+                return result;\r
+            }\r
+            BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));\r
+            String line;\r
+            StringBuffer sb = new StringBuffer();\r
+            while ((line = rd.readLine()) != null) {\r
+                sb.append(line).append("\r\n");\r
+            }\r
+            String msg = sb.toString();\r
+            result.result = msg;\r
+            rd.close();\r
+        } catch (Throwable t){\r
+            //System.err.println("ERROR getting content from response: "+t);\r
+            result.error = t.toString();\r
+        }\r
+        wr.close();\r
+\r
+\r
+        String deleteURL = "";\r
+        String location = "";\r
+        Map<String, List<String>> headers = conn.getHeaderFields();\r
+        List<String> locations = headers.get("Location");\r
+        if (locations != null){\r
+            String locationZero = locations.get(0);\r
+            if (locationZero != null){\r
+                String[] segments = locationZero.split("/");\r
+                location = segments[segments.length - 1];\r
+                deleteURL = Tools.glue(urlString, "/", location);\r
+            }\r
+        }\r
+        result.location = location;\r
+        result.deleteURL = deleteURL;\r
+        result.CSID = location;\r
+        result.method = method;\r
+        return result;\r
+    }\r
+\r
+}\r