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
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
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
216 for (String config : configs) {
217 String dstDir = config.split(" to ")[1];
218
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("driver", "com.mysql.jdbc.Driver");
292 * properties.put("url", "jdbc:mysql://localhost:3306/someDb");
293 * properties.put("user", "someUserName");
294 * properties.put("password", "somePassword");
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
427
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 }