2 * Copyright 2003-2007 the original author or authors.
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 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org
.codehaus
.groovy
.tools
;
18 import groovy
.text
.RegexUtils
;
21 import java
.net
.MalformedURLException
;
23 import java
.util
.ArrayList
;
24 import java
.util
.List
;
25 import java
.util
.regex
.Matcher
;
26 import java
.util
.regex
.Pattern
;
29 * Class used to configure a RootLoader from a stream or by using
32 * The stream can be for example a FileInputStream from a file with
33 * the following format:
40 * load pathWith${property}
41 * load pathWith!{required.property}
43 * load path/**/*.jar
46 * <li>All lines starting with "#" are ignored.</li>
47 * <li>The "main is" part may only be once in the file. The String
48 * afterwards is the name of a class with a main method. </li>
49 * <li>The "load" command will add the given file or path to the
50 * classpath in this configuration object. If the path does not
51 * exist, the path will be ignored.
53 * <li>properties referenced using !{x} are required.</li>
54 * <li>properties referenced using ${x} are not required. If the
55 * property does not exist the whole load instruction line will
57 * <li>* is used to match zero or more characters in a file.</li>
58 * <li>** is used to match zero or more directories.</li>
61 * Defining the main class is required unless setRequireMain(boolean) is
62 * called with false, before reading the configuration.
63 * You can use the wildcard "*" to filter the path, but only for files, not
64 * directories. To match directories use "**". The ${propertyname} is replaced by the value of the system's
65 * property name. You can use user.home here for example. If the property does
66 * not exist, an empty string will be used. If the path or file after the load
67 * command does not exist, the path will be ignored.
69 * @author Jochen Theodorou
73 public class LoaderConfiguration
{
75 private static final String MAIN_PREFIX
= "main is", LOAD_PREFIX
= "load";
76 private List classPath
= new ArrayList();
78 private boolean requireMain
;
79 private static final char WILDCARD
= '*';
80 private static final String ALL_WILDCARD
= "" + WILDCARD
+ WILDCARD
;
81 private static final String MATCH_FILE_NAME
= "\\\\E[^/]+?\\\\Q";
82 private static final String MATCH_ALL
= "\\\\E.+?\\\\Q";
85 * creates a new loader configuration
87 public LoaderConfiguration() {
88 this.requireMain
= true;
92 * configures this loader with a stream
94 * @param is stream used to read the configuration
95 * @throws IOException if reading or parsing the contents of the stream fails
97 public void configure(InputStream is
) throws IOException
{
98 BufferedReader reader
= new BufferedReader(new InputStreamReader(is
));
102 String line
= reader
.readLine();
103 if (line
== null) break;
108 if (line
.startsWith("#") || line
.length() == 0) continue;
110 if (line
.startsWith(LOAD_PREFIX
)) {
111 String loadPath
= line
.substring(LOAD_PREFIX
.length()).trim();
112 loadPath
= assignProperties(loadPath
);
113 loadFilteredPath(loadPath
);
114 } else if (line
.startsWith(MAIN_PREFIX
)) {
116 throw new IOException("duplicate definition of main in line " + lineNumber
+ " : " + line
);
117 main
= line
.substring(MAIN_PREFIX
.length()).trim();
119 throw new IOException("unexpected line in " + lineNumber
+ " : " + line
);
123 if (requireMain
&& main
== null) throw new IOException("missing main class definition in config file");
127 * Expands the properties inside the given string to it's values.
129 private String
assignProperties(String str
) {
130 int propertyIndexStart
= 0, propertyIndexEnd
= 0;
131 boolean requireProperty
= false;
134 while (propertyIndexStart
< str
.length()) {
136 int i1
= str
.indexOf("${", propertyIndexStart
);
137 int i2
= str
.indexOf("!{", propertyIndexStart
);
139 propertyIndexStart
= i2
;
140 } else if (i2
== -1) {
141 propertyIndexStart
= i1
;
143 propertyIndexStart
= Math
.min(i1
, i2
);
145 requireProperty
= propertyIndexStart
== i2
;
147 if (propertyIndexStart
== -1) break;
148 result
+= str
.substring(propertyIndexEnd
, propertyIndexStart
);
150 propertyIndexEnd
= str
.indexOf("}", propertyIndexStart
);
151 if (propertyIndexEnd
== -1) break;
153 String propertyKey
= str
.substring(propertyIndexStart
+ 2, propertyIndexEnd
);
154 String propertyValue
= System
.getProperty(propertyKey
);
155 // assume properties contain paths
156 if (propertyValue
== null) {
157 if (requireProperty
) {
158 throw new IllegalArgumentException("Variable " + propertyKey
+ " in groovy-starter.conf references a non-existent System property! Try passing the property to the VM using -D" + propertyKey
+ "=myValue in JAVA_OPTS");
163 propertyValue
= getSlashyPath(propertyValue
);
164 result
+= propertyValue
;
167 propertyIndexStart
= propertyIndexEnd
;
170 if (propertyIndexStart
== -1 || propertyIndexStart
>= str
.length()) {
171 result
+= str
.substring(propertyIndexEnd
);
172 } else if (propertyIndexEnd
== -1) {
173 result
+= str
.substring(propertyIndexStart
);
181 * Load a possibly filtered path. Filters are defined
182 * by using the * wildcard like in any shell.
184 private void loadFilteredPath(String filter
) {
185 if (filter
== null) return;
186 int starIndex
= filter
.indexOf(WILDCARD
);
187 if (starIndex
== -1) {
188 addFile(new File(filter
));
191 boolean recursive
= filter
.indexOf(ALL_WILDCARD
) != -1;
193 String startDir
= filter
.substring(0, starIndex
- 1);
194 File root
= new File(startDir
);
196 filter
= RegexUtils
.quote(filter
);
197 filter
= filter
.replaceAll("\\" + WILDCARD
+ "\\" + WILDCARD
, MATCH_ALL
);
198 filter
= filter
.replaceAll("\\" + WILDCARD
, MATCH_FILE_NAME
);
199 Pattern pattern
= Pattern
.compile(filter
);
201 final File
[] files
= root
.listFiles();
203 findMatchingFiles(files
, pattern
, recursive
);
207 private void findMatchingFiles(File
[] files
, Pattern pattern
, boolean recursive
) {
208 for (int i
= 0; i
< files
.length
; i
++) {
209 File file
= files
[i
];
210 String fileString
= getSlashyPath(file
.getPath());
211 Matcher m
= pattern
.matcher(fileString
);
212 if (m
.matches() && file
.isFile()) {
215 if (file
.isDirectory() && recursive
) {
216 final File
[] dirFiles
= file
.listFiles();
217 if (dirFiles
!= null) {
218 findMatchingFiles(dirFiles
, pattern
, true);
224 // change path representation to something more system independent.
225 // This solution is based on an absolute path
226 private String
getSlashyPath(final String path
) {
227 String changedPath
= path
;
228 if (File
.separatorChar
!= '/')
229 changedPath
= changedPath
.replace(File
.separatorChar
, '/');
235 * return true if the parent of the path inside the given
238 private boolean parentPathDoesExist(String path
) {
239 File dir
= new File(path
).getParentFile();
244 * seperates the given path at the last '/'
246 private String
getParentPath(String filter
) {
247 int index
= filter
.lastIndexOf('/');
248 if (index
== -1) return "";
249 return filter
.substring(index
+ 1);
253 * Adds a file to the classpath if it exists.
255 * @param file the file to add
257 public void addFile(File file
) {
258 if (file
!= null && file
.exists()) {
260 classPath
.add(file
.toURI().toURL());
261 } catch (MalformedURLException e
) {
262 throw new AssertionError("converting an existing file to an url should have never thrown an exception!");
268 * Adds a file to the classpath if it exists.
270 * @param filename the name of the file to add
272 public void addFile(String filename
) {
273 if (filename
!= null) addFile(new File(filename
));
277 * Adds a classpath to this configuration. It expects a string with
278 * multiple paths, seperated by the system dependent path separator
280 * @param path the path as a path separator delimited string
281 * @see java.io.File#pathSeparator
283 public void addClassPath(String path
) {
284 String
[] paths
= path
.split(File
.pathSeparator
);
285 for (int i
= 0; i
< paths
.length
; i
++) {
286 addFile(new File(paths
[i
]));
291 * The classpath as URL[] from this configuration.
292 * This can be used to construct a class loader.
294 * @return the classpath
295 * @see java.net.URLClassLoader
297 public URL
[] getClassPathUrls() {
298 return (URL
[]) classPath
.toArray(new URL
[classPath
.size()]);
302 * Returns the name of the main class for this configuration.
304 * @return the name of the main class or null if not defined
306 public String
getMainClass() {
311 * Sets the main class. If there is already a main class
312 * it is overwritten. Calling @see #configure(InputStream)
313 * after calling this method does not require a main class
314 * definition inside the stream.
316 * @param classname the name to become the main class
318 public void setMainClass(String classname
) {
324 * Determines if a main class is required when calling.
326 * @param requireMain set to false if no main class is required
327 * @see #configure(InputStream)
329 public void setRequireMain(boolean requireMain
) {
330 this.requireMain
= requireMain
;