001    /*
002    $Id: LoaderConfiguration.java 3103 2005-11-13 16:28:18Z blackdrag $
003    
004    Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005    
006    Redistribution and use of this software and associated documentation
007    ("Software"), with or without modification, are permitted provided
008    that the following conditions are met:
009    
010    1. Redistributions of source code must retain copyright
011       statements and notices.  Redistributions must also contain a
012       copy of this document.
013    
014    2. Redistributions in binary form must reproduce the
015       above copyright notice, this list of conditions and the
016       following disclaimer in the documentation and/or other
017       materials provided with the distribution.
018    
019    3. The name "groovy" must not be used to endorse or promote
020       products derived from this Software without prior written
021       permission of The Codehaus.  For written permission,
022       please contact info@codehaus.org.
023    
024    4. Products derived from this Software may not be called "groovy"
025       nor may "groovy" appear in their names without prior written
026       permission of The Codehaus. "groovy" is a registered
027       trademark of The Codehaus.
028    
029    5. Due credit should be given to The Codehaus -
030       http://groovy.codehaus.org/
031    
032    THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
033    ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
034    NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
035    FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
036    THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
037    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
039    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
040    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
041    STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
042    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
043    OF THE POSSIBILITY OF SUCH DAMAGE.
044    
045    */
046    package org.codehaus.groovy.tools;
047    
048    import java.io.BufferedReader;
049    import java.io.File;
050    import java.io.FilenameFilter;
051    import java.io.IOException;
052    import java.io.InputStream;
053    import java.io.InputStreamReader;
054    import java.net.MalformedURLException;
055    import java.net.URL;
056    import java.util.ArrayList;
057    
058    /**
059     * class used to configure a RootLoader from a stream or by using 
060     * it's methods.
061     * 
062     * The stream can be for example a FileInputStream from a file with
063     * the following format:
064     * 
065     * # comment
066     * main is classname
067     * load path
068     * load file
069     * load pathWith${property}
070     * load path/*.jar
071     *
072     *<ul>
073     * <li>All lines starting with "#" are ignored.</li> 
074     * <li>The "main is" part may only be once in the file. The String
075     * afterwards is the name of a class if a main method. </li>
076     * <li>The "load" command will add the given file or path to the 
077     * classpath in this configuration object.
078     * </li>
079     *</ul>
080     * 
081     * Defining the main class is optional if @see #setRequireMain(boolean) was 
082     * called with false, before reading the configuration. 
083     * You can use the wildcard "*" to filter the path, but only for files, not
084     * directories. The  ${propertyname} is replaced by the value of the system's
085     * propertyname. You can use user.home here for example. If the property does
086     * not exist, an empty string will be used. If the path or file after the load
087     * does not exist, the path will be ignored.
088     *
089     * @see RootLoader
090     * @author Jochen Theodorou
091     * @version $Revision: 3103 $
092     */
093    public class LoaderConfiguration {
094        
095        private final static String 
096            MAIN_PREFIX = "main is", LOAD_PREFIX="load";
097        private ArrayList classPath = new ArrayList();
098        private String main;
099        private boolean requireMain;
100        
101        /**
102         * creates a new loader configuration
103         */
104        public LoaderConfiguration() {
105            this.requireMain = true;
106        }
107        
108        /**
109         * configures this loader with a stream 
110         * 
111         * @param is           stream used to read the configuration
112         * @throws IOException if reading or parsing the contents of the stream fails
113         */
114        public void configure(InputStream is) throws IOException {
115            BufferedReader reader = new BufferedReader( new InputStreamReader(is));
116            int lineNumber=0;
117            
118            while(true) {
119                String line = reader.readLine();
120                if (line==null) break;
121                
122                line = line.trim();
123                lineNumber++;
124                
125                if (line.startsWith("#") || line.length()==0) continue;
126                
127                if (line.startsWith(LOAD_PREFIX)) {
128                    String loadPath = line.substring(LOAD_PREFIX.length()).trim();
129                    loadPath = assignProperties(loadPath);
130                    loadFilteredPath(loadPath);
131                } else if (line.startsWith(MAIN_PREFIX)) {
132                    if (main!=null) throw new IOException("duplicate definition of main in line "+lineNumber+" : "+line);
133                    main = line.substring(MAIN_PREFIX.length()).trim();
134                } else {
135                    throw new IOException("unexpected line in "+lineNumber+" : "+line);
136                }
137            }
138            
139            if (requireMain && main == null) throw new IOException("missing main class definition in config file");
140        }
141       
142        /**
143         * exapands the properties inside the given string to it's values
144         */
145        private String assignProperties(String str) {
146            int propertyIndexStart=0,propertyIndexEnd=0;
147            String result="";
148    
149            while (propertyIndexStart<str.length()) {
150                propertyIndexStart=str.indexOf("${",propertyIndexStart);
151                if (propertyIndexStart==-1) break;
152                result += str.substring(propertyIndexEnd,propertyIndexStart);
153    
154                propertyIndexEnd=str.indexOf("}",propertyIndexStart);
155                if (propertyIndexEnd==-1) break;
156                
157                String propertyKey = str.substring(propertyIndexStart+2,propertyIndexEnd);
158                String propertyValue = System.getProperty(propertyKey);
159                result+=propertyValue;
160                
161                propertyIndexEnd++;
162                propertyIndexStart=propertyIndexEnd;
163            }
164            
165            if (propertyIndexStart==-1 || propertyIndexStart>=str.length()) {
166                result+=str.substring(propertyIndexEnd);
167            } else if (propertyIndexEnd==-1) {
168                result+=str.substring(propertyIndexStart);
169            } 
170            
171            return result;
172        }
173        
174        
175        /**
176         * load a possible filtered path. Filters are defined
177         * by using the * wildcard like in any shell
178         */
179        private void loadFilteredPath(String filter) {
180            int starIndex = filter.indexOf('*');
181            if (starIndex==-1) {
182                addFile(new File(filter));
183                return;
184            } 
185            if (!parentPathDoesExist(filter)) return;        
186            String filterPart = getParentPath(filter);
187            int index = filterPart.indexOf('*');
188            final String prefix = filterPart.substring(0,index);
189            final String suffix = filterPart.substring(index+1);
190            File dir = new File(filter.substring(0,filter.length()-filterPart.length()));
191            FilenameFilter ff = new FilenameFilter() {
192                public boolean accept(File dir, String name) {
193                    if (!name.startsWith(prefix)) return false;
194                    if (!name.endsWith(suffix)) return false;
195                    return true;
196                }
197            };
198            File[] matches = dir.listFiles(ff);
199            for (int i=0; i<matches.length; i++) addFile(matches[i]);
200        }
201        
202        /**
203         * return true if the parent of the path inside the given
204         * string does exist
205         */
206        private boolean parentPathDoesExist(String path) {
207            File dir = new File (path).getParentFile();
208            return dir.exists();
209        }
210        
211        /**
212         * seperates the given path at the last '/'
213         */
214        private String getParentPath(String filter) {
215            int index = filter.lastIndexOf('/');
216            if (index==-1) return "";
217            return filter.substring(index+1);
218        }
219        
220        /**
221         * adds a file to the classpath if it does exist
222         */
223        public void addFile(File f) {
224            if (f!=null && f.exists()) {
225                try {
226                    classPath.add(f.toURI().toURL());
227                } catch (MalformedURLException e) {
228                    throw new AssertionError("converting an existing file to an url should have never thrown an exception!");
229                }
230            }        
231        }
232        
233        /**
234         * adds a file to the classpath if it does exist
235         */
236        public void addFile(String s) {
237            if (s!=null) addFile(new File(s));
238        }
239        
240        /**
241         * adds a classpath to this configuration. It expects a string
242         * with multiple paths, seperated by the system dependent 
243         * @see java.io.File#pathSeparator
244         */
245        public void addClassPath(String path) {
246            String[] paths = path.split(File.pathSeparator);
247            for (int i=0; i<paths.length; i++) {
248                addFile(new File(paths[i]));
249            }
250        }
251        
252        /**
253         * gets a classpath as URL[] from this configuration. 
254         * This can be used to construct a @see java.net.URLClassLoader
255         */
256        public URL[] getClassPathUrls() {
257            return (URL[]) classPath.toArray(new URL[]{});
258        }
259        
260        /**
261         * returns the main class or null is no is defined
262         */
263        public String getMainClass() {
264            return main;
265        }
266        
267        /**
268         * sets the main class. If there is already a main class
269         * it is overwritten. Calling @see #configure(InputStream) 
270         * after calling this method does not require a main class
271         * definition inside the stream 
272         */
273        public void setMainClass(String clazz) {
274            main = clazz;
275            requireMain = false;
276        }
277        
278        /**
279         * if set to false no main class is required when calling
280         * @see #configure(InputStream)
281         */
282        public void setRequireMain(boolean requireMain) {
283            this.requireMain = requireMain;
284        }
285    }