View Javadoc

1   package atg.test;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.lang.reflect.Method;
6   import java.sql.SQLException;
7   import java.util.ArrayList;
8   import java.util.Arrays;
9   import java.util.HashMap;
10  import java.util.Iterator;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.Properties;
14  import java.util.Map.Entry;
15  
16  import junit.framework.TestCase;
17  
18  import org.apache.commons.io.FileUtils;
19  import org.apache.log4j.Logger;
20  
21  import atg.nucleus.GenericService;
22  import atg.nucleus.Nucleus;
23  import atg.nucleus.logging.ClassLoggingFactory;
24  import atg.nucleus.logging.ConsoleLogListener;
25  import atg.test.configuration.BasicConfiguration;
26  import atg.test.configuration.RepositoryConfiguration;
27  import atg.test.util.FileUtil;
28  import atg.test.util.RepositoryManager;
29  
30  /***
31   * Replacement base class for {@link AtgDustTestCase}. Extend this class and use
32   * the following 'pattern' whenever you want to junit test some atg components:
33   * <ul>
34   * <li><b>Copy</b> all needed configuration and repository mapping files to a
35   * staging location outside of your source tree using<b>
36   * {@link AtgDustCase#copyConfigurationFiles(String[], String, String...)}</b>.
37   * The staging directory will automatically be used as the configuration
38   * directory. Copying all needed priorities to a location outside of the source
39   * tree is the preferred method, because this frameworks creates properties on
40   * the fly and that could pollute your current source tree.</li>
41   * <!--
42   * <li><b>
43   * 
44   * <i>Or: </i></b>tell {@link AtgDustCase} class where the configuration
45   * location is by using <b>{@link AtgDustCase#setConfigurationLocation(String)}
46   * </b>, but be aware that the location will also be used for properties file
47   * generation.</li>
48   * -->
49   * </ul>
50   * 
51   * <!-- p> <b>Rule of thumb:</b> When running repository tests, copy everything
52   * outside of your source tree (or when you use maven, use the target directory
53   * ). If you run basic component/formhandler tests, pointing it to your existing
54   * configuration directory might be sufficient.
55   * 
56   * </p-->
57   * 
58   * Repository based tests are depended on one of the two steps previously
59   * described plus:
60   * <ul>
61   * <li><b>{@link AtgDustCase#prepareRepository(String, String...)}</b> for
62   * testing against an default in-memory hsql database or <b>
63   * {@link AtgDustCase#prepareRepository(String, Properties, boolean, String...)}
64   * </b> for testing against an existing database.</li>
65   * </ul>
66   * 
67   * If you need to generate some components "on the fly":
68   * <ul>
69   * <li><b>{@link AtgDustCase#createPropertyFile(String, String, Class)}</b></li>
70   * </ul>
71   * 
72   * <p>
73   * Example usage can be found in test.SongsRepositoryTest.
74   * </p>
75   * 
76   * <p>
77   * This class overrides Junit 3 and not Junit 4 because currently Junit 4 has
78   * some test runner/eclipse related bugs which makes it impossible for me to use
79   * it.
80   * </p>
81   * 
82   * @author robert
83   */
84  @SuppressWarnings("unchecked")
85  public class AtgDustCase extends TestCase {
86  
87  	private static final Logger log = Logger.getLogger(AtgDustCase.class);
88  	private RepositoryManager repositoryManager = new RepositoryManager();
89  	private final BasicConfiguration basicConfiguration = new BasicConfiguration();
90  	private File configurationLocation;
91  	private Nucleus nucleus;
92  	private boolean isDebug;
93  	private String atgConfigPath;
94  	private String environment;
95  	private String localConfig;
96  	private List<String> configDstsDir;
97  	private static Map<String, Long> CONFIG_FILES_TIMESTAMPS,
98  			CONFIG_FILES_GLOBAL_FORCE = null;
99  	private static Class<?> perflib;
100 
101 	public static final File TIMESTAMP_SER = new File(System
102 			.getProperty("java.io.tmpdir")
103 			+ File.separator + "atg-dust-tstamp-rh.ser"),
104 			GLOBAL_FORCE_SER = new File(System.getProperty("java.io.tmpdir")
105 					+ File.separator + "atg-dust-gforce-rh.ser");
106 	private static long SERIAL_TTL = 43200000L;
107 
108 	/***
109 	 * Every *.properties file copied using this method will have it's scope (if
110 	 * one is available) set to global.
111 	 * 
112 	 * @param srcDirs
113 	 *            One or more directories containing needed configuration files.
114 	 * @param dstDir
115 	 *            where to copy the above files to. This will also be the
116 	 *            configuration location.
117 	 * @param excludes
118 	 *            One or more directories not to include during the copy
119 	 *            process. Use this one to speeds up the test cycle
120 	 *            considerably. You can also call it with an empty
121 	 *            {@link String[]} or <code>null</code> if nothing should be
122 	 *            excluded
123 	 * @throws IOException
124 	 *             Whenever some file related error's occur.
125 	 */
126 	protected final void copyConfigurationFiles(final String[] srcDirs,
127 			final String dstDir, final String... excludes) throws IOException {
128 
129 		setConfigurationLocation(dstDir);
130 
131 		if (log.isDebugEnabled()) {
132 			log.debug("Copying configuration files and "
133 					+ "forcing global scope on all configs");
134 		}
135 		preCopyingOfConfigurationFiles(srcDirs, excludes);
136 
137 		for (final String srcs : srcDirs) {
138 			FileUtil.copyDirectory(srcs, dstDir, Arrays
139 					.asList(excludes == null ? new String[] {} : excludes));
140 		}
141 
142 		forceGlobalScopeOnAllConfigs(dstDir);
143 
144 		if (FileUtil.isDirty()) {
145 			FileUtil.serialize(GLOBAL_FORCE_SER, FileUtil
146 					.getConfigFilesTimestamps());
147 		}
148 
149 	}
150 
151 	/***
152 	 * Donated by Remi Dupuis
153 	 * 
154 	 * @param properties
155 	 * @throws IOException
156 	 */
157 	protected final void manageConfigurationFiles(Properties properties)
158 			throws IOException {
159 
160 		String atgConfigPath = properties.getProperty("atgConfigsJars")
161 				.replace("/", File.separator);
162 		String[] configs = properties.getProperty("configs").split(",");
163 		String environment = properties.getProperty("environment");
164 		String localConfig = properties.getProperty("localConfig");
165 		String[] excludes = properties.getProperty("excludes").split(",");
166 		String rootConfigDir = properties.getProperty("rootConfigDir").replace(
167 				"/", File.separator);
168 		int i = 0;
169 		for (String conf : configs) {
170 			String src = conf.split(" to ")[0];
171 			String dst = conf.split(" to ")[1];
172 			configs[i] = (rootConfigDir + "/" + src.trim() + " to "
173 					+ rootConfigDir + "/" + dst.trim()).replace("/",
174 					File.separator);
175 			i++;
176 		}
177 		i = 0;
178 		for (String dir : excludes) {
179 			excludes[i] = dir.trim();
180 			i++;
181 		}
182 		final List<String> srcsAsList = new ArrayList<String>();
183 		final List<String> distsAsList = new ArrayList<String>();
184 
185 		for (String config : configs) {
186 			srcsAsList.add(config.split(" to ")[0]);
187 			distsAsList.add(config.split(" to ")[1]);
188 		}
189 
190 		this.atgConfigPath = atgConfigPath;
191 		this.environment = environment;
192 		this.localConfig = localConfig;
193 		// The Last dstdir is used for Configuration location
194 		setConfigurationLocation(distsAsList.get(distsAsList.size() - 1));
195 
196 		if (log.isDebugEnabled()) {
197 			log.debug("Copying configuration files and "
198 					+ "forcing global scope on all configs");
199 		}
200 		preCopyingOfConfigurationFiles(srcsAsList.toArray(new String[] {}),
201 				excludes);
202 
203 		log.info("Copying configuration files and "
204 				+ "forcing global scope on all configs");
205 		// copy all files to it's destination
206 		for (String config : configs) {
207 			FileUtil.copyDirectory(config.split(" to ")[0], config
208 					.split(" to ")[1], Arrays
209 					.asList(excludes == null ? new String[] {} : excludes));
210 			log.debug(config);
211 			log.debug(config.split(" to ")[0]);
212 			log.debug(config.split(" to ")[1]);
213 		}
214 
215 		// forcing global scope on all configurations
216 		for (String config : configs) {
217 			String dstDir = config.split(" to ")[1];
218 			// forcing global scope on all property files
219 			forceGlobalScopeOnAllConfigs(dstDir);
220 		}
221 		this.configDstsDir = distsAsList;
222 
223 	}
224 
225 	/***
226 	 * @param configurationStagingLocation
227 	 *            The location where the property file should be created. This
228 	 *            will also set the {@link AtgDustCase#configurationLocation}.
229 	 * 
230 	 * @param nucleusComponentPath
231 	 *            Nucleus component path (e.g /Some/Service/Impl).
232 	 * 
233 	 * @param clazz
234 	 *            The {@link Class} implementing the nucleus component specified
235 	 *            in previous argument.
236 	 * 
237 	 * @throws IOException
238 	 *             If we have some File related errors
239 	 */
240 	protected final void createPropertyFile(
241 			final String configurationStagingLocation,
242 			final String nucleusComponentPath, final Class<?> clazz)
243 			throws IOException {
244 		this.configurationLocation = new File(configurationStagingLocation);
245 		FileUtil.createPropertyFile(nucleusComponentPath,
246 				configurationLocation, clazz.getClass(),
247 				new HashMap<String, String>());
248 	}
249 
250 	/***
251 	 * Prepares a test against an default in-memory hsql database.
252 	 * 
253 	 * @param repoPath
254 	 *            the nucleus component path of the repository to be tested.
255 	 * 
256 	 * @param definitionFiles
257 	 *            one or more repository definition files.
258 	 * @throws IOException
259 	 *             The moment we have some properties/configuration related
260 	 *             error
261 	 * @throws SQLException
262 	 *             Whenever there is a database related error
263 	 * 
264 	 */
265 	protected final void prepareRepository(final String repoPath,
266 			final String... definitionFiles) throws SQLException, IOException {
267 
268 		final Properties properties = new Properties();
269 		properties.put("driver", "org.hsqldb.jdbcDriver");
270 		properties.put("url", "jdbc:hsqldb:mem:testDb");
271 		properties.put("user", "sa");
272 		properties.put("password", "");
273 
274 		prepareRepository(repoPath, properties, true, true, definitionFiles);
275 
276 	}
277 
278 	/***
279 	 * Prepares a test against an existing database.
280 	 * 
281 	 * @param repositoryPath
282 	 *            The the repository to be tested, specified as nucleus
283 	 *            component path.
284 	 * @param connectionProperties
285 	 *            A {@link Properties} instance with the following values (in
286 	 *            this example the properties are geared towards an mysql
287 	 *            database):
288 	 * 
289 	 *            <pre>
290 	 * final Properties properties = new Properties();
291 	 * properties.put(&quot;driver&quot;, &quot;com.mysql.jdbc.Driver&quot;);
292 	 * properties.put(&quot;url&quot;, &quot;jdbc:mysql://localhost:3306/someDb&quot;);
293 	 * properties.put(&quot;user&quot;, &quot;someUserName&quot;);
294 	 * properties.put(&quot;password&quot;, &quot;somePassword&quot;);
295 	 * </pre>
296 	 * 
297 	 * 
298 	 * @param dropTables
299 	 *            If <code>true</code> then existing tables will be dropped and
300 	 *            re-created, if set to <code>false</code> the existing tables
301 	 *            will be used.
302 	 * 
303 	 * @param createTables
304 	 *            if set to <code>true</code> all non existing tables needed for
305 	 *            the current test run will be created, if set to
306 	 *            <code>false</code> this class expects all needed tables for
307 	 *            this test run to be already created
308 	 * 
309 	 * @param definitionFiles
310 	 *            One or more needed repository definition files.
311 	 * @throws IOException
312 	 *             The moment we have some properties/configuration related
313 	 *             error
314 	 * @throws SQLException
315 	 *             Whenever there is a database related error
316 	 * 
317 	 */
318 	protected final void prepareRepository(final String repositoryPath,
319 			final Properties connectionProperties, final boolean dropTables,
320 			final boolean createTables, final String... definitionFiles)
321 			throws SQLException, IOException {
322 
323 		final Map<String, String> connectionSettings = new HashMap<String, String>();
324 
325 		for (final Iterator<Entry<Object, Object>> it = connectionProperties
326 				.entrySet().iterator(); it.hasNext();) {
327 			final Entry<Object, Object> entry = it.next();
328 			connectionSettings.put((String) entry.getKey(), (String) entry
329 					.getValue());
330 
331 		}
332 		final RepositoryConfiguration repositoryConfiguration = new RepositoryConfiguration();
333 
334 		repositoryConfiguration.setDebug(isDebug);
335 		repositoryConfiguration
336 				.createPropertiesByConfigurationLocation(configurationLocation);
337 		repositoryConfiguration.createFakeXADataSource(configurationLocation,
338 				connectionSettings);
339 		repositoryConfiguration.createRepositoryConfiguration(
340 				configurationLocation, repositoryPath, dropTables,
341 				createTables, definitionFiles);
342 
343 		repositoryManager.initializeMinimalRepositoryConfiguration(
344 				configurationLocation, repositoryPath, connectionSettings,
345 				dropTables, isDebug, definitionFiles);
346 	}
347 
348 	/***
349 	 * Method for retrieving a fully injected atg component
350 	 * 
351 	 * @param nucleusComponentPath
352 	 *            Path to a nucleus component (e.g. /Some/Service/Impl).
353 	 * @return Fully injected instance of the component registered under
354 	 *         previous argument or <code>null</code> if there is an error.
355 	 * @throws IOException
356 	 */
357 	protected Object resolveNucleusComponent(final String nucleusComponentPath)
358 			throws IOException {
359 		startNucleus(configurationLocation);
360 		return enableLoggingOnGenericService(nucleus
361 				.resolveName(nucleusComponentPath));
362 	}
363 
364 	/***
365 	 * Call this method to set the configuration location.
366 	 * 
367 	 * @param configurationLocation
368 	 *            The configuration location to set. Most of the time this
369 	 *            location is a directory containing all repository definition
370 	 *            files and component property files which are needed for the
371 	 *            test.
372 	 */
373 	protected final void setConfigurationLocation(
374 			final String configurationLocation) {
375 		this.configurationLocation = new File(configurationLocation);
376 		if (log.isDebugEnabled()) {
377 			log.debug("Using configuration location: "
378 					+ this.configurationLocation.getPath());
379 		}
380 	}
381 
382 	/***
383 	 * Always make sure to call this because it will do necessary clean up
384 	 * actions (shutting down in-memory database (if it was used) and the
385 	 * nucleus) so he next test can run safely.
386 	 */
387 	@Override
388 	protected void tearDown() throws Exception {
389 		super.tearDown();
390 		if (repositoryManager != null) {
391 			repositoryManager.shutdownInMemoryDbAndCloseConnections();
392 		}
393 		if (nucleus != null) {
394 			nucleus.doStopService();
395 			nucleus.stopService();
396 			nucleus.destroy();
397 		}
398 	}
399 
400 	/***
401 	 * Enables or disables the debug level of nucleus components.
402 	 * 
403 	 * @param isDebug
404 	 *            Setting this to <code>true</code> will enable debug on all
405 	 *            (currently only on repository related) components, setting it
406 	 *            to <code>false</code> turn's the debug off again.
407 	 */
408 	protected void setDebug(boolean isDebug) {
409 		this.isDebug = isDebug;
410 	}
411 
412 	/***
413 	 * 
414 	 * @param configpath
415 	 * @return
416 	 * @throws IOException
417 	 */
418 	private void startNucleus(final File configpath) throws IOException {
419 		if (nucleus == null || !nucleus.isRunning()) {
420 			ClassLoggingFactory.getFactory();
421 			basicConfiguration.setDebug(isDebug);
422 			basicConfiguration
423 					.createPropertiesByConfigurationLocation(configpath);
424 			System.setProperty("atg.dynamo.license.read", "true");
425 			System.setProperty("atg.license.read", "true");
426 			// TODO: Can I safely keep this one disabled?
427 			// NucleusServlet.addNamingFactoriesAndProtocolHandlers();
428 
429 			if (environment != null && !environment.equals("")) {
430 				for (String property : environment.split(";")) {
431 					String[] keyvalue = property.split("=");
432 					System.setProperty(keyvalue[0], keyvalue[1]);
433 					log.info(keyvalue[0] + "=" + keyvalue[1]);
434 				}
435 			}
436 
437 			String fullConfigPath = "";
438 			if (atgConfigPath != null && !atgConfigPath.equals("")) {
439 				fullConfigPath = atgConfigPath + ";" + fullConfigPath;
440 			}
441 			if (configDstsDir != null && configDstsDir.size() > 0) {
442 				for (String dst : configDstsDir) {
443 					fullConfigPath = fullConfigPath + dst + ";";
444 				}
445 			} else
446 				fullConfigPath = configpath.getAbsolutePath();
447 			if (atgConfigPath != null && !atgConfigPath.equals(""))
448 				fullConfigPath = fullConfigPath
449 						+ localConfig.replace("/", File.separator);
450 
451 			log.info("The full config path used to start nucleus: "
452 					+ fullConfigPath);
453 			System.setProperty("atg.configpath", new File(fullConfigPath)
454 					.getAbsolutePath());
455 			nucleus = Nucleus.startNucleus(new String[] { fullConfigPath });
456 
457 		}
458 	}
459 
460 	/***
461 	 * Will enable logging on the object/service that was passed in (as a method
462 	 * argument) if it's an instance of {@link GenericService}. This method is
463 	 * automatically called from
464 	 * {@link AtgDustCase#resolveNucleusComponent(String)}. Debug level is
465 	 * enabled the moment {@link AtgDustCase#setDebug(boolean)} was called with
466 	 * <code>true</code>.
467 	 * 
468 	 * @param service
469 	 *            an instance of GenericService
470 	 * 
471 	 * @return the GenericService instance that was passed in with all log
472 	 *         levels enabled, if it's a {@link GenericService}
473 	 */
474 	private Object enableLoggingOnGenericService(final Object service) {
475 		if (service instanceof GenericService) {
476 			((GenericService) service).setLoggingDebug(isDebug);
477 			((GenericService) service).setLoggingInfo(true);
478 			((GenericService) service).setLoggingWarning(true);
479 			((GenericService) service).setLoggingError(true);
480 			((GenericService) service)
481 					.removeLogListener(new ConsoleLogListener());
482 			((GenericService) service).addLogListener(new ConsoleLogListener());
483 		}
484 		return service;
485 	}
486 
487 	private void preCopyingOfConfigurationFiles(final String[] srcDirs,
488 			final String excludes[]) throws IOException {
489 		boolean isDirty = false;
490 		for (final String src : srcDirs) {
491 			for (final File file : (List<File>) FileUtils.listFiles(new File(
492 					src), null, true)) {
493 				if (!Arrays.asList(
494 						excludes == null ? new String[] {} : excludes)
495 						.contains(file.getName())
496 						&& !file.getPath().contains(".svn") && file.isFile()) {
497 					if (CONFIG_FILES_TIMESTAMPS.get(file.getPath()) != null
498 							&& file.lastModified() == CONFIG_FILES_TIMESTAMPS
499 									.get(file.getPath())) {
500 					} else {
501 						CONFIG_FILES_TIMESTAMPS.put(file.getPath(), file
502 								.lastModified());
503 						isDirty = true;
504 					}
505 				}
506 			}
507 		}
508 		if (isDirty) {
509 			if (log.isDebugEnabled()) {
510 				log
511 						.debug("Config files timestamps map is dirty an will be re serialized");
512 			}
513 
514 			FileUtil.serialize(TIMESTAMP_SER, CONFIG_FILES_TIMESTAMPS);
515 		}
516 
517 		FileUtil.setConfigFilesTimestamps(CONFIG_FILES_TIMESTAMPS);
518 		FileUtil.setConfigFilesGlobalForce(CONFIG_FILES_GLOBAL_FORCE);
519 	}
520 
521 	private void forceGlobalScopeOnAllConfigs(final String dstDir)
522 			throws IOException {
523 		if (perflib == null) {
524 			for (final File file : (List<File>) FileUtils.listFiles(new File(
525 					dstDir), new String[] { "properties" }, true)) {
526 				new FileUtil().searchAndReplace("$scope=", "$scope=global\n",
527 						file);
528 			}
529 		} else {
530 			try {
531 				List<File> payload = (List<File>) FileUtils.listFiles(new File(
532 						dstDir), new String[] { "properties" }, true);
533 
534 				Method schedule = perflib.getMethod("schedule", new Class[] {
535 						int.class, List.class, Class.class, String.class,
536 						Class[].class, List.class });
537 
538 				List<Object> list = new ArrayList<Object>();
539 				list.add("$scope=");
540 				list.add("$scope=global\n");
541 				schedule.invoke(perflib.newInstance(), 4, payload,
542 						FileUtil.class, "searchAndReplace", new Class[] {
543 								String.class, String.class, File.class }, list);
544 			} catch (Exception e) {
545 				log.error("Error: ", e);
546 			}
547 		}
548 
549 	}
550 
551 	static {
552 		final String s = System.getProperty("SERIAL_TTL");
553 		if (log.isDebugEnabled()) {
554 			log.debug(s == null ? "SERIAL_TTL has not been set "
555 					+ "using default value of: " + SERIAL_TTL
556 					+ " m/s or start VM with -DSERIAL_TTL=some_number_value"
557 					: "SERIAL_TTL is set to:" + s);
558 		}
559 		try {
560 			SERIAL_TTL = s != null ? Long.parseLong(s) * 1000 : SERIAL_TTL;
561 		} catch (NumberFormatException e) {
562 			log.error("Error using the -DSERIAL_TTL value: ", e);
563 		}
564 		CONFIG_FILES_TIMESTAMPS = FileUtil.deserialize(TIMESTAMP_SER,
565 				SERIAL_TTL);
566 		CONFIG_FILES_GLOBAL_FORCE = FileUtil.deserialize(GLOBAL_FORCE_SER,
567 				SERIAL_TTL);
568 
569 		try {
570 			perflib = Class
571 					.forName("com.bsdroot.util.concurrent.SchedulerService");
572 		} catch (ClassNotFoundException e) {
573 			log
574 					.debug("com.bsdroot.util.concurrent experimantal performance library not found, continuing normally");
575 		}
576 	}
577 
578 }