View Javadoc

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 }