1 package org.testng.xml; 2 3 import org.testng.collections.Lists; 4 import org.testng.collections.Maps; 5 import org.xml.sax.SAXException; 6 7 import javax.xml.parsers.ParserConfigurationException; 8 9 import java.io.File; 10 import java.io.FileInputStream; 11 import java.io.FileNotFoundException; 12 import java.io.IOException; 13 import java.io.InputStream; 14 import java.util.Collection; 15 import java.util.List; 16 import java.util.Map; 17 import java.util.ServiceLoader; 18 19 /** 20 * <code>Parser</code> is a parser for a TestNG XML test suite file. 21 */ 22 public class Parser { 23 24 /** The name of the TestNG DTD. */ 25 public static final String TESTNG_DTD = "testng-1.0.dtd"; 26 27 /** The URL to the deprecated TestNG DTD. */ 28 public static final String DEPRECATED_TESTNG_DTD_URL = "http://beust.com/testng/" + TESTNG_DTD; 29 30 /** The URL to the TestNG DTD. */ 31 public static final String TESTNG_DTD_URL = "http://testng.org/" + TESTNG_DTD; 32 33 /** The default file name for the TestNG test suite if none is specified (testng.xml). */ 34 public static final String DEFAULT_FILENAME = "testng.xml"; 35 36 private static final ISuiteParser DEFAULT_FILE_PARSER = new SuiteXmlParser(); 37 private static final List<ISuiteParser> PARSERS = Lists.newArrayList(DEFAULT_FILE_PARSER); 38 static { 39 ServiceLoader<ISuiteParser> suiteParserLoader = ServiceLoader.load(ISuiteParser.class); 40 for (ISuiteParser parser : suiteParserLoader) { 41 PARSERS.add(parser); 42 } 43 } 44 45 /** The file name of the xml suite being parsed. This may be null if the Parser 46 * has not been initialized with a file name. TODO CQ This member is never used. */ 47 private String m_fileName; 48 49 private InputStream m_inputStream; 50 private IPostProcessor m_postProcessor; 51 52 private boolean m_loadClasses = true; 53 54 /** 55 * Constructs a <code>Parser</code> to use the inputStream as the source of 56 * the xml test suite to parse. 57 * @param fileName the filename corresponding to the inputStream or null if 58 * unknown. 59 */ Parser(String fileName)60 public Parser(String fileName) { 61 init(fileName, null, null); 62 } 63 64 /** 65 * Creates a parser that will try to find the DEFAULT_FILENAME from the jar. 66 * @throws FileNotFoundException if the DEFAULT_FILENAME resource is not 67 * found in the classpath. 68 */ Parser()69 public Parser() throws FileNotFoundException { 70 init(null, null, null); 71 } 72 Parser(InputStream is)73 public Parser(InputStream is) { 74 init(null, is, null); 75 } 76 init(String fileName, InputStream is, IFileParser fp)77 private void init(String fileName, InputStream is, IFileParser fp) { 78 m_fileName = fileName != null ? fileName : DEFAULT_FILENAME; 79 m_inputStream = is; 80 } 81 setPostProcessor(IPostProcessor processor)82 public void setPostProcessor(IPostProcessor processor) { 83 m_postProcessor = processor; 84 } 85 86 /** 87 * If false, don't try to load the classes during the parsing. 88 */ setLoadClasses(boolean loadClasses)89 public void setLoadClasses(boolean loadClasses) { 90 m_loadClasses = loadClasses; 91 } 92 93 /** 94 * Returns an input stream on the resource named DEFAULT_FILENAME. 95 * 96 * @return an input stream on the resource named DEFAULT_FILENAME. 97 * @throws FileNotFoundException if the DEFAULT_FILENAME resource is not 98 * found in the classpath. 99 */ 100 // private static InputStream getInputStream(String fileName) throws FileNotFoundException { 101 // // Try to look for the DEFAULT_FILENAME from the jar 102 // ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 103 // InputStream in; 104 // // TODO CQ is this OK? should we fall back to the default classloader if the 105 // // context classloader fails. 106 // if (classLoader != null) { 107 // in = classLoader.getResourceAsStream(fileName); 108 // } 109 // else { 110 // in = Parser.class.getResourceAsStream(fileName); 111 // } 112 // if (in == null) { 113 // throw new FileNotFoundException(fileName); 114 // } 115 // return in; 116 // } 117 getParser(String fileName)118 private static IFileParser getParser(String fileName) { 119 for (ISuiteParser parser : PARSERS) { 120 if (parser.accept(fileName)) { 121 return parser; 122 } 123 } 124 125 return DEFAULT_FILE_PARSER; 126 } 127 128 /** 129 * Parses the TestNG test suite and returns the corresponding XmlSuite, 130 * and possibly, other XmlSuite that are pointed to by <suite-files> 131 * tags. 132 * 133 * @return the parsed TestNG test suite. 134 * 135 * @throws ParserConfigurationException 136 * @throws SAXException 137 * @throws IOException if an I/O error occurs while parsing the test suite file or 138 * if the default testng.xml file is not found. 139 */ parse()140 public Collection<XmlSuite> parse() 141 throws ParserConfigurationException, SAXException, IOException 142 { 143 // Each suite found is put in this list, using their canonical 144 // path to make sure we don't add a same file twice 145 // (e.g. "testng.xml" and "./testng.xml") 146 List<String> processedSuites = Lists.newArrayList(); 147 XmlSuite resultSuite = null; 148 149 List<String> toBeParsed = Lists.newArrayList(); 150 List<String> toBeAdded = Lists.newArrayList(); 151 List<String> toBeRemoved = Lists.newArrayList(); 152 153 if (m_fileName != null) { 154 File mainFile = new File(m_fileName); 155 toBeParsed.add(mainFile.getCanonicalPath()); 156 } 157 158 /* 159 * Keeps a track of parent XmlSuite for each child suite 160 */ 161 Map<String, XmlSuite> childToParentMap = Maps.newHashMap(); 162 while (toBeParsed.size() > 0) { 163 164 for (String currentFile : toBeParsed) { 165 File currFile = new File(currentFile); 166 File parentFile = currFile.getParentFile(); 167 InputStream inputStream = m_inputStream != null 168 ? m_inputStream 169 : new FileInputStream(currentFile); 170 171 IFileParser<XmlSuite> fileParser = getParser(currentFile); 172 XmlSuite currentXmlSuite = fileParser.parse(currentFile, inputStream, m_loadClasses); 173 processedSuites.add(currentFile); 174 toBeRemoved.add(currentFile); 175 176 if (childToParentMap.containsKey(currentFile)) { 177 XmlSuite parentSuite = childToParentMap.get(currentFile); 178 //Set parent 179 currentXmlSuite.setParentSuite(parentSuite); 180 //append children 181 parentSuite.getChildSuites().add(currentXmlSuite); 182 } 183 184 if (null == resultSuite) { 185 resultSuite = currentXmlSuite; 186 } 187 188 List<String> suiteFiles = currentXmlSuite.getSuiteFiles(); 189 if (suiteFiles.size() > 0) { 190 for (String path : suiteFiles) { 191 String canonicalPath; 192 if (parentFile != null && new File(parentFile, path).exists()) { 193 canonicalPath = new File(parentFile, path).getCanonicalPath(); 194 } else { 195 canonicalPath = new File(path).getCanonicalPath(); 196 } 197 if (!processedSuites.contains(canonicalPath)) { 198 toBeAdded.add(canonicalPath); 199 childToParentMap.put(canonicalPath, currentXmlSuite); 200 } 201 } 202 } 203 } 204 205 // 206 // Add and remove files from toBeParsed before we loop 207 // 208 for (String s : toBeRemoved) { 209 toBeParsed.remove(s); 210 } 211 toBeRemoved = Lists.newArrayList(); 212 213 for (String s : toBeAdded) { 214 toBeParsed.add(s); 215 } 216 toBeAdded = Lists.newArrayList(); 217 218 } 219 220 //returning a list of single suite to keep changes minimum 221 List<XmlSuite> resultList = Lists.newArrayList(); 222 resultList.add(resultSuite); 223 224 boolean postProcess = true; 225 226 if (postProcess && m_postProcessor != null) { 227 return m_postProcessor.process(resultList); 228 } else { 229 return resultList; 230 } 231 232 } 233 parseToList()234 public List<XmlSuite> parseToList() 235 throws ParserConfigurationException, SAXException, IOException 236 { 237 List<XmlSuite> result = Lists.newArrayList(); 238 Collection<XmlSuite> suites = parse(); 239 for (XmlSuite suite : suites) { 240 result.add(suite); 241 } 242 243 return result; 244 } 245 246 247 248 } 249 250