View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.maven.plugin;
20  
21  import java.io.BufferedOutputStream;
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.io.LineNumberReader;
29  import java.net.MalformedURLException;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Properties;
37  import java.util.Random;
38  import java.util.Set;
39  
40  import org.apache.maven.artifact.Artifact;
41  import org.apache.maven.plugin.AbstractMojo;
42  import org.apache.maven.plugin.MojoExecutionException;
43  import org.apache.maven.plugin.MojoFailureException;
44  import org.apache.maven.plugin.descriptor.PluginDescriptor;
45  import org.apache.maven.project.MavenProject;
46  import org.eclipse.jetty.util.IO;
47  
48  
49  /**
50   * <p>
51   *  This goal is used to assemble your webapp into a war and automatically deploy it to Jetty in a forked JVM.
52   *  </p>
53   *  <p>
54   *  You need to define a jetty.xml file to configure connectors etc and a context xml file that sets up anything special
55   *  about your webapp. This plugin will fill in the:
56   *  <ul>
57   *  <li>context path
58   *  <li>classes
59   *  <li>web.xml
60   *  <li>root of the webapp
61   *  </ul>
62   *  Based on a combination of information that you supply and the location of files in your unassembled webapp.
63   *  </p>
64   *  <p>
65   *  There is a <a href="run-war-mojo.html">reference guide</a> to the configuration parameters for this plugin, and more detailed information
66   *  with examples in the <a href="http://docs.codehaus.org/display/JETTY/Maven+Jetty+Plugin/">Configuration Guide</a>.
67   *  </p>
68   * 
69   * @goal run-forked
70   * @requiresDependencyResolution compile+runtime
71   * @execute phase="test-compile"
72   * @description Runs Jetty in forked JVM on an unassembled webapp
73   *
74   */
75  public class JettyRunForkedMojo extends AbstractMojo
76  {    
77      public String PORT_SYSPROPERTY = "jetty.port";
78      
79      /**
80       * Whether or not to include dependencies on the plugin's classpath with &lt;scope&gt;provided&lt;/scope&gt;
81       * Use WITH CAUTION as you may wind up with duplicate jars/classes.
82       * @parameter  default-value="false"
83       */
84      protected boolean useProvidedScope;
85      
86      
87      /**
88       * The maven project.
89       *
90       * @parameter expression="${project}"
91       * @required
92       * @readonly
93       */
94      private MavenProject project;
95  
96      
97      /**
98       * If true, the &lt;testOutputDirectory&gt;
99       * and the dependencies of &lt;scope&gt;test&lt;scope&gt;
100      * will be put first on the runtime classpath.
101      * @parameter alias="useTestClasspath" default-value="false"
102      */
103     private boolean useTestScope;
104     
105     
106     /**
107      * The default location of the web.xml file. Will be used
108      * if &lt;webAppConfig&gt;&lt;descriptor&gt; is not set.
109      * 
110      * @parameter expression="${basedir}/src/main/webapp/WEB-INF/web.xml"
111      * @readonly
112      */
113     private String webXml;
114     
115     
116     /**
117      * The target directory
118      * 
119      * @parameter expression="${project.build.directory}"
120      * @required
121      * @readonly
122      */
123     protected File target;
124     
125     
126     /**
127      * The temporary directory to use for the webapp.
128      * Defaults to target/tmp
129      *
130      * @parameter expression="${project.build.directory}/tmp"
131      * @required
132      * @readonly
133      */
134     protected File tmpDirectory;
135 
136     
137     /**
138      * The directory containing generated classes.
139      *
140      * @parameter expression="${project.build.outputDirectory}"
141      * @required
142      * 
143      */
144     private File classesDirectory;    
145     
146     
147     /**
148      * The directory containing generated test classes.
149      * 
150      * @parameter expression="${project.build.testOutputDirectory}"
151      * @required
152      */
153     private File testClassesDirectory;
154    
155     
156     /**
157      * Root directory for all html/jsp etc files
158      *
159      * @parameter expression="${basedir}/src/main/webapp"
160      *
161      */
162     private File webAppSourceDirectory;   
163 
164     
165     /**
166      * If true, the webAppSourceDirectory will be first on the list of 
167      * resources that form the resource base for the webapp. If false, 
168      * it will be last.
169      * 
170      * @parameter  default-value="true"
171      */
172     private boolean baseAppFirst;
173     
174 
175     /**
176      * Location of jetty xml configuration files whose contents 
177      * will be applied before any plugin configuration. Optional.
178      * @parameter
179      */
180     private String jettyXml;
181     
182     /**
183      * The context path for the webapp. Defaults to / for jetty-9
184      *
185      * @parameter expression="/"
186      */
187     private String contextPath;
188 
189 
190     /**
191      * Location of a context xml configuration file whose contents
192      * will be applied to the webapp AFTER anything in &lt;webAppConfig&gt;.Optional.
193      * @parameter
194      */
195     private String contextXml;
196 
197     
198     /**  
199      * @parameter expression="${jetty.skip}" default-value="false"
200      */
201     private boolean skip;
202 
203     
204     /**
205      * Port to listen to stop jetty on executing -DSTOP.PORT=&lt;stopPort&gt; 
206      * -DSTOP.KEY=&lt;stopKey&gt; -jar start.jar --stop
207      * @parameter
208      * @required
209      */
210     protected int stopPort;
211  
212     
213     /**
214      * Key to provide when stopping jetty on executing java -DSTOP.KEY=&lt;stopKey&gt; 
215      * -DSTOP.PORT=&lt;stopPort&gt; -jar start.jar --stop
216      * @parameter
217      * @required
218      */
219     protected String stopKey;
220 
221     
222     /**
223      * Arbitrary jvm args to pass to the forked process
224      * @parameter
225      */
226     private String jvmArgs;
227     
228     
229     /**
230      * @parameter expression="${plugin.artifacts}"
231      * @readonly
232      */
233     private List pluginArtifacts;
234     
235     
236     /**
237      * @parameter expression="${plugin}"
238      * @readonly
239      */
240     private PluginDescriptor plugin;
241     
242     
243     /**
244      * @parameter expression="true" default-value="true"
245      */
246     private boolean waitForChild;
247 
248     /**
249      * @parameter default-value="50"
250      */
251     private int maxStartupLines;
252 
253     /**
254      * The forked jetty instance
255      */
256     private Process forkedProcess;
257     
258     
259     /**
260      * Random number generator
261      */
262     private Random random;    
263     
264     
265     
266     
267     
268     
269     /**
270      * ShutdownThread
271      *
272      *
273      */
274     public class ShutdownThread extends Thread
275     {
276         public ShutdownThread()
277         {
278             super("RunForkedShutdown");
279         }
280         
281         public void run ()
282         {
283             if (forkedProcess != null && waitForChild)
284             {
285                 forkedProcess.destroy();
286             }
287         }
288     }
289     
290 
291     
292     
293     /**
294      * ConsoleStreamer
295      * 
296      * Simple streamer for the console output from a Process
297      */
298     private static class ConsoleStreamer implements Runnable
299     {
300         private String mode;
301         private BufferedReader reader;
302 
303         public ConsoleStreamer(String mode, InputStream is)
304         {
305             this.mode = mode;
306             this.reader = new BufferedReader(new InputStreamReader(is));
307         }
308 
309 
310         public void run()
311         {
312             String line;
313             try
314             {
315                 while ((line = reader.readLine()) != (null))
316                 {
317                     System.out.println("[" + mode + "] " + line);
318                 }
319             }
320             catch (IOException ignore)
321             {
322                 /* ignore */
323             }
324             finally
325             {
326                 IO.close(reader);
327             }
328         }
329     }
330     
331     
332     
333     
334     
335     /**
336      * @see org.apache.maven.plugin.Mojo#execute()
337      */
338     public void execute() throws MojoExecutionException, MojoFailureException
339     {
340         getLog().info("Configuring Jetty for project: " + project.getName());
341         if (skip)
342         {
343             getLog().info("Skipping Jetty start: jetty.skip==true");
344             return;
345         }
346         PluginLog.setLog(getLog());
347         Runtime.getRuntime().addShutdownHook(new ShutdownThread());
348         random = new Random();
349         startJettyRunner();
350     }
351     
352     
353     
354     
355     /**
356      * @return
357      * @throws MojoExecutionException
358      */
359     public List<String> getProvidedJars() throws MojoExecutionException
360     {  
361         //if we are configured to include the provided dependencies on the plugin's classpath
362         //(which mimics being on jetty's classpath vs being on the webapp's classpath), we first
363         //try and filter out ones that will clash with jars that are plugin dependencies, then
364         //create a new classloader that we setup in the parent chain.
365         if (useProvidedScope)
366         {
367             
368                 List<String> provided = new ArrayList<String>();        
369                 for ( Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext(); )
370                 {                   
371                     Artifact artifact = iter.next();
372                     if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()) && !isPluginArtifact(artifact))
373                     {
374                         provided.add(artifact.getFile().getAbsolutePath());
375                         if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
376                     }
377                 }
378                 return provided;
379 
380         }
381         else
382             return null;
383     }
384     
385    
386     
387     
388     /**
389      * @return
390      * @throws MojoExecutionException
391      */
392     public File prepareConfiguration() throws MojoExecutionException
393     {
394         try
395         {   
396             //work out the configuration based on what is configured in the pom
397             File propsFile = new File (target, "fork.props");
398             if (propsFile.exists())
399                 propsFile.delete();   
400 
401             propsFile.createNewFile();
402             //propsFile.deleteOnExit();
403 
404             Properties props = new Properties();
405 
406 
407             //web.xml
408             if (webXml != null)
409                 props.put("web.xml", webXml);
410 
411             //sort out the context path
412             if (contextPath != null)
413                 props.put("context.path", contextPath);
414 
415             //sort out the tmp directory (make it if it doesn't exist)
416             if (tmpDirectory != null)
417             {
418                 if (!tmpDirectory.exists())
419                     tmpDirectory.mkdirs();
420                 props.put("tmp.dir", tmpDirectory.getAbsolutePath());
421             }
422 
423             //sort out base dir of webapp
424             if (webAppSourceDirectory != null)
425                 props.put("base.dir", webAppSourceDirectory.getAbsolutePath());
426 
427             //sort out the resource base directories of the webapp
428             StringBuilder builder = new StringBuilder();
429             props.put("base.first", Boolean.toString(baseAppFirst));
430 
431             //web-inf classes
432             List<File> classDirs = getClassesDirs();
433             StringBuffer strbuff = new StringBuffer();
434             for (int i=0; i<classDirs.size(); i++)
435             {
436                 File f = classDirs.get(i);
437                 strbuff.append(f.getAbsolutePath());
438                 if (i < classDirs.size()-1)
439                     strbuff.append(",");
440             }
441 
442             if (classesDirectory != null)
443             {
444                 props.put("classes.dir", classesDirectory.getAbsolutePath());
445             }
446             
447             if (useTestScope && testClassesDirectory != null)
448             {
449                 props.put("testClasses.dir", testClassesDirectory.getAbsolutePath());
450             }
451 
452             //web-inf lib
453             List<File> deps = getDependencyFiles();
454             strbuff.setLength(0);
455             for (int i=0; i<deps.size(); i++)
456             {
457                 File d = deps.get(i);
458                 strbuff.append(d.getAbsolutePath());
459                 if (i < deps.size()-1)
460                     strbuff.append(",");
461             }
462             props.put("lib.jars", strbuff.toString());
463 
464             //any war files
465             List<Artifact> warArtifacts = getWarArtifacts(); 
466             for (int i=0; i<warArtifacts.size(); i++)
467             {
468                 strbuff.setLength(0);           
469                 Artifact a  = warArtifacts.get(i);
470                 strbuff.append(a.getGroupId()+",");
471                 strbuff.append(a.getArtifactId()+",");
472                 strbuff.append(a.getFile().getAbsolutePath());
473                 props.put("maven.war.artifact."+i, strbuff.toString());
474             }
475           
476             
477             //any overlay configuration
478             WarPluginInfo warPlugin = new WarPluginInfo(project);
479             
480             //add in the war plugins default includes and excludes
481             props.put("maven.war.includes", toCSV(warPlugin.getDependentMavenWarIncludes()));
482             props.put("maven.war.excludes", toCSV(warPlugin.getDependentMavenWarExcludes()));
483             
484             
485             List<OverlayConfig> configs = warPlugin.getMavenWarOverlayConfigs();
486             int i=0;
487             for (OverlayConfig c:configs)
488             {
489                 props.put("maven.war.overlay."+(i++), c.toString());
490             }
491             
492             props.store(new BufferedOutputStream(new FileOutputStream(propsFile)), "properties for forked webapp");
493             return propsFile;
494         }
495         catch (Exception e)
496         {
497             throw new MojoExecutionException("Prepare webapp configuration", e);
498         }
499     }
500     
501 
502     
503     
504     /**
505      * @return
506      */
507     private List<File> getClassesDirs ()
508     {
509         List<File> classesDirs = new ArrayList<File>();
510         
511         //if using the test classes, make sure they are first
512         //on the list
513         if (useTestScope && (testClassesDirectory != null))
514             classesDirs.add(testClassesDirectory);
515         
516         if (classesDirectory != null)
517             classesDirs.add(classesDirectory);
518         
519         return classesDirs;
520     }
521   
522     
523   
524     
525     /**
526      * @return
527      * @throws MalformedURLException
528      * @throws IOException
529      */
530     private List<Artifact> getWarArtifacts()
531     throws MalformedURLException, IOException
532     {
533         List<Artifact> warArtifacts = new ArrayList<Artifact>();
534         for ( Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext(); )
535         {
536             Artifact artifact = (Artifact) iter.next();  
537             
538             if (artifact.getType().equals("war"))
539                 warArtifacts.add(artifact);
540         }
541 
542         return warArtifacts;
543     }
544     
545     
546     
547     
548     /**
549      * @return
550      */
551     private List<File> getDependencyFiles ()
552     {
553         List<File> dependencyFiles = new ArrayList<File>();
554     
555         for ( Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext(); )
556         {
557             Artifact artifact = (Artifact) iter.next();
558             
559             if (((!Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) && (!Artifact.SCOPE_TEST.equals( artifact.getScope()))) 
560                     ||
561                 (useTestScope && Artifact.SCOPE_TEST.equals( artifact.getScope())))
562             {
563                 dependencyFiles.add(artifact.getFile());
564                 getLog().debug( "Adding artifact " + artifact.getFile().getName() + " for WEB-INF/lib " );   
565             }
566         }
567         
568         return dependencyFiles; 
569     }
570     
571     
572     
573     
574     /**
575      * @param artifact
576      * @return
577      */
578     public boolean isPluginArtifact(Artifact artifact)
579     {
580         if (pluginArtifacts == null || pluginArtifacts.isEmpty())
581             return false;
582         
583         boolean isPluginArtifact = false;
584         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext() && !isPluginArtifact; )
585         {
586             Artifact pluginArtifact = iter.next();
587             if (getLog().isDebugEnabled()) { getLog().debug("Checking "+pluginArtifact);}
588             if (pluginArtifact.getGroupId().equals(artifact.getGroupId()) && pluginArtifact.getArtifactId().equals(artifact.getArtifactId()))
589                 isPluginArtifact = true;
590         }
591         
592         return isPluginArtifact;
593     }
594     
595     
596     
597     
598     /**
599      * @return
600      * @throws Exception
601      */
602     private Set<Artifact> getExtraJars()
603     throws Exception
604     {
605         Set<Artifact> extraJars = new HashSet<Artifact>();
606   
607         
608         List l = pluginArtifacts;
609         Artifact pluginArtifact = null;
610 
611         if (l != null)
612         {
613             Iterator itor = l.iterator();
614             while (itor.hasNext() && pluginArtifact == null)
615             {              
616                 Artifact a = (Artifact)itor.next();
617                 if (a.getArtifactId().equals(plugin.getArtifactId())) //get the jetty-maven-plugin jar
618                 {
619                     extraJars.add(a);
620                 }
621             }
622         }
623 
624         return extraJars;
625     }
626 
627     
628 
629     
630     /**
631      * @throws MojoExecutionException
632      */
633     public void startJettyRunner() throws MojoExecutionException
634     {      
635         try
636         {
637         
638             File props = prepareConfiguration();
639             
640             List<String> cmd = new ArrayList<String>();
641             cmd.add(getJavaBin());
642             
643             if (jvmArgs != null)
644             {
645                 String[] args = jvmArgs.split(" ");
646                 for (int i=0;args != null && i<args.length;i++)
647                 {
648                     if (args[i] !=null && !"".equals(args[i]))
649                         cmd.add(args[i].trim());
650                 }
651             }
652             
653             String classPath = getClassPath();
654             if (classPath != null && classPath.length() > 0)
655             {
656                 cmd.add("-cp");
657                 cmd.add(classPath);
658             }
659             cmd.add(Starter.class.getCanonicalName());
660             
661             if (stopPort > 0 && stopKey != null)
662             {
663                 cmd.add("--stop-port");
664                 cmd.add(Integer.toString(stopPort));
665                 cmd.add("--stop-key");
666                 cmd.add(stopKey);
667             }
668             if (jettyXml != null)
669             {
670                 cmd.add("--jetty-xml");
671                 cmd.add(jettyXml);
672             }
673         
674             if (contextXml != null)
675             {
676                 cmd.add("--context-xml");
677                 cmd.add(contextXml);
678             }
679             
680             cmd.add("--props");
681             cmd.add(props.getAbsolutePath());
682             
683             String token = createToken();
684             cmd.add("--token");
685             cmd.add(token);
686             
687             ProcessBuilder builder = new ProcessBuilder(cmd);
688             builder.directory(project.getBasedir());
689             
690             if (PluginLog.getLog().isDebugEnabled())
691                 PluginLog.getLog().debug(Arrays.toString(cmd.toArray()));
692             
693             forkedProcess = builder.start();
694             PluginLog.getLog().info("Forked process starting");
695 
696             if (waitForChild)
697             {
698                 startPump("STDOUT",forkedProcess.getInputStream());
699                 startPump("STDERR",forkedProcess.getErrorStream());
700                 int exitcode = forkedProcess.waitFor();            
701                 PluginLog.getLog().info("Forked execution exit: "+exitcode);
702             }
703             else
704             {   //we're not going to be reading the stderr as we're not waiting for the child to finish
705                 forkedProcess.getErrorStream().close();
706 
707                 //wait for the child to be ready before terminating.
708                 //child indicates it has finished starting by printing on stdout the token passed to it
709                 try
710                 {
711                     LineNumberReader reader = new LineNumberReader(new InputStreamReader(forkedProcess.getInputStream()));
712                     String line = "";
713                     int attempts = maxStartupLines; //max lines we'll read trying to get token
714                     while (attempts>0 && line != null)
715                     {
716                         --attempts;
717                         line = reader.readLine();
718                         if (line != null && line.startsWith(token))
719                             break;
720                     }
721 
722                     reader.close();
723 
724                     if (line != null && line.trim().equals(token))
725                         PluginLog.getLog().info("Forked process started.");
726                     else
727                     {
728                         String err = (line == null?"":(line.startsWith(token)?line.substring(token.length()):line));
729                         PluginLog.getLog().info("Forked process startup errors"+(!"".equals(err)?", received: "+err:""));
730                     }
731                 }
732                 catch (Exception e)
733                 {
734                     throw new MojoExecutionException ("Problem determining if forked process is ready: "+e.getMessage());
735                 }
736             }
737         }
738         catch (InterruptedException ex)
739         {
740             if (forkedProcess != null && waitForChild)
741                 forkedProcess.destroy();
742             
743             throw new MojoExecutionException("Failed to start Jetty within time limit");
744         }
745         catch (Exception ex)
746         {
747             if (forkedProcess != null && waitForChild)
748                 forkedProcess.destroy();
749             
750             throw new MojoExecutionException("Failed to create Jetty process", ex);
751         }
752     }
753     
754  
755 
756     
757     /**
758      * @return
759      * @throws Exception
760      */
761     public String getClassPath() throws Exception
762     {
763         StringBuilder classPath = new StringBuilder();
764         for (Object obj : pluginArtifacts)
765         {
766             Artifact artifact = (Artifact) obj;
767             if ("jar".equals(artifact.getType()))
768             {
769                 if (classPath.length() > 0)
770                 {
771                     classPath.append(File.pathSeparator);
772                 }
773                 classPath.append(artifact.getFile().getAbsolutePath());
774 
775             }
776         }
777         
778         //Any jars that we need from the plugin environment (like the ones containing Starter class)
779         Set<Artifact> extraJars = getExtraJars();
780         for (Artifact a:extraJars)
781         { 
782             classPath.append(File.pathSeparator);
783             classPath.append(a.getFile().getAbsolutePath());
784         }
785         
786         
787         //Any jars that we need from the project's dependencies because we're useProvided
788         List<String> providedJars = getProvidedJars();
789         if (providedJars != null && !providedJars.isEmpty())
790         {
791             for (String jar:providedJars)
792             {
793                 classPath.append(File.pathSeparator);
794                 classPath.append(jar);
795                 if (getLog().isDebugEnabled()) getLog().debug("Adding provided jar: "+jar);
796             }
797         }
798         
799         return classPath.toString();
800     }
801 
802     
803 
804     
805     /**
806      * @return
807      */
808     private String getJavaBin()
809     {
810         String javaexes[] = new String[]
811         { "java", "java.exe" };
812 
813         File javaHomeDir = new File(System.getProperty("java.home"));
814         for (String javaexe : javaexes)
815         {
816             File javabin = new File(javaHomeDir,fileSeparators("bin/" + javaexe));
817             if (javabin.exists() && javabin.isFile())
818             {
819                 return javabin.getAbsolutePath();
820             }
821         }
822 
823         return "java";
824     }
825     
826 
827     
828     
829     /**
830      * @param path
831      * @return
832      */
833     public static String fileSeparators(String path)
834     {
835         StringBuilder ret = new StringBuilder();
836         for (char c : path.toCharArray())
837         {
838             if ((c == '/') || (c == '\\'))
839             {
840                 ret.append(File.separatorChar);
841             }
842             else
843             {
844                 ret.append(c);
845             }
846         }
847         return ret.toString();
848     }
849 
850 
851     
852     
853     /**
854      * @param path
855      * @return
856      */
857     public static String pathSeparators(String path)
858     {
859         StringBuilder ret = new StringBuilder();
860         for (char c : path.toCharArray())
861         {
862             if ((c == ',') || (c == ':'))
863             {
864                 ret.append(File.pathSeparatorChar);
865             }
866             else
867             {
868                 ret.append(c);
869             }
870         }
871         return ret.toString();
872     }
873 
874 
875     
876     
877     /**
878      * @return
879      */
880     private String createToken ()
881     {
882         return Long.toString(random.nextLong()^System.currentTimeMillis(), 36).toUpperCase(Locale.ENGLISH);
883     }
884     
885 
886     
887     
888     /**
889      * @param mode
890      * @param inputStream
891      */
892     private void startPump(String mode, InputStream inputStream)
893     {
894         ConsoleStreamer pump = new ConsoleStreamer(mode,inputStream);
895         Thread thread = new Thread(pump,"ConsoleStreamer/" + mode);
896         thread.setDaemon(true);
897         thread.start();
898     }
899 
900 
901     
902     
903     /**
904      * @param strings
905      * @return
906      */
907     private String toCSV (List<String> strings)
908     {
909         if (strings == null)
910             return "";
911         StringBuffer strbuff = new StringBuffer();
912         Iterator<String> itor = strings.iterator();
913         while (itor.hasNext())
914         {
915             strbuff.append(itor.next());
916             if (itor.hasNext())
917                 strbuff.append(",");
918         }
919         return strbuff.toString();
920     }
921 }