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("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 | // 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 | } |