1 | /** |
2 | * Copyright 2009 ATG DUST Project |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * |
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 |
8 | * |
9 | * Unless required by applicable law or agreed to in writing, software |
10 | * distributed under the License is distributed on an "AS IS" BASIS, |
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | * See the License for the specific language governing permissions and limitations under the License. |
13 | */ |
14 | package atg.nucleus; |
15 | |
16 | import java.io.File; |
17 | import java.io.FileWriter; |
18 | import java.io.IOException; |
19 | import java.net.MalformedURLException; |
20 | import java.net.ServerSocket; |
21 | import java.net.URI; |
22 | import java.net.URISyntaxException; |
23 | import java.net.URL; |
24 | import java.util.ArrayList; |
25 | import java.util.Collections; |
26 | import java.util.HashMap; |
27 | import java.util.Iterator; |
28 | import java.util.List; |
29 | import java.util.Map; |
30 | import java.util.Properties; |
31 | |
32 | import javax.servlet.ServletException; |
33 | |
34 | import org.apache.log4j.Logger; |
35 | |
36 | import atg.applauncher.AppLauncher; |
37 | import atg.applauncher.AppLauncherException; |
38 | import atg.applauncher.AppModuleManager; |
39 | import atg.applauncher.MultiInstallLocalAppModuleManager; |
40 | import atg.applauncher.dynamo.DynamoServerLauncher; |
41 | import atg.core.io.FileUtils; |
42 | import atg.core.util.CommandProcessor; |
43 | import atg.core.util.JarUtils; |
44 | import atg.core.util.StringUtils; |
45 | import atg.nucleus.naming.ComponentName; |
46 | import atg.nucleus.servlet.NucleusServlet; |
47 | import atg.service.dynamo.ServerConfig; |
48 | import atg.test.util.FileUtil; |
49 | |
50 | /** |
51 | * NucleusTestUtils |
52 | * @author adamb |
53 | * @version $Id: //test/UnitTests/base/main/src/Java/atg/nucleus/NucleusTestUtils.java#21 $ |
54 | * |
55 | * This class contains some utility methods to make it faster |
56 | * to write a unit test that needs to resolve componants against Nucleus. |
57 | * |
58 | */ |
59 | public class NucleusTestUtils { |
60 | |
61 | //------------------------------------- |
62 | /** Class version string */ |
63 | |
64 | public static String CLASS_VERSION = "$Id: //test/UnitTests/base/main/src/Java/atg/nucleus/NucleusTestUtils.java#21 $$Change: 556195 $"; |
65 | |
66 | private static final String EXTRACT_TEMP_JAR_FILE_FOR_PATH = "Extract temp jar file for path "; |
67 | private static final String ATG_DUST_CONFIG = "atg-dust-config-"; |
68 | private static final String FILE = "file:"; |
69 | public static final Logger log = Logger |
70 | .getLogger(NucleusTestUtils.class); |
71 | public static final String ATG_DUST_TESTCONFIG = "atg.dust.testconfig"; |
72 | private static final String ATG_DUST_TESTCONFIG_ENV = "ATG_DUST_TESTCONFIG"; |
73 | |
74 | /** A map from Nucleus instance to temporary directory. Used by |
75 | * startNucleusWithModules. */ |
76 | static Map<Nucleus, File> sNucleusToTempAtgServerDirectory = |
77 | Collections.synchronizedMap(new HashMap<Nucleus, File>()); |
78 | |
79 | /** Whether or not to remove tempoary ATG server directories |
80 | * created by startNucleusWithModules(). True by default, but |
81 | * can be set to false for debugging. */ |
82 | public static boolean sRemoveTempAtgServerDirectories = true; |
83 | |
84 | |
85 | /** |
86 | * Creates an Initial.properties file |
87 | * pRoot The root directory of the configpath |
88 | * pInitialServices A list of initial services |
89 | * @param pRoot The root of the config path entry. |
90 | * @param pInitialServices initial services list |
91 | * @return the create initial services properties file. |
92 | * @exception IOException if an error occurs |
93 | */ |
94 | public static File createInitial(File pRoot,List pInitialServices) throws IOException { |
95 | Properties prop = new Properties(); |
96 | Iterator iter = pInitialServices.iterator(); |
97 | StringBuffer services = new StringBuffer(); |
98 | while (iter.hasNext()) { |
99 | if (services.length() != 0) services.append(","); |
100 | services.append((String)iter.next()); |
101 | } |
102 | prop.put("initialServices",services.toString()); |
103 | return NucleusTestUtils.createProperties("Initial", new File(pRoot.getAbsolutePath()),"atg.nucleus.InitialService", prop); |
104 | } |
105 | |
106 | /** |
107 | * Allows the absoluteName of the given service to be explicitly defined. |
108 | * Normally this is determined by the object's location in the Nucleus |
109 | * hierarchy. For test items that are not really bound to Nucleus, it's |
110 | * convenient to just give it an absolute name rather than going through |
111 | * the whole configuration and binding process. |
112 | |
113 | * @param pName The absolute name value to set |
114 | * @param pService The service whose absolute nameshould be set. |
115 | */ |
116 | public static void setAbsoluteName(String pName, GenericService pService) { |
117 | pService.mAbsoluteName = pName; |
118 | } |
119 | // --------------------- |
120 | /** |
121 | * Adds the given object, pComponent to Nucleus, pNucleus at the path given |
122 | * by pComponentPath. |
123 | * @param pNucleus The Nucleus instance to which the component should be added |
124 | * @param pComponentPath the component path at which the component should be added |
125 | * @param pComponent the component instance to add |
126 | */ |
127 | public static void addComponent( |
128 | Nucleus pNucleus, |
129 | String pComponentPath, |
130 | Object pComponent) { |
131 | // make sure it's not already there |
132 | if (pNucleus.resolveName(pComponentPath) != null) |
133 | return; |
134 | ComponentName name = ComponentName.getComponentName(pComponentPath); |
135 | ComponentName[] subNames = name.getSubNames(); |
136 | GenericContext[] contexts = new GenericContext[subNames.length - 1]; |
137 | contexts[0] = pNucleus; |
138 | for (int i = 1; i < subNames.length - 1; i++) { |
139 | contexts[i] = new GenericContext(); |
140 | // Make sure it's not there |
141 | GenericContext tmpContext = |
142 | (GenericContext) contexts[i - 1].getElement(subNames[i].getName()); |
143 | if (tmpContext == null) |
144 | contexts[i - 1].putElement(subNames[i].getName(), contexts[i]); |
145 | else |
146 | contexts[i] = tmpContext; |
147 | } |
148 | contexts[contexts.length |
149 | - 1].putElement(subNames[subNames.length - 1].getName(), pComponent); |
150 | } |
151 | |
152 | /** |
153 | * Creates a .properties file |
154 | * @param pComponentName Name of the component |
155 | * @param pConfigDir Name of the configuration directory. If null, |
156 | * will add to the current working directory. |
157 | * @param pClass The class of the component (used for $class property). |
158 | * @param pProps Other properties of the component. |
159 | * @return The created file. |
160 | * @throws IOException |
161 | */ |
162 | public static File createProperties(String pComponentName, File pConfigDir, String pClass, Properties pProps) |
163 | throws IOException { |
164 | File prop; |
165 | if (pConfigDir == null) |
166 | prop = new File("./" + pComponentName + ".properties"); |
167 | else { |
168 | pConfigDir.mkdirs(); |
169 | prop = new File(pConfigDir, pComponentName + ".properties"); |
170 | new File(prop.getParent()).mkdirs(); |
171 | } |
172 | |
173 | if (prop.exists()) prop.delete(); |
174 | prop.createNewFile(); |
175 | FileWriter fw = new FileWriter(prop); |
176 | String classLine = "$class=" + pClass + "\n"; |
177 | try { |
178 | if (pClass != null) fw.write(classLine); |
179 | if (pProps != null) { |
180 | Iterator iter = pProps.keySet().iterator(); |
181 | while (iter.hasNext()) { |
182 | String key = (String) iter.next(); |
183 | String thisLine = key + "=" + StringUtils.replace(pProps.getProperty(key),'\\', "\\\\") + "\n"; |
184 | fw.write(thisLine); |
185 | } |
186 | } |
187 | } |
188 | finally { |
189 | fw.flush(); |
190 | fw.close(); |
191 | } |
192 | return prop; |
193 | } |
194 | /** |
195 | * Starts Nucleus using the given config directory |
196 | * @param configpath the config path directory entry |
197 | * to use as the entire config path. |
198 | * @return the started Nucleus |
199 | */ |
200 | public static Nucleus startNucleus(File configpath) { |
201 | return startNucleus(configpath.getAbsolutePath()); |
202 | } |
203 | |
204 | /** |
205 | * Starts Nucleus using the given config directory |
206 | * @param pSingleConfigpathEntry the path name of the config path |
207 | * entry to specify. |
208 | * @return The started nucleus. |
209 | */ |
210 | public static Nucleus startNucleus(String pSingleConfigpathEntry) { |
211 | System.setProperty("atg.dynamo.license.read", "true"); |
212 | System.setProperty("atg.license.read", "true"); |
213 | NucleusServlet.addNamingFactoriesAndProtocolHandlers(); |
214 | Nucleus n = Nucleus.startNucleus(new String[] {pSingleConfigpathEntry}); |
215 | return n; |
216 | } |
217 | |
218 | |
219 | /** Cache of the config path for a given Class. Used by getConfigpath. */ |
220 | static Map<Class, Map<String, File>> sConfigDir = |
221 | new HashMap<Class, Map<String, File>>(); |
222 | |
223 | |
224 | /** |
225 | * A convenience method for returning the configpath for a test. |
226 | * pConfigDirectory is the top level name to be used for the configpath. |
227 | * Returns a file in the "config/data" subdirectory of the passed in file. |
228 | * |
229 | * @param pBaseConfigDirectory the base configuration directory. |
230 | * @return The calculated configuration path. |
231 | */ |
232 | public static File getConfigpath(String pBaseConfigDirectory) { |
233 | return getConfigpath(NucleusTestUtils.class, pBaseConfigDirectory, true); |
234 | } |
235 | |
236 | |
237 | /** |
238 | * A convenience method for returning the configpath for a test. |
239 | * pConfigDirectory is the top level name to be used for the configpath. |
240 | * Returns a file in the "data" subdirectory of the passed in file. |
241 | * |
242 | * @param pBaseConfigDirectory the base configuration directory. |
243 | * @param pCreate whether to create the config/data subdirectory if |
244 | * it does not exist. |
245 | * |
246 | * |
247 | * @return The calculated configuration path. |
248 | */ |
249 | public static File getConfigpath(String pBaseConfigDirectory, boolean pCreate) { |
250 | return getConfigpath(NucleusTestUtils.class, pBaseConfigDirectory, pCreate); |
251 | } |
252 | /** |
253 | * A convenience method for returning the configpath for a test. |
254 | * pConfigDirectory is the top level name to be used for the configpath. |
255 | * Returns a file in the pBaseConfigDirectory (or pBaseConfigDirectory + |
256 | * "data") subdirectory of the the passed in class's location. |
257 | * <P> |
258 | * The directory location is calculated as (in psuedocode): <code> |
259 | * (pClassRelativeTo's package location) + "/" + (pConfigDirectory or "data") + "/config" |
260 | * </code> |
261 | * This method always creates the config/data subdirectory if it does not |
262 | * exist. |
263 | * |
264 | * @param pClassRelativeTo |
265 | * the class whose package the config/data (or |
266 | * pBaseConfigDirectory/data) should be relative in. |
267 | * @param pBaseConfigDirectory |
268 | * the base configuration directory If null, uses "config". |
269 | * @return The calculated configuration path. |
270 | */ |
271 | |
272 | public static File getConfigpath(Class pClassRelativeTo, |
273 | String pBaseConfigDirectory) { |
274 | return getConfigpath(pClassRelativeTo, pBaseConfigDirectory, true); |
275 | } |
276 | |
277 | |
278 | |
279 | /** |
280 | * A convenience method for returning the configpath for a test. |
281 | * pConfigDirectory is the top level name to be used for the configpath. |
282 | * Returns a file in the pBaseConfigDirectory (or pBaseConfigDirectory + |
283 | * "data") subdirectory of the the passed in class's location.<P> |
284 | * |
285 | * The directory location is calculated as (in psuedocode): |
286 | * <code> |
287 | * (pClassRelativeTo's package location) + "/" + (pConfigDirectory or "data") + "/config" |
288 | * </code> |
289 | * |
290 | * |
291 | * @param pClassRelativeTo the class whose package the config/data |
292 | * (or pBaseConfigDirectory/data) should be relative in. |
293 | * @param pBaseConfigDirectory the base configuration directory If null, |
294 | * uses "config". |
295 | * @param pCreate whether to create the config/data subdirectory if |
296 | * it does not exist. |
297 | * |
298 | * |
299 | * @return The calculated configuration path. |
300 | */ |
301 | public static File getConfigpath(Class pClassRelativeTo, String pBaseConfigDirectory, boolean pCreate) { |
302 | //System.out.println("getConfigpath(" + |
303 | // pClassRelativeTo + ", " + |
304 | // pBaseConfigDirectory + "," + pCreate + ")"); |
305 | Map<String, File> baseConfigToFile = sConfigDir.get(pClassRelativeTo); |
306 | if (baseConfigToFile == null) { |
307 | baseConfigToFile = new HashMap<String, File>(); |
308 | sConfigDir.put(pClassRelativeTo, baseConfigToFile); |
309 | } |
310 | |
311 | File fileFound = baseConfigToFile.get(pBaseConfigDirectory); |
312 | |
313 | if (fileFound == null) { |
314 | String configdirname = "config"; |
315 | String packageName = StringUtils.replace(pClassRelativeTo.getPackage() |
316 | .getName(), '.', "/"); |
317 | if (pBaseConfigDirectory != null) |
318 | configdirname = pBaseConfigDirectory; |
319 | |
320 | String configFolder = packageName + "/data/" + configdirname; |
321 | URL dataURL = pClassRelativeTo.getClassLoader().getResource(configFolder); |
322 | |
323 | // Mkdir |
324 | if (dataURL == null) { |
325 | URL root = pClassRelativeTo.getClassLoader().getResource(packageName); |
326 | |
327 | File f = new File(root.getFile()); |
328 | File f2 = new File(f, "/data/" + configdirname); |
329 | if (pCreate) { |
330 | f2.mkdirs(); |
331 | } |
332 | dataURL = NucleusTestUtils.class.getClassLoader().getResource(configFolder); |
333 | if (dataURL == null) { |
334 | System.err.println("Warning: Could not find resource \"" + |
335 | configFolder + "\" in CLASSPATH"); |
336 | } |
337 | } |
338 | if (dataURL != null) {// check if this URL is contained within a jar file |
339 | // if so, extract to a temp dir, otherwise just return |
340 | // the directory |
341 | fileFound = extractJarDataURL(dataURL); |
342 | baseConfigToFile.put(pBaseConfigDirectory ,fileFound); |
343 | } |
344 | } |
345 | if (fileFound != null) |
346 | System.setProperty("atg.configpath", |
347 | fileFound.getAbsolutePath()); |
348 | return fileFound; |
349 | } |
350 | |
351 | |
352 | // ------------------------------------ |
353 | /** |
354 | * This method is used to extract a configdir from a jar archive. |
355 | * Given a URL this method will extract the jar contents to a temp dir and return that path. |
356 | * It also adds a shutdown hook to cleanup the tmp dir on normal jvm completion. |
357 | * If the given URL does not appear to be a path into a jar archive, this method returns |
358 | * a new File object initialied with <code>dataURL.getFile()</code>. |
359 | * @ param A URL referring to an archive path contained within a jar. Ex: jar:file:foo.jar!/atg/data/config |
360 | * @return A temporary directory to be used as a configdir |
361 | */ |
362 | private static File extractJarDataURL(URL dataURL) { |
363 | // TODO: Extract to a temp location |
364 | // atg.core.util.JarUtils.extractEntry(arg0, arg1, arg2) |
365 | int endIndex = dataURL.getFile().lastIndexOf('!'); |
366 | if (endIndex == -1) { |
367 | // Not a jar file url |
368 | return new File(dataURL.getFile()); |
369 | } |
370 | log.info(EXTRACT_TEMP_JAR_FILE_FOR_PATH + dataURL.getFile()); |
371 | File configDir = null; |
372 | try { |
373 | File tmpFile = File.createTempFile("atg-dust" + System.currentTimeMillis() ,".tmp"); |
374 | tmpFile.deleteOnExit(); |
375 | final File tmpDir = new File(tmpFile.getParentFile(), ATG_DUST_CONFIG |
376 | + System.currentTimeMillis()); |
377 | |
378 | String jarPath = dataURL.getFile().substring(0, endIndex); |
379 | // Strip leading file: |
380 | int fileColonIndex = jarPath.indexOf(FILE) + FILE.length(); |
381 | jarPath = jarPath.substring(fileColonIndex, jarPath.length()); |
382 | JarUtils.unJar(new File(jarPath), tmpDir, false); |
383 | // Now get the configpath dir relative to this temp dir |
384 | String relativePath = dataURL.getFile().substring(endIndex + 1, |
385 | dataURL.getFile().length()); |
386 | // Add a shutdown hook to delete this temp directory |
387 | FileUtil.deleteDirectoryOnShutdown(tmpDir); |
388 | configDir = new File(tmpDir, relativePath); |
389 | } catch (IOException e) { |
390 | e.printStackTrace(); |
391 | } |
392 | return configDir; |
393 | } |
394 | |
395 | /** |
396 | * Look up the global testconfig path. |
397 | * This path is specified in either an |
398 | * @return |
399 | */ |
400 | public static String getGlobalTestConfig() { |
401 | // First Check System Property |
402 | String config = System.getProperty(ATG_DUST_TESTCONFIG); |
403 | // If NULL check environment variable |
404 | if (config == null) |
405 | config = System.getenv(ATG_DUST_TESTCONFIG_ENV); |
406 | // If that's null, there is no global test config specified |
407 | return config; |
408 | } |
409 | |
410 | /** |
411 | * This method starts nucleus with a config path calculated from the |
412 | * specified list of Dynamo modules ("DAS", "DPS", "DSS", |
413 | * "Publishing.base", etc). Additionally adds a directory calculated relative |
414 | * to the location of pClassRelativeTo's package name from the classloader. |
415 | * The added config directory is calculated as (in psuedocode): |
416 | * <code> |
417 | * (pClassRelativeTo's package location) + "/data/" + (pClassRelativeTo's simpleClassName) + "/config" |
418 | * </code> |
419 | * and is only added if the directory exists. <P> |
420 | * |
421 | * You must specify a <code>pInitialService</code> parameter, which |
422 | * will be the initial service started by Nucleus (rather than the |
423 | * normally Initial component, which would do a full Nucleus component |
424 | * start-up). <P> |
425 | * |
426 | * This method also creates a temporary server directory, which is deleted |
427 | * when shutdownNucleus in invoked on the returned directory. <P> |
428 | * |
429 | * Note: If you need to start up a complete ATG instance, you should |
430 | * use DUST rather than a unit test. <P> |
431 | * |
432 | * Note: You may also wish to use a {@see |
433 | * atg.nucleus.ConfigCreationFilter}. You can either set a value for |
434 | * Nucleus.CREATION_FILTER_CLASS_PROPERTY_NAME |
435 | * ("atg.nucleus.Nucleus.creationFilterClass") as a DynamoEnv or System |
436 | * property, or set the creationFilter property in Nucleus.properties in |
437 | * your configuration. This allows on to block creation of referenced |
438 | * components without having to make additional properties file changes. |
439 | * |
440 | * Note 3: Nucleus's classReplacementMap can also be useful for replacing |
441 | * a component instance with a subclass. |
442 | * |
443 | * @param pModules the list of modules to use to calculate the |
444 | * Nucleus configuration path. |
445 | * @param pClassRelativeTo the class whose name and package |
446 | * will be used for the {packageName}/config/{ClassName}/data directory |
447 | * @param pInitialService the nucleus path of the Nucleus component |
448 | * to start-up. This is a required property to prevent accidental |
449 | * full start-up. |
450 | * @return the started Nucleus instance that should later be shut down |
451 | * with the shutdownNucleus method. |
452 | * @exception ServletException if an error occurs |
453 | */ |
454 | public static Nucleus startNucleusWithModules( |
455 | String[] pModules, Class pClassRelativeTo, |
456 | String pInitialService) throws ServletException { |
457 | return startNucleusWithModules( |
458 | new NucleusStartupOptions( |
459 | pModules, pClassRelativeTo, |
460 | pClassRelativeTo.getSimpleName() + "/config", |
461 | pInitialService)); |
462 | } |
463 | |
464 | |
465 | /** |
466 | * This method starts nucleus with a config path calculated from the |
467 | * specified list of Dynamo modules ("DAS", "DPS", "DSS", |
468 | * "Publishing.base", etc). Additionally adds a directory calculated relative |
469 | * to the location of pClassRelativeTo's package name from the classloader. |
470 | * The added config directory is calculated as (in psuedocode): |
471 | * <code> |
472 | * (pClassRelativeTo's package location) + "/data/" + (pBaseConfigDirectory or "config") |
473 | * </code> |
474 | * and is only added if the directory exists. <P> |
475 | * |
476 | * You must specify a <code>pInitialService</code> parameter, which |
477 | * will be the initial service started by Nucleus (rather than the |
478 | * normally Initial component, which would do a full Nucleus component |
479 | * start-up). <P> |
480 | * |
481 | * This method also creates a temporary server directory, which is deleted |
482 | * when shutdownNucleus in invoked on the returned directory. <P> |
483 | * |
484 | * Note: If you need to start up a complete ATG instance, you should |
485 | * use DUST rather than a unit test. <P> |
486 | * |
487 | * Note: You may also wish to use a {@see |
488 | * atg.nucleus.ConfigCreationFilter}. You can either set a value for |
489 | * Nucleus.CREATION_FILTER_CLASS_PROPERTY_NAME |
490 | * ("atg.nucleus.Nucleus.creationFilterClass") as a DynamoEnv or System |
491 | * property, or set the creationFilter property in Nucleus.properties in |
492 | * your configuration. This allows on to block creation of referenced |
493 | * components without having to make additional properties file changes. |
494 | * |
495 | * Note 3: Nucleus's classReplacementMap can also be useful for replacing |
496 | * a component instance with a subclass. |
497 | * |
498 | * @param pModules the list of modules to use to calculate the |
499 | * Nucleus configuration path. |
500 | * @param pClassRelativeTo the class whose package the config/data |
501 | * (or pBaseConfigDirectory/data) should be relative in. |
502 | * @param pBaseConfigDirectory the base configuration directory. If |
503 | * this parameter is non-null, the relative configuration subdirectory will be |
504 | * ("data/" + pBaseConfigDirectory) rather than "data/config". |
505 | * @param pInitialService the nucleus path of the Nucleus component |
506 | * to start-up. This is a required property to prevent accidental |
507 | * full start-up. |
508 | * @return the started Nucleus instance that should later be shut down |
509 | * with the shutdownNucleus method. |
510 | * @exception ServletException if an error occurs |
511 | */ |
512 | public static Nucleus startNucleusWithModules( |
513 | String[] pModules, Class pClassRelativeTo, String pBaseConfigDirectory, |
514 | String pInitialService) throws ServletException { |
515 | return startNucleusWithModules( |
516 | new NucleusStartupOptions(pModules, pClassRelativeTo, pBaseConfigDirectory, |
517 | pInitialService)); |
518 | } |
519 | |
520 | |
521 | /** |
522 | * This method starts nucleus with a config path calculated from the |
523 | * specified list of Dynamo modules ("DAS", "DPS", "DSS", |
524 | * "Publishing.base", etc). Additionally adds a directory calculated relative |
525 | * to the location of pClassRelativeTo's package name from the classloader. |
526 | * The added config directory is calculated as (in psuedocode): |
527 | * <code> |
528 | * (pClassRelativeTo's package location) + "/data/" + (pBaseConfigDirectory or "config") |
529 | * </code> |
530 | * and is only added if the directory exists. <P> |
531 | * |
532 | * You must specify a <code>pInitialService</code> parameter, which |
533 | * will be the initial service started by Nucleus (rather than the |
534 | * normally Initial component, which would do a full Nucleus component |
535 | * start-up). <P> |
536 | * |
537 | * This method also creates a temporary server directory, which is deleted |
538 | * when shutdownNucleus in invoked on the returned directory. <P> |
539 | * |
540 | * Note: If you need to start up a complete ATG instance, you should |
541 | * use DUST rather than a unit test. <P> |
542 | * |
543 | * Note: You may also wish to use a {@see |
544 | * atg.nucleus.ConfigCreationFilter}. You can either set a value for |
545 | * Nucleus.CREATION_FILTER_CLASS_PROPERTY_NAME |
546 | * ("atg.nucleus.Nucleus.creationFilterClass") as a DynamoEnv or System |
547 | * property, or set the creationFilter property in Nucleus.properties in |
548 | * your configuration. This allows on to block creation of referenced |
549 | * components without having to make additional properties file changes. |
550 | * |
551 | * Note 3: Nucleus's classReplacementMap can also be useful for replacing |
552 | * a component instance with a subclass. |
553 | * |
554 | * See NucleusStartupOptions for the effects of individual properties |
555 | * of pOptions. |
556 | * |
557 | * |
558 | * @param pOptions the startup Options for Nucleus. |
559 | * |
560 | * @return the started Nucleus instance that should later be shut down |
561 | * with the shutdownNucleus method. |
562 | * @exception ServletException if an error occurs |
563 | */ |
564 | public static Nucleus startNucleusWithModules( |
565 | NucleusStartupOptions pOptions) throws ServletException { |
566 | |
567 | if (pOptions.getInitialService() == null) { |
568 | throw new IllegalArgumentException("Initial service must be specified."); |
569 | } |
570 | |
571 | // now let's try to find dynamo home... |
572 | String dynamoRootStr = findDynamoRoot(); |
573 | |
574 | if (dynamoRootStr == null) { |
575 | throw new ServletException("Could not find dynamo root."); |
576 | } |
577 | |
578 | if (!new File(dynamoRootStr).exists()) { |
579 | throw new ServletException( |
580 | "Could not find dynamo root at " + |
581 | dynamoRootStr + " because directory does not exist."); |
582 | } |
583 | |
584 | if (DynamoEnv.getProperty("atg.dynamo.root") == null) { |
585 | // make sure root is set as a property |
586 | DynamoEnv.setProperty("atg.dynamo.root", dynamoRootStr); |
587 | } |
588 | |
589 | if (DynamoEnv.getProperty("atg.dynamo.home") == null) { |
590 | // make sure home is set as a property |
591 | DynamoEnv.setProperty("atg.dynamo.home", |
592 | dynamoRootStr + File.separator + "home"); |
593 | } |
594 | |
595 | |
596 | |
597 | File dynamoRootFile = new File(dynamoRootStr); |
598 | |
599 | String strModulesString = StringUtils.joinStringsWithQuoting( |
600 | pOptions.getModules(), File.pathSeparatorChar); |
601 | |
602 | DynamoEnv.setProperty("atg.dynamo.modules", strModulesString); |
603 | |
604 | |
605 | // our temporary server directory. |
606 | File fileServerDir = null; |
607 | |
608 | try { |
609 | |
610 | AppModuleManager modMgr = new MultiInstallLocalAppModuleManager( |
611 | dynamoRootStr, dynamoRootFile, |
612 | strModulesString); |
613 | |
614 | AppLauncher launcher = AppLauncher.getLauncher(modMgr, strModulesString); |
615 | |
616 | // Start Nucleus |
617 | String configpath = DynamoServerLauncher.calculateConfigPath( |
618 | launcher, pOptions.getLiveconfig(), |
619 | pOptions.getLayersAsString(), false, null); |
620 | |
621 | // use the NucleusTestUtils config dir as a base, since it |
622 | // empties out license checks, etc. |
623 | File fileBaseConfig = getConfigpath(NucleusTestUtils.class, null, false); |
624 | |
625 | if ((fileBaseConfig != null) && fileBaseConfig.exists()) { |
626 | configpath = configpath + File.pathSeparator + |
627 | fileBaseConfig.getAbsolutePath(); |
628 | } |
629 | |
630 | |
631 | // add the additional config path as the last arg, if needed |
632 | File fileTestConfig = getConfigpath( |
633 | pOptions.getClassRelativeTo(), |
634 | pOptions.getBaseConfigDirectory(), false); |
635 | |
636 | // now add it to the end of our config path |
637 | if ((fileTestConfig != null) && fileTestConfig.exists()) { |
638 | configpath = configpath + File.pathSeparator + |
639 | fileTestConfig.getAbsolutePath(); |
640 | } |
641 | else if (fileTestConfig != null) { |
642 | System.err.println("Warning: did not find directory " + |
643 | fileTestConfig.getAbsolutePath()); |
644 | } |
645 | |
646 | // finally, create a server dir. |
647 | fileServerDir = createTempServerDir(); |
648 | |
649 | System.setProperty("atg.dynamo.server.home", |
650 | fileServerDir.getAbsolutePath()); |
651 | System.setProperty("atg.dynamo.license.read", "true"); |
652 | System.setProperty("atg.license.read", "true"); |
653 | NucleusServlet.addNamingFactoriesAndProtocolHandlers(); |
654 | |
655 | ArrayList<String> listArgs = new ArrayList<String>(); |
656 | listArgs.add(configpath); |
657 | listArgs.add("-initialService"); |
658 | listArgs.add(pOptions.getInitialService()); |
659 | |
660 | PropertyEditors.registerEditors(); |
661 | System.out.println("Starting nucleus with arguments: " + listArgs); |
662 | Nucleus n = Nucleus.startNucleus(listArgs.toArray(new String[0])); |
663 | |
664 | // remember our temporary server directory for later deletion |
665 | sNucleusToTempAtgServerDirectory.put(n, fileServerDir); |
666 | // clear out the variable, so our finally clause knows not to |
667 | // delete it |
668 | fileServerDir = null; |
669 | |
670 | return n; |
671 | } catch (AppLauncherException e) { |
672 | throw new ServletException(e); |
673 | } |
674 | catch (IOException e) { |
675 | throw new ServletException(e); |
676 | } |
677 | finally { |
678 | if ((fileServerDir != null) && sRemoveTempAtgServerDirectories) { |
679 | try { |
680 | // a non-null value means it was created, but not added to our list, |
681 | // so we should nuke it. |
682 | FileUtils.deleteDir(fileServerDir.getAbsolutePath()); |
683 | } catch (IOException e) { |
684 | // we shouldn't rethrow here, since we might block |
685 | // the exception in the main clause, so we'll do the bad |
686 | // thing and print the stack trace and swallow the exception |
687 | e.printStackTrace(); |
688 | } |
689 | } |
690 | } |
691 | } |
692 | |
693 | /** |
694 | * Shutdown the specified Nucleus and try to delete the associated |
695 | * temporary server directory. Typically used on a Nucleus created |
696 | * by startNucleusWithModules. |
697 | * @param pNucleus the nucleus instance to shut down. |
698 | * @exception ServiceException if an error occurs |
699 | * @exception IOException if an error occurs (such as a failure |
700 | * to remove the temporary server directory). |
701 | */ |
702 | public static void shutdownNucleus(Nucleus pNucleus) throws ServiceException, IOException { |
703 | boolean bComplete = false; |
704 | try { |
705 | if (pNucleus.isRunning()) { |
706 | pNucleus.stopService(); |
707 | } |
708 | bComplete = true; |
709 | } finally { |
710 | File fileTempAtgServDirectory = |
711 | sNucleusToTempAtgServerDirectory.get(pNucleus); |
712 | |
713 | // try to delete the temp server directory. |
714 | if (sRemoveTempAtgServerDirectories && |
715 | (fileTempAtgServDirectory != null) && |
716 | fileTempAtgServDirectory.exists()) { |
717 | try { |
718 | FileUtils.deleteDir(fileTempAtgServDirectory.getAbsolutePath()); |
719 | } catch (IOException e) { |
720 | if (bComplete) { |
721 | // only throw if we if we finished our try clause, because |
722 | // otherwise we might block our initial exception |
723 | throw e; |
724 | } |
725 | } finally { |
726 | sNucleusToTempAtgServerDirectory.remove(pNucleus); |
727 | } |
728 | } |
729 | } |
730 | } |
731 | |
732 | /** |
733 | * Create a temporary, empty server directory. This is to satisfy |
734 | * Dynamo's need to have a server directory, yet not conflict if |
735 | * multiple tests are running at the same time against the same Dynamo |
736 | * instance. The directory name is generated by File.createTempFile. |
737 | * @return the created temporary server directory. |
738 | * @exception IOException if an error occurs |
739 | */ |
740 | protected static File createTempServerDir() throws IOException { |
741 | File fileTemp = File.createTempFile("tempServer", "dir"); |
742 | fileTemp.delete(); |
743 | if (!fileTemp.mkdir()) { |
744 | throw new IOException("Unable to create directory " + |
745 | fileTemp.getAbsolutePath()); |
746 | } |
747 | for (String strSubDir : ServerConfig.smConfigFileDirs) { |
748 | File fileSubDir = new File(fileTemp, strSubDir); |
749 | if (!fileSubDir.mkdirs()) { |
750 | throw new IOException("Unable to create directory " + |
751 | fileSubDir.getAbsolutePath()); |
752 | } |
753 | } |
754 | return fileTemp; |
755 | } |
756 | |
757 | |
758 | /** A crazily ugly and elaborate method where we try to discover |
759 | * DYNAMO_ROOT by various means. This is mostly made complicated |
760 | * by the ROAD DUST environment being so different from devtools. |
761 | */ |
762 | static String findDynamoRoot() { |
763 | // now let's try to find dynamo home... |
764 | String dynamoRootStr = DynamoEnv.getProperty("atg.dynamo.root"); |
765 | |
766 | |
767 | if (dynamoRootStr == null) { |
768 | // let's try to look at an environment variable, just to |
769 | // see.... |
770 | dynamoRootStr = |
771 | CommandProcessor.getProcEnvironmentVar("DYNAMO_ROOT"); |
772 | } |
773 | |
774 | if (dynamoRootStr == null) { |
775 | // try dynamo home |
776 | String dynamoHomeStr = DynamoEnv.getProperty("atg.dynamo.home"); |
777 | if (StringUtils.isEmpty(dynamoHomeStr)) { |
778 | dynamoHomeStr = null; |
779 | } |
780 | |
781 | if (dynamoHomeStr == null) { |
782 | dynamoHomeStr = |
783 | CommandProcessor.getProcEnvironmentVar("DYNAMO_HOME"); |
784 | |
785 | if (StringUtils.isEmpty(dynamoHomeStr)) { |
786 | dynamoHomeStr = null; |
787 | } |
788 | |
789 | if (dynamoHomeStr != null) { |
790 | // make sure home is set as a property |
791 | DynamoEnv.setProperty("atg.dynamo.home", dynamoHomeStr); |
792 | } |
793 | } |
794 | |
795 | if (dynamoHomeStr != null) { |
796 | dynamoRootStr = dynamoHomeStr.trim() + File.separator + ".."; |
797 | } |
798 | } |
799 | |
800 | if (dynamoRootStr == null) { |
801 | // okay, start searching upwards for something that looks like |
802 | // a dynamo directory, which should be the case for devtools |
803 | File currentDir = new File(new File(".").getAbsolutePath()); |
804 | String strDynamoHomeLocalConfig = "Dynamo" + File.separator + "home" + File.separator + "localconfig"; |
805 | |
806 | while (currentDir != null) { |
807 | File filePotentialHomeLocalconfigDir = new File(currentDir, |
808 | strDynamoHomeLocalConfig); |
809 | if (filePotentialHomeLocalconfigDir.exists()) { |
810 | dynamoRootStr = new File(currentDir, "Dynamo").getAbsolutePath(); |
811 | System.out.println("Found dynamo root via parent directory: " + dynamoRootStr); |
812 | break; |
813 | } |
814 | currentDir = currentDir.getParentFile(); |
815 | } |
816 | } |
817 | |
818 | if (dynamoRootStr == null) { |
819 | // okay, we are not devtools-ish, so let's try using our ClassLoader |
820 | // to figure things out. |
821 | |
822 | URL urlClass = |
823 | NucleusTestUtils.class.getClassLoader().getResource("atg/nucleus/Nucleus.class"); |
824 | |
825 | // okay... this should be jar URL... |
826 | if ((urlClass != null) && "jar".equals(urlClass.getProtocol())) { |
827 | String strFile = urlClass.getFile(); |
828 | int separator = strFile.indexOf('!'); |
829 | strFile = strFile.substring(0, separator); |
830 | |
831 | File fileCur = null; |
832 | try { |
833 | fileCur = urlToFile(new URL(strFile)); |
834 | } catch (MalformedURLException e) { |
835 | // ignore |
836 | } |
837 | |
838 | if (fileCur != null) { |
839 | String strSubPath = |
840 | "DAS/taglib/dspjspTaglib/1.0".replace('/', File.separatorChar); |
841 | while ((fileCur != null) && fileCur.exists()) { |
842 | if (new File(fileCur, strSubPath).exists()) { |
843 | dynamoRootStr = fileCur.getAbsolutePath(); |
844 | System.out.println("Found dynamo root by Nucleus.class location: " + |
845 | dynamoRootStr); |
846 | |
847 | |
848 | break; |
849 | } |
850 | fileCur = fileCur.getParentFile(); |
851 | } |
852 | } |
853 | } |
854 | } |
855 | |
856 | return dynamoRootStr; |
857 | } |
858 | |
859 | /** Try to convert a file URL to a File object. You'd think this would be |
860 | * easier, but no. */ |
861 | static File urlToFile(URL url) { |
862 | URI uri; |
863 | |
864 | if (!"file".equals(url.getProtocol())) { |
865 | throw new IllegalArgumentException("URL must be a file URL, got " + url); |
866 | } |
867 | |
868 | try { |
869 | // this is the step that can fail, and so |
870 | // it should be this step that should be fixed |
871 | uri = url.toURI(); |
872 | } catch (URISyntaxException e) { |
873 | // OK if we are here, then obviously the URL did |
874 | // not comply with RFC 2396. This can only |
875 | // happen if we have illegal unescaped characters. |
876 | // If we have one unescaped character, then |
877 | // the only automated fix we can apply, is to assume |
878 | // all characters are unescaped. |
879 | // If we want to construct a URI from unescaped |
880 | // characters, then we have to use the component |
881 | // constructors: |
882 | try { |
883 | uri = new URI(url.getProtocol(), url.getUserInfo(), url |
884 | .getHost(), url.getPort(), url.getPath(), url |
885 | .getQuery(), url.getRef()); |
886 | } catch (URISyntaxException e1) { |
887 | // The URL is broken beyond automatic repair |
888 | throw new IllegalArgumentException("broken URL: " + url); |
889 | } |
890 | } |
891 | return new File(uri); |
892 | } |
893 | |
894 | /** |
895 | * This method returns a free port number on the current machine. There is |
896 | * some chance that the port number could be taken by the time the caller |
897 | * actually gets around to using it. |
898 | * |
899 | * This method returns -9999 if it's not able to find a port. |
900 | */ |
901 | public static int findFreePort() { |
902 | ServerSocket socket = null; |
903 | int freePort = -9999; |
904 | try { |
905 | socket = new ServerSocket(0); |
906 | freePort = socket.getLocalPort(); |
907 | } catch (IOException e) { |
908 | ; |
909 | } finally { |
910 | try { |
911 | socket.close(); |
912 | } catch (IOException e) { |
913 | ; |
914 | } |
915 | } |
916 | return freePort; |
917 | } |
918 | |
919 | //------------------------------------------------------- |
920 | /** A class representing NucleusStartupOptions, used by |
921 | * startNucleusWithModules(). |
922 | */ |
923 | public static class NucleusStartupOptions { |
924 | |
925 | /** List of dynamo modules. */ |
926 | private String[] mModules; |
927 | /** Class whose package data subdir is relative to. */ |
928 | private Class mClassRelativeTo; |
929 | /** The base config directory, realtive to mClassRelativeTo's package |
930 | * + "/data". If null, then "config" |
931 | */ |
932 | private String mBaseConfigDirectory; |
933 | |
934 | /** The Nucleus path of the intial service to resolve. */ |
935 | private String mInitialService; |
936 | |
937 | |
938 | /** |
939 | * This constructor creates NucleusStartupOptions with the |
940 | * specified list of Dynamo modules ("DAS", "DPS", "DSS", |
941 | * "Publishing.base", etc). |
942 | * Additionally sets opts to add a directory calculated relative |
943 | * to the location of pClassRelativeTo's package name from the classloader. |
944 | * The added config directory is calculated as (in psuedocode): |
945 | * <code> |
946 | * (pClassRelativeTo's package location) + "/data/" + (pClassRelativeTo's simpleClassName) + "/config" |
947 | * </code> |
948 | * and is only added if the directory exists. <P> |
949 | * |
950 | * You must specify a <code>pInitialService</code> parameter, which |
951 | * will be the initial service started by Nucleus (rather than the |
952 | * normally Initial component, which would do a full Nucleus component |
953 | * start-up). <P> |
954 | * |
955 | * |
956 | * @param pModules the list of modules to use to calculate the |
957 | * Nucleus configuration path. |
958 | * @param pClassRelativeTo the class whose name and package |
959 | * will be used for the {packageName}/config/{simpleClassName}/data directory |
960 | * @param pInitialService the nucleus path of the Nucleus component |
961 | * to start-up. This is a required property to prevent accidental |
962 | * full start-up. |
963 | * @return the started Nucleus instance that should later be shut down |
964 | * with the shutdownNucleus method. |
965 | * @exception ServletException if an error occurs |
966 | */ |
967 | public NucleusStartupOptions(String[] pModules, Class pClassRelativeTo, |
968 | String pInitialService) { |
969 | |
970 | |
971 | mModules = pModules; |
972 | mClassRelativeTo = pClassRelativeTo; |
973 | mInitialService = pInitialService; |
974 | mBaseConfigDirectory = pClassRelativeTo.getSimpleName() + "/config"; |
975 | } |
976 | |
977 | |
978 | /** |
979 | * This constructor creates NucleusStartupOptions with the |
980 | * specified list of Dynamo modules ("DAS", "DPS", "DSS", |
981 | * "Publishing.base", etc). |
982 | * Additionally sets opts to add a directory calculated relative |
983 | * to the location of pClassRelativeTo's package name from the classloader. |
984 | * The added config directory is calculated as (in psuedocode): |
985 | * <code> |
986 | * (pClassRelativeTo's package location) + "/" + (pConfigDirectory or "data") + "/config" |
987 | * </code> |
988 | * and is only added if the directory exists. <P> |
989 | * |
990 | * You must specify a <code>pInitialService</code> parameter, which |
991 | * will be the initial service started by Nucleus (rather than the |
992 | * normally Initial component, which would do a full Nucleus component |
993 | * start-up). <P> |
994 | * |
995 | * |
996 | * @param pModules the list of modules to use to calculate the |
997 | * Nucleus configuration path. |
998 | * @param pClassRelativeTo the class whose package the config/data |
999 | * (or pBaseConfigDirectory/data) should be relative in. |
1000 | * @param pBaseConfigDirectory the base configuration directory. If |
1001 | * this parameter is non-null, the relative configuration subdirectory will be |
1002 | * ("data/" + pBaseConfigDirectory) rather than "data/config". |
1003 | * @param pInitialService the nucleus path of the Nucleus component |
1004 | * to start-up. This is a required property to prevent accidental |
1005 | * full start-up. |
1006 | * @return the started Nucleus instance that should later be shut down |
1007 | * with the shutdownNucleus method. |
1008 | * @exception ServletException if an error occurs |
1009 | */ |
1010 | public NucleusStartupOptions(String[] pModules, Class pClassRelativeTo, |
1011 | String pBaseConfigDirectory, |
1012 | String pInitialService) { |
1013 | |
1014 | mModules = pModules; |
1015 | mClassRelativeTo = pClassRelativeTo; |
1016 | mInitialService = pInitialService; |
1017 | mBaseConfigDirectory = pBaseConfigDirectory; |
1018 | } |
1019 | |
1020 | /** Return the list of modules for starting Nucleus. These modules |
1021 | * are the modules whose config path will be included. */ |
1022 | public String[] getModules() { |
1023 | return mModules; |
1024 | } |
1025 | |
1026 | /** Return the "class relative to" property. This is the Class whose |
1027 | * package the config directory will be relative to. */ |
1028 | public Class getClassRelativeTo() { |
1029 | return mClassRelativeTo; |
1030 | } |
1031 | |
1032 | /** Gets the initialService. This is the InitialService for Nucleus |
1033 | * to resolve at start-up. Required. */ |
1034 | public String getInitialService() { |
1035 | return mInitialService; |
1036 | } |
1037 | |
1038 | //------------------------------------- |
1039 | // property: baseConfigDirectory |
1040 | |
1041 | /** Set the basic config directory. This is the directory that will be |
1042 | * taked on to the package path of the classRelativeTo class. If this |
1043 | * property is non-null, the relative configuration subdirectory will |
1044 | * be ("data/" + baseConfigDirectory). */ |
1045 | public String getBaseConfigDirectory() { |
1046 | return mBaseConfigDirectory; |
1047 | } |
1048 | |
1049 | |
1050 | //------------------------------------- |
1051 | // property: layers |
1052 | |
1053 | private String[] mLayers; |
1054 | |
1055 | /** Sets the Dynamo layers to run with. */ |
1056 | public void setLayers(String[] pLayers) { |
1057 | mLayers = pLayers; |
1058 | } |
1059 | |
1060 | /** Returns the Dynamo layers to run with. */ |
1061 | public String[] getLayers() { |
1062 | return mLayers; |
1063 | } |
1064 | |
1065 | /** Return the layers as a string appropriate for passing to |
1066 | * DynamoServerLauncher, calculateConfigPath. |
1067 | * |
1068 | * @return null if layers is null. Otherwise returns a space delimited |
1069 | * list of layers. |
1070 | */ |
1071 | public String getLayersAsString() { |
1072 | if (mLayers == null) { |
1073 | return null; |
1074 | } |
1075 | StringBuilder strbuf = new StringBuilder(); |
1076 | for (String strCur : mLayers) { |
1077 | if (strbuf.length() != 0) { |
1078 | strbuf.append(" "); |
1079 | } |
1080 | strbuf.append(strCur); |
1081 | } |
1082 | return strbuf.toString(); |
1083 | } |
1084 | |
1085 | //------------------------------------- |
1086 | // property: liveconfig |
1087 | |
1088 | private boolean mLiveconfig; |
1089 | |
1090 | /** Sets property liveconfig. |
1091 | * |
1092 | * @param pLiveconfig true if Nucleus should be started in liveconfig |
1093 | * mode, false otherwise. |
1094 | */ |
1095 | public void setLiveconfig(boolean pLiveconfig) { |
1096 | mLiveconfig = pLiveconfig; |
1097 | } |
1098 | |
1099 | /** Returns property liveconfig. |
1100 | * @return true if liveconfig should be set, false otherwise. |
1101 | */ |
1102 | public boolean getLiveconfig() { |
1103 | return mLiveconfig; |
1104 | } |
1105 | |
1106 | /** Modify Nucleus command-line options, as needed. This will |
1107 | * be invoked just before Nucleus is started as a final |
1108 | * chance to adjust any command-line options. |
1109 | */ |
1110 | public void modifyNucleusCommandLineOptions(List<String> listArgs) { |
1111 | return; |
1112 | } |
1113 | } // end inner-class NucleusStartupOptions |
1114 | |
1115 | } |