| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.codehaus.groovy.tools; |
| |
| import org.apache.groovy.util.SystemUtil; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Class used to configure a RootLoader from a stream or by using |
| * its methods. |
| * <p> |
| * The stream can be for example a FileInputStream from a file with |
| * the following format: |
| * <p> |
| * <pre> |
| * # comment |
| * main is classname |
| * load path |
| * load file |
| * load pathWith${property} |
| * load pathWith!{required.property} |
| * load path/*.jar |
| * load path/**/*.jar |
| * </pre> |
| * <ul> |
| * <li>All lines starting with "#" are ignored.</li> |
| * <li>The "main is" part may only be once in the file. The String |
| * afterwards is the name of a class with a main method. </li> |
| * <li>The "load" command will add the given file or path to the |
| * classpath in this configuration object. If the path does not |
| * exist, the path will be ignored. |
| * </li> |
| * <li>properties referenced using !{x} are required.</li> |
| * <li>properties referenced using ${x} are not required. If the |
| * property does not exist the whole load instruction line will |
| * be ignored.</li> |
| * <li>* is used to match zero or more characters in a file.</li> |
| * <li>** is used to match zero or more directories.</li> |
| * <li>Loading paths with <code>load ./*.jar</code> or <code>load *.jar</code> are not supported.</li> |
| * </ul> |
| * <p> |
| * Defining the main class is required unless setRequireMain(boolean) is |
| * called with false, before reading the configuration. |
| * You can use the wildcard "*" to filter the path, but only for files, not |
| * directories. To match directories use "**". The ${propertyname} is replaced by the value of the system's |
| * property name. You can use user.home here for example. If the property does |
| * not exist, an empty string will be used. If the path or file after the load |
| * command does not exist, the path will be ignored. |
| * |
| * @see RootLoader |
| */ |
| public class LoaderConfiguration { |
| |
| private static final String MAIN_PREFIX = "main is", LOAD_PREFIX = "load", GRAB_PREFIX = "grab", PROP_PREFIX = "property", CONFIGSCRIPT_PREFIX = "configscript"; |
| private final List<URL> classPath = new ArrayList<URL>(); |
| private String main; |
| private boolean requireMain; |
| private static final char WILDCARD = '*'; |
| private static final String ALL_WILDCARD = "" + WILDCARD + WILDCARD; |
| private static final String MATCH_FILE_NAME = "\\\\E[^/]+?\\\\Q"; |
| private static final String MATCH_ALL = "\\\\E.+?\\\\Q"; |
| private final List<String> grabList = new ArrayList<String>(); |
| private final List<String> configScripts = new ArrayList<String>(); |
| |
| /** |
| * creates a new loader configuration |
| */ |
| public LoaderConfiguration() { |
| this.requireMain = true; |
| } |
| |
| /** |
| * configures this loader with a stream |
| * |
| * @param is stream used to read the configuration |
| * @throws IOException if reading or parsing the contents of the stream fails |
| */ |
| public void configure(InputStream is) throws IOException { |
| try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { |
| int lineNumber = 0; |
| |
| while (true) { |
| String line = reader.readLine(); |
| if (line == null) break; |
| |
| line = line.trim(); |
| lineNumber++; |
| |
| if (line.startsWith("#") || line.length() == 0) continue; |
| |
| if (line.startsWith(LOAD_PREFIX)) { |
| String loadPath = line.substring(LOAD_PREFIX.length()).trim(); |
| loadPath = assignProperties(loadPath); |
| loadFilteredPath(loadPath); |
| } else if (line.startsWith(GRAB_PREFIX)) { |
| String grabParams = line.substring(GRAB_PREFIX.length()).trim(); |
| grabList.add(assignProperties(grabParams)); |
| } else if (line.startsWith(MAIN_PREFIX)) { |
| if (main != null) |
| throw new IOException("duplicate definition of main in line " + lineNumber + " : " + line); |
| main = line.substring(MAIN_PREFIX.length()).trim(); |
| } else if (line.startsWith(PROP_PREFIX)) { |
| String params = line.substring(PROP_PREFIX.length()).trim(); |
| String key = SystemUtil.setSystemPropertyFrom(params); |
| System.setProperty(key, assignProperties(System.getProperty(key))); |
| } else if (line.startsWith(CONFIGSCRIPT_PREFIX)) { |
| String script = line.substring(CONFIGSCRIPT_PREFIX.length()).trim(); |
| configScripts.add(assignProperties(script)); |
| } else { |
| throw new IOException("unexpected line in " + lineNumber + " : " + line); |
| } |
| } |
| } |
| |
| if (requireMain && main == null) throw new IOException("missing main class definition in config file"); |
| if (!configScripts.isEmpty()) { |
| System.setProperty("groovy.starter.configscripts", String.join(",", configScripts)); |
| } |
| } |
| |
| /* |
| * Expands the properties inside the given string to their values. |
| */ |
| private static String assignProperties(String str) { |
| int propertyIndexStart = 0, propertyIndexEnd = 0; |
| boolean requireProperty; |
| StringBuilder result = new StringBuilder(); |
| |
| while (propertyIndexStart < str.length()) { |
| { |
| int i1 = str.indexOf("${", propertyIndexStart); |
| int i2 = str.indexOf("!{", propertyIndexStart); |
| if (i1 == -1) { |
| propertyIndexStart = i2; |
| } else if (i2 == -1) { |
| propertyIndexStart = i1; |
| } else { |
| propertyIndexStart = Math.min(i1, i2); |
| } |
| requireProperty = propertyIndexStart == i2; |
| } |
| if (propertyIndexStart == -1) break; |
| result.append(str, propertyIndexEnd, propertyIndexStart); |
| |
| propertyIndexEnd = str.indexOf('}', propertyIndexStart); |
| if (propertyIndexEnd == -1) break; |
| |
| String propertyKey = str.substring(propertyIndexStart + 2, propertyIndexEnd); |
| String propertyValue = System.getProperty(propertyKey); |
| // assume properties contain paths |
| if (propertyValue == null) { |
| if (requireProperty) { |
| 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"); |
| } else { |
| return null; |
| } |
| } |
| propertyValue = getSlashyPath(propertyValue); |
| propertyValue = correctDoubleSlash(propertyValue, propertyIndexEnd, str); |
| result.append(propertyValue); |
| |
| propertyIndexEnd++; |
| propertyIndexStart = propertyIndexEnd; |
| } |
| |
| if (propertyIndexStart == -1 || propertyIndexStart >= str.length()) { |
| result.append(str, propertyIndexEnd, str.length()); |
| } else if (propertyIndexEnd == -1) { |
| result.append(str, propertyIndexStart, str.length()); |
| } |
| |
| return result.toString(); |
| } |
| |
| |
| private static String correctDoubleSlash(String propertyValue, int propertyIndexEnd, String str) { |
| int index = propertyIndexEnd + 1; |
| if (index < str.length() && str.charAt(index) == '/' && |
| propertyValue.endsWith("/") && |
| propertyValue.length() > 0) { |
| propertyValue = propertyValue.substring(0, propertyValue.length() - 1); |
| } |
| return propertyValue; |
| } |
| |
| /* |
| * Load a possibly filtered path. Filters are defined |
| * by using the * wildcard like in any shell. |
| */ |
| private void loadFilteredPath(String filter) { |
| if (filter == null) return; |
| filter = getSlashyPath(filter); |
| int starIndex = filter.indexOf(WILDCARD); |
| if (starIndex == -1) { |
| addFile(new File(filter)); |
| return; |
| } |
| boolean recursive = filter.contains(ALL_WILDCARD); |
| |
| if (filter.lastIndexOf('/') < starIndex) { |
| starIndex = filter.lastIndexOf('/') + 1; |
| } |
| String startDir = filter.substring(0, starIndex - 1); |
| File root = new File(startDir); |
| |
| filter = Pattern.quote(filter); |
| filter = filter.replaceAll("\\" + WILDCARD + "\\" + WILDCARD, MATCH_ALL); |
| filter = filter.replaceAll("\\" + WILDCARD, MATCH_FILE_NAME); |
| Pattern pattern = Pattern.compile(filter); |
| |
| final File[] files = root.listFiles(); |
| if (files != null) { |
| findMatchingFiles(files, pattern, recursive); |
| } |
| } |
| |
| private void findMatchingFiles(File[] files, Pattern pattern, boolean recursive) { |
| for (File file : files) { |
| String fileString = getSlashyPath(file.getPath()); |
| Matcher m = pattern.matcher(fileString); |
| if (m.matches() && file.isFile()) { |
| addFile(file); |
| } |
| if (file.isDirectory() && recursive) { |
| final File[] dirFiles = file.listFiles(); |
| if (dirFiles != null) { |
| findMatchingFiles(dirFiles, pattern, true); |
| } |
| } |
| } |
| } |
| |
| // change path representation to something more system independent. |
| // This solution is based on an absolute path |
| private static String getSlashyPath(final String path) { |
| String changedPath = path; |
| if (File.separatorChar != '/') |
| changedPath = changedPath.replace(File.separatorChar, '/'); |
| |
| return changedPath; |
| } |
| |
| /** |
| * Adds a file to the classpath if it exists. |
| * |
| * @param file the file to add |
| */ |
| public void addFile(File file) { |
| if (file != null && file.exists()) { |
| try { |
| classPath.add(file.toURI().toURL()); |
| } catch (MalformedURLException e) { |
| throw new AssertionError("converting an existing file to an url should have never thrown an exception!"); |
| } |
| } |
| } |
| |
| /** |
| * Adds a file to the classpath if it exists. |
| * |
| * @param filename the name of the file to add |
| */ |
| public void addFile(String filename) { |
| if (filename != null) addFile(new File(filename)); |
| } |
| |
| /** |
| * Adds a classpath to this configuration. It expects a string with |
| * multiple paths, separated by the system dependent path separator. |
| * Expands wildcards, e.g. dir/* into all the jars in dir. |
| * |
| * @param path the path as a path separator delimited string |
| * @see java.io.File#pathSeparator |
| */ |
| public void addClassPath(String path) { |
| String[] paths = path.split(File.pathSeparator); |
| for (String cpPath : paths) { |
| // Check to support wild card classpath |
| if (cpPath.endsWith("*")) { |
| File dir = new File(cpPath.substring(0, cpPath.length() - 1)); |
| File[] files = dir.listFiles(); |
| if (files != null) { |
| for (File file : files) { |
| if (file.isFile() && file.getName().endsWith(".jar")) addFile(file); |
| } |
| } |
| } else { |
| addFile(new File(cpPath)); |
| } |
| } |
| } |
| |
| /** |
| * The classpath as URL[] from this configuration. |
| * This can be used to construct a class loader. |
| * |
| * @return the classpath |
| * @see java.net.URLClassLoader |
| */ |
| public URL[] getClassPathUrls() { |
| return classPath.toArray(new URL[0]); |
| } |
| |
| /** |
| * The extra grab configuration. |
| * |
| * @return the list of grab urls |
| */ |
| public List<String> getGrabUrls() { |
| return grabList; |
| } |
| |
| /** |
| * Returns the name of the main class for this configuration. |
| * |
| * @return the name of the main class or null if not defined |
| */ |
| public String getMainClass() { |
| return main; |
| } |
| |
| /** |
| * Sets the main class. If there is already a main class |
| * it is overwritten. Calling {@link #configure(InputStream)} |
| * after calling this method does not require a main class |
| * definition inside the stream. |
| * |
| * @param classname the name to become the main class |
| */ |
| public void setMainClass(String classname) { |
| main = classname; |
| requireMain = false; |
| } |
| |
| /** |
| * Determines if a main class is required when calling. |
| * |
| * @param requireMain set to false if no main class is required |
| * @see #configure(InputStream) |
| */ |
| public void setRequireMain(boolean requireMain) { |
| this.requireMain = requireMain; |
| } |
| |
| } |