ConfigurationParser.java
/*
* 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.plexus.classworlds.launcher;
/*
* Copyright 2001-2006 Codehaus Foundation.
*
* Licensed 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.
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Properties;
import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
/**
* Event based launcher configuration parser, delegating effective configuration handling to ConfigurationHandler.
*
* @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
* @author Jason van Zyl
* @author Igor Fedorenko
* @see ConfigurationHandler
*/
public class ConfigurationParser {
public static final String MAIN_PREFIX = "main is";
public static final String SET_PREFIX = "set";
public static final String IMPORT_PREFIX = "import";
public static final String LOAD_PREFIX = "load";
public static final String MODULE_PREFIX = "module";
public static final String ADD_EXPORTS_PREFIX = "add-exports";
public static final String ADD_OPENS_PREFIX = "add-opens";
public static final String ADD_READS_PREFIX = "add-reads";
/**
* Optionally spec prefix.
*/
public static final String OPTIONALLY_PREFIX = "optionally";
protected static final String FROM_SEPARATOR = " from ";
protected static final String USING_SEPARATOR = " using ";
protected static final String DEFAULT_SEPARATOR = " default ";
private final ConfigurationHandler handler;
private final Properties systemProperties;
public ConfigurationParser(ConfigurationHandler handler, Properties systemProperties) {
this.handler = handler;
this.systemProperties = systemProperties;
}
/**
* Parse launcher configuration file and send events to the handler.
*
* @param is the inputstream
* @throws IOException when IOException occurs
* @throws ConfigurationException when ConfigurationException occurs
* @throws DuplicateRealmException when realm already exists
* @throws NoSuchRealmException when realm doesn't exist
*/
public void parse(InputStream is)
throws IOException, ConfigurationException, DuplicateRealmException, NoSuchRealmException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
int lineNo = 0;
boolean mainSet = false;
String curRealm = null;
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
++lineNo;
line = line.trim();
if (canIgnore(line)) {
continue;
}
char lineFirstChar = line.charAt(0);
switch (lineFirstChar) {
case 'm':
if (line.startsWith(MODULE_PREFIX)) {
handleModuleConfiguration(line, lineNo);
} else {
mainSet = handleMainConfiguration(line, lineNo, mainSet);
}
break;
case 's':
if (handleSetConfiguration(line, lineNo)) {
continue;
}
break;
case '[':
curRealm = handleRealmConfiguration(line, lineNo);
break;
case 'i':
handleImportConfiguration(line, lineNo, curRealm);
break;
case 'l':
handleLoadConfiguration(line, lineNo);
break;
case 'a':
handleAddDirective(line, lineNo);
break;
case 'o':
handleOptionallyConfiguration(line, lineNo);
break;
default:
throw new ConfigurationException("Unhandled configuration", lineNo, line);
}
}
}
}
/**
* Load a glob into the specified classloader.
*
* @param line The path configuration line.
* @param optionally Whether the path is optional or required
* @throws MalformedURLException If the line does not represent
* a valid path element.
* @throws FileNotFoundException If the line does not represent
* a valid path element in the filesystem.
* @throws ConfigurationException will never occur (thrown for backwards compatibility)
*/
protected void loadGlob(String line, boolean optionally)
throws MalformedURLException, FileNotFoundException, ConfigurationException {
File globFile = new File(line);
File dir = globFile.getParentFile();
if (!dir.exists()) {
if (optionally) {
return;
} else {
throw new FileNotFoundException(dir.toString());
}
}
String localName = globFile.getName();
int starLoc = localName.indexOf("*");
final String prefix = localName.substring(0, starLoc);
final String suffix = localName.substring(starLoc + 1);
File[] matches = dir.listFiles((dir1, name) -> {
if (!name.startsWith(prefix)) {
return false;
}
if (!name.endsWith(suffix)) {
return false;
}
return true;
});
for (File match : matches) {
handler.addLoadFile(match);
}
}
/**
* Filter a string for system properties.
*
* <p>Supports {@code ${name}}, {@code ${name:-default}} (use default when the property is
* absent or empty), and {@code ${name:+alternate}} (use alternate when the property is
* present and non-empty) syntax.</p>
*
* @param text The text to filter.
* @return The filtered text.
* @throws ConfigurationException If a required property does not exist or if there is a
* syntax error (unterminated placeholder).
*/
protected String filter(String text) throws ConfigurationException {
// Quick check: if no placeholder present, return as-is
if (!text.contains("${")) {
return text;
}
// Validate: no unterminated placeholders
int start = text.indexOf("${");
while (start >= 0) {
int stop = text.indexOf("}", start);
if (stop < 0) {
throw new ConfigurationException("Unterminated property: " + text.substring(start));
}
start = text.indexOf("${", stop + 1);
}
try {
String result = Interpolator.interpolate(text, name -> {
String value = systemProperties.getProperty(name);
/* do our best if we are not running from surefire */
if (value == null && "basedir".equals(name)) {
value = new File("").getAbsolutePath();
}
return value;
});
// If any placeholder survived unexpanded (no default, property missing), report it
int unresolved = result.indexOf("${");
if (unresolved >= 0) {
int stop = result.indexOf("}", unresolved);
String propName =
stop > unresolved ? result.substring(unresolved + 2, stop) : result.substring(unresolved + 2);
throw new ConfigurationException("No such property: " + propName);
}
return result;
} catch (IllegalArgumentException e) {
throw new ConfigurationException(e.getMessage());
}
}
/**
* Determine if a line can be ignored because it is
* a comment or simply blank.
*
* @param line The line to test.
* @return <code>true</code> if the line is ignorable,
* otherwise <code>false</code>.
*/
private boolean canIgnore(String line) {
return (line.isEmpty() || line.startsWith("#"));
}
private boolean handleMainConfiguration(String line, int lineNo, boolean mainSet) throws ConfigurationException {
if (line.startsWith(MAIN_PREFIX)) {
if (mainSet) {
throw new ConfigurationException("Duplicate main configuration", lineNo, line);
}
int fromLoc = line.indexOf(FROM_SEPARATOR, MAIN_PREFIX.length());
if (fromLoc < 0) {
throw new ConfigurationException("Missing from clause", lineNo, line);
}
String mainClassName =
filter(line.substring(MAIN_PREFIX.length(), fromLoc).trim());
String mainRealmName =
filter(line.substring(fromLoc + FROM_SEPARATOR.length()).trim());
this.handler.setAppMain(mainClassName, mainRealmName);
return true;
}
throw new ConfigurationException("Unhandled configuration", lineNo, line);
}
private boolean handleSetConfiguration(String line, int lineNo) throws ConfigurationException {
if (line.startsWith(SET_PREFIX)) {
String conf = line.substring(SET_PREFIX.length()).trim();
int usingLoc = conf.indexOf(USING_SEPARATOR);
String property = null;
String propertiesFileName = null;
if (usingLoc >= 0) {
property = conf.substring(0, usingLoc).trim();
propertiesFileName = filter(
conf.substring(usingLoc + USING_SEPARATOR.length()).trim());
conf = propertiesFileName;
}
String defaultValue = null;
int defaultLoc = conf.indexOf(DEFAULT_SEPARATOR);
if (defaultLoc >= 0) {
defaultValue = filter(
conf.substring(defaultLoc + DEFAULT_SEPARATOR.length()).trim());
if (property == null) {
property = conf.substring(0, defaultLoc).trim();
} else {
propertiesFileName = conf.substring(0, defaultLoc).trim();
}
}
String value = systemProperties.getProperty(property);
if (value != null) {
return true;
}
if (propertiesFileName != null) {
File propertiesFile = new File(propertiesFileName);
if (propertiesFile.exists()) {
Properties properties = new Properties();
try (InputStream inputStream = Files.newInputStream(Paths.get(propertiesFileName))) {
properties.load(inputStream);
value = properties.getProperty(property);
} catch (Exception e) {
// do nothing
}
}
}
if (value == null && defaultValue != null) {
value = defaultValue;
}
if (value != null) {
value = filter(value);
systemProperties.setProperty(property, value);
}
return false;
}
throw new ConfigurationException("Unhandled configuration", lineNo, line);
}
private String handleRealmConfiguration(String line, int lineNo)
throws ConfigurationException, DuplicateRealmException {
int rbrack = line.indexOf("]");
if (rbrack < 0) {
throw new ConfigurationException("Invalid realm specifier", lineNo, line);
}
String realmName = line.substring(1, rbrack);
handler.addRealm(realmName);
return realmName;
}
private void handleImportConfiguration(String line, int lineNo, String curRealm)
throws ConfigurationException, NoSuchRealmException {
if (line.startsWith(IMPORT_PREFIX)) {
if (curRealm == null) {
throw new ConfigurationException("Unhandled import", lineNo, line);
}
int fromLoc = line.indexOf(FROM_SEPARATOR, IMPORT_PREFIX.length());
if (fromLoc < 0) {
throw new ConfigurationException("Missing from clause", lineNo, line);
}
String importSpec = line.substring(IMPORT_PREFIX.length(), fromLoc).trim();
String realmName = line.substring(fromLoc + FROM_SEPARATOR.length()).trim();
handler.addImportFrom(realmName, importSpec);
return;
}
throw new ConfigurationException("Unhandled configuration", lineNo, line);
}
private void handleLoadConfiguration(String line, int lineNo)
throws ConfigurationException, FileNotFoundException, MalformedURLException {
if (line.startsWith(LOAD_PREFIX)) {
String constituent = line.substring(LOAD_PREFIX.length()).trim();
constituent = filter(constituent);
if (constituent.contains("*")) {
loadGlob(constituent, false /*not optionally*/);
} else {
File file = new File(constituent);
if (file.exists()) {
handler.addLoadFile(file);
} else {
try {
handler.addLoadURL(new URL(constituent));
} catch (MalformedURLException e) {
throw new FileNotFoundException(constituent);
}
}
}
return;
}
throw new ConfigurationException("Unhandled configuration", lineNo, line);
}
private void handleModuleConfiguration(String line, int lineNo)
throws ConfigurationException, FileNotFoundException, MalformedURLException {
if (line.startsWith(MODULE_PREFIX)) {
String constituent = line.substring(MODULE_PREFIX.length()).trim();
constituent = filter(constituent);
if (constituent.contains("*")) {
moduleGlob(constituent);
} else {
File file = new File(constituent);
if (file.exists()) {
handler.addModuleFile(file);
} else {
throw new FileNotFoundException(constituent);
}
}
return;
}
throw new ConfigurationException("Unhandled configuration", lineNo, line);
}
private void handleAddDirective(String line, int lineNo) throws ConfigurationException {
if (line.startsWith(ADD_EXPORTS_PREFIX)) {
String spec = line.substring(ADD_EXPORTS_PREFIX.length()).trim();
parseModulePackageDirective(spec, lineNo, line, handler::addExports);
} else if (line.startsWith(ADD_OPENS_PREFIX)) {
String spec = line.substring(ADD_OPENS_PREFIX.length()).trim();
parseModulePackageDirective(spec, lineNo, line, handler::addOpens);
} else if (line.startsWith(ADD_READS_PREFIX)) {
String spec = line.substring(ADD_READS_PREFIX.length()).trim();
int eqLoc = spec.indexOf('=');
if (eqLoc < 0) {
throw new ConfigurationException("Missing '=' in add-reads directive", lineNo, line);
}
String source = spec.substring(0, eqLoc).trim();
String target = spec.substring(eqLoc + 1).trim();
handler.addReads(source, target);
} else {
throw new ConfigurationException("Unhandled configuration", lineNo, line);
}
}
private void parseModulePackageDirective(String spec, int lineNo, String line, ModulePackageHandler mph)
throws ConfigurationException {
int slashLoc = spec.indexOf('/');
if (slashLoc < 0) {
throw new ConfigurationException("Missing '/' in module/package directive", lineNo, line);
}
String module = spec.substring(0, slashLoc).trim();
String rest = spec.substring(slashLoc + 1).trim();
String pkg;
String target;
int eqLoc = rest.indexOf('=');
if (eqLoc >= 0) {
pkg = rest.substring(0, eqLoc).trim();
target = rest.substring(eqLoc + 1).trim();
} else {
pkg = rest;
target = "ALL-UNNAMED";
}
mph.handle(module, pkg, target);
}
@FunctionalInterface
private interface ModulePackageHandler {
void handle(String module, String pkg, String target);
}
protected void moduleGlob(String line) throws MalformedURLException, FileNotFoundException {
File globFile = new File(line);
File dir = globFile.getParentFile();
if (!dir.exists()) {
return;
}
String localName = globFile.getName();
int starLoc = localName.indexOf("*");
final String prefix = localName.substring(0, starLoc);
final String suffix = localName.substring(starLoc + 1);
File[] matches = dir.listFiles((dir1, name) -> name.startsWith(prefix) && name.endsWith(suffix));
if (matches != null) {
for (File match : matches) {
handler.addModuleFile(match);
}
}
}
private void handleOptionallyConfiguration(String line, int lineNo)
throws ConfigurationException, FileNotFoundException, MalformedURLException {
if (line.startsWith(OPTIONALLY_PREFIX)) {
String constituent = line.substring(OPTIONALLY_PREFIX.length()).trim();
constituent = filter(constituent);
if (constituent.contains("*")) {
loadGlob(constituent, true /*optionally*/);
} else {
File file = new File(constituent);
if (file.exists()) {
handler.addLoadFile(file);
} else {
try {
handler.addLoadURL(new URL(constituent));
} catch (MalformedURLException e) {
// swallow
}
}
}
return;
}
throw new ConfigurationException("Unhandled configuration", lineNo, line);
}
}