blob: 5876fe8ddc72545e69f4b06d9c37d660edcf86a6 [file] [log] [blame]
/*
* 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.netbeans.modules.maven.runjar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.modules.maven.NbMavenProjectImpl;
import org.netbeans.modules.maven.api.customizer.ModelHandle2;
import org.netbeans.modules.maven.customizer.RunJarPanel;
import org.netbeans.modules.maven.execute.ActionToGoalUtils;
import org.netbeans.modules.maven.execute.model.ActionToGoalMapping;
import org.netbeans.modules.maven.execute.model.NetbeansActionMapping;
import org.netbeans.spi.project.ActionProvider;
/**
*
* @author sdedic
*/
public final class MavenExecuteUtils {
/**
* Hint for the default PrereqqCheckeers that explicit parameters have been already processed.
* Will not be propagated to maven process.
*/
public static final String RUN_EXPLICIT_PROCESSED = "NbIde.ExplicitParametersApplied"; // NOI18N
/**
* Name of the property for VM arguments.
* @since 2.144
*/
public static final String RUN_VM_PARAMS = "exec.vmArgs"; //NOI18N
/**
* Name of the property to pass main class. ${packageClassName} works as well.
* @since 2.144
*/
public static final String RUN_MAIN_CLASS = "exec.mainClass"; //NOI18N
/**
* Name of the property for application arguments.
* @since 2.144
*/
public static final String RUN_APP_PARAMS = "exec.appArgs"; //NOI18N
/**
* Name of the property that collects the entire command line.
*/
public static final String RUN_PARAMS = "exec.args"; //NOI18N
/**
* Name of the property for working directory passed to th exec plugin
*/
public static final String RUN_WORKDIR = "exec.workingdir"; //NOI18N
private static final String RUN_VM_PARAMS_TOKEN = "${" + RUN_VM_PARAMS + "}"; //NOI18N
private static final String RUN_APP_PARAMS_TOKEN = "${" + RUN_APP_PARAMS + "}"; //NOI18N
private static final String RUN_MAIN_CLASS_TOKEN = "${" + RUN_MAIN_CLASS + "}"; //NOI18N
static final String PACKAGE_CLASS_NAME_TOKEN = "${packageClassName}"; //NOI18N
public static final String EXEC_ARGS_CLASSPATH_TOKEN = "-classpath %classpath"; // NOI18N
public static final String DEFAULT_EXEC_ARGS_CLASSPATH = EXEC_ARGS_CLASSPATH_TOKEN + " ${packageClassName}"; // NOI18N
static final String DEFAULT_DEBUG_PARAMS = "-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address}"; //NOI18N
static final String DEFAULT_EXEC_ARGS_CLASSPATH2 = "${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs}"; // NOI18N
public static final String ENV_PREFIX = "Env."; // NOI18N
public static final String ENV_REMOVED = new String("null"); // A special instance for env vars to be removed. // NOI18N
/**
* ID of the 'profile' action.
*/
public static final String PROFILE_CMD = "profile"; // NOI18N
/**
* A helper that can update action mappings based on changes
* made on the helper instance. Use {@link #createExecutionEnvHelper}
* to make an instance.
*/
public static final class ExecutionEnvHelper {
private final ActionToGoalMapping goalMappings;
private final NbMavenProjectImpl project;
private String oldAllParams;
private String oldVmParams;
private String oldAppParams;
private String oldWorkDir;
private String oldMainClass;
private boolean currentRun;
private boolean currentDebug;
private boolean currentProfile;
private String vmParams;
private String appParams;
private String workDir;
private String mainClass;
private NetbeansActionMapping run;
private NetbeansActionMapping debug;
private NetbeansActionMapping profile;
private boolean mergedConfig;
private boolean modified;
ExecutionEnvHelper(
NbMavenProjectImpl project,
NetbeansActionMapping run,
NetbeansActionMapping debug,
NetbeansActionMapping profile,
ActionToGoalMapping goalMappings) {
this.project = project;
this.goalMappings = goalMappings;
this.run = run;
this.debug = debug;
this.profile = profile;
}
private String fallbackParams(String paramName, boolean stripDebug) {
String val = run != null ? run.getProperties().get(paramName) : null;
if (val == null && debug != null) {
val = debug.getProperties().get(paramName);
if (val != null && stripDebug) {
val = String.join(" ", extractDebugJVMOptions(val));
}
}
if (val == null && profile != null) {
val = profile.getProperties().get(paramName);
}
return val == null ? "" : val.trim(); // NOI18N
}
private String appendIfNotEmpty(String a, String b) {
if (a == null || a.isEmpty()) {
return b;
}
if (b == null || b.isEmpty()) {
return a;
}
return a + " " + b;
}
public ActionToGoalMapping getGoalMappings() {
return goalMappings;
}
public boolean isModified() {
return modified;
}
public boolean isValid() {
return currentRun && currentDebug && currentProfile;
}
public boolean isCurrentRun() {
return currentRun;
}
public boolean isCurrentDebug() {
return currentDebug;
}
public boolean isCurrentProfile() {
return currentProfile;
}
public void setMainClass(String mainClass) {
this.mainClass = mainClass;
}
public void setVmParams(String vmParams) {
this.vmParams = vmParams;
}
public void setAppParams(String appParams) {
this.appParams = appParams;
}
public NbMavenProjectImpl getProject() {
return project;
}
public String getWorkDir() {
return oldWorkDir;
}
public void setWorkDir(String workDir) {
this.workDir = workDir;
}
public String getMainClass() {
return mainClass;
}
public NetbeansActionMapping getRun() {
return run;
}
public NetbeansActionMapping getProfile() {
return profile;
}
public String getAllParams() {
return oldAllParams;
}
public String getVmParams() {
return vmParams;
}
public String getAppParams() {
return appParams;
}
private NetbeansActionMapping getMapping(String a) {
NetbeansActionMapping m = ActionToGoalUtils.getDefaultMapping(a, project);
return m;
}
/**
* Loads and parses values from the project's nbactions.xml
*/
public void loadFromProject() {
NetbeansActionMapping m;
if (run == null) {
run = getMapping(ActionProvider.COMMAND_RUN);
}
if (debug == null) {
debug = getMapping(ActionProvider.COMMAND_DEBUG);
}
if (profile == null) {
profile = getMapping(PROFILE_CMD);
}
currentRun = checkNewMapping(run);
currentDebug = checkNewMapping(debug);
currentProfile = checkNewMapping(profile);
oldWorkDir = fallbackParams(RUN_WORKDIR, false);
oldAllParams = fallbackParams(RUN_PARAMS, false);
oldVmParams = fallbackParams(RUN_VM_PARAMS, true);
oldAppParams = fallbackParams(RUN_APP_PARAMS, false);
oldMainClass = fallbackParams(RUN_MAIN_CLASS, false);
mergedConfig = (oldVmParams.isEmpty() && oldAppParams.isEmpty() && oldMainClass.isEmpty());
appendVMParamsFromOldParams();
addAppParamsFromOldParams();
loadMainClass();
workDir = oldWorkDir;
vmParams = oldVmParams;
appParams = oldAppParams;
mainClass = oldMainClass;
}
private String eraseTokens(String original, boolean withNewlines, String... tokens) {
StringBuilder sb = new StringBuilder();
for (String p : tokens) {
if (sb.length() > 0) {
sb.append("|");
}
sb.append(Pattern.quote(p));
if (withNewlines) {
sb.append("\\n?");
}
}
return original.replaceAll(sb.toString(), "").trim();
}
private void appendVMParamsFromOldParams() {
String oldSplitVMParams = splitJVMParams(oldAllParams, true);
if (!oldSplitVMParams.isEmpty()) {
// try to get VM arguments out of all exec.args, but ignore -classpath added automatically, and
// exec.vmArgs present / added by default.
oldSplitVMParams = eraseTokens(oldSplitVMParams, true, "-classpath %classpath", RUN_VM_PARAMS_TOKEN);
oldVmParams = appendIfNotEmpty(oldVmParams, oldSplitVMParams);
}
}
private void addAppParamsFromOldParams() {
String p = splitParams(oldAllParams);
if (!p.isEmpty()) {
p = eraseTokens(p, false, RUN_APP_PARAMS_TOKEN);
oldAppParams = appendIfNotEmpty(oldAppParams, p);
}
}
private void loadMainClass() {
if (oldMainClass.trim().isEmpty()) {
oldMainClass = splitMainClass(oldAllParams);
// splitMainClass is never null
}
if (PACKAGE_CLASS_NAME_TOKEN.equals(oldMainClass) || RUN_MAIN_CLASS_TOKEN.equals(oldMainClass)) {
oldMainClass = "";
}
}
private boolean checkNewMapping(NetbeansActionMapping map) {
if (map == null || map.getGoals() == null) {
return false; //#164323
}
Iterator it = map.getGoals().iterator();
while (it.hasNext()) {
String goal = (String) it.next();
if (goal.matches("org\\.codehaus\\.mojo\\:exec-maven-plugin\\:(.)+\\:exec") //NOI18N
|| goal.indexOf("exec:exec") > -1) { //NOI18N
if (map.getProperties() != null) {
if (map.getProperties().containsKey("exec.args")) {
String execArgs = map.getProperties().get("exec.args");
if (execArgs.contains("-classpath")) {
return true;
}
}
if (map.getProperties().containsKey("exec.vmArgs")) {
String execArgs = map.getProperties().get("exec.vmArgs");
if (execArgs.contains("-classpath")) {
return true;
}
}
}
}
}
return false;
}
public void applyToMappings() {
if (!(currentRun || currentDebug || currentProfile)) {
return;
}
if (currentRun) {
updateAction(run, "");
}
if (currentDebug) {
updateAction(debug, DEFAULT_DEBUG_PARAMS);
}
if (currentProfile) {
updateAction(profile, "");
}
}
private void updateAction(NetbeansActionMapping mapping, String debuVMArgs) {
boolean changed = false;
if (!oldWorkDir.equals(workDir)) {
mapping.addProperty(RUN_WORKDIR, workDir);
changed = true;
}
if (!oldAppParams.equals(appParams)) {
mapping.addProperty(RUN_APP_PARAMS, appParams);
changed = true;
}
String newMainClass = this.mainClass;
if (newMainClass.trim().length() == 0) {
newMainClass = PACKAGE_CLASS_NAME_TOKEN;
}
if (!oldMainClass.equals(newMainClass)) {
mapping.addProperty(RUN_MAIN_CLASS, newMainClass);
changed = true;
}
if (!workDir.equals(oldWorkDir)) {
mapping.addProperty(RUN_WORKDIR, workDir);
changed = true;
}
String oneLineVMParams = vmParams.replace('\n', ' ');
String newVMParams = appendIfNotEmpty(oneLineVMParams, debuVMArgs);
if (!oldVmParams.equals(newVMParams)) {
mapping.addProperty(RUN_VM_PARAMS, newVMParams);
changed = true;
}
if (changed) {
// define the properties, if not defined ...
Map<String, String> props = mapping.getProperties();
if (mapping.getProperties().get(RUN_VM_PARAMS) == null) {
mapping.addProperty(RUN_VM_PARAMS, vmParams);
}
if (mapping.getProperties().get(RUN_APP_PARAMS) == null) {
mapping.addProperty(RUN_APP_PARAMS, appParams);
}
if (mapping.getProperties().get(RUN_MAIN_CLASS) == null) {
mapping.addProperty(RUN_MAIN_CLASS, newMainClass);
}
mapping.addProperty(RUN_PARAMS,
"${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs}"
);
}
if (changed) {
ModelHandle2.setUserActionMapping(mapping, goalMappings);
modified = true;
}
}
}
/**
* Splits a command line, pays respect to quoting and newlines.
* @param line original line
* @return line split into individual arguments.
*/
public static String[] splitCommandLine(String line) {
if (line == null) {
return new String[0];
}
String l = line.trim();
if (l.isEmpty()) {
return new String[0];
}
List<String> result = new ArrayList<>();
for (String part : propertySplitter(l, true)) {
result.add(part);
}
return result.toArray(new String[result.size()]);
}
private static boolean isNullOrEmpty(String s) {
return s == null || s.trim().isEmpty();
}
/**
* Checks that the mapping does not specify custom exec arguments. If
* the exec.args is set to {@link #DEFAULT_EXEC_ARGS_CLASSPATH}, the
* `packageClassName' has not been set (= unspecified).
* If the exec.args is set tp {@link #DEFAULT_EXEC_ARGS_CLASSPATH2}, then
* none of the referenced properties can provide a value (the exec.args itself
* is not changed by the IDE, just the referenced properties).
* <p>
* Other values of `exec.args' means user customizations.
* <p>
* Returns {@code null}, if there are user customizations. Returns the value of
* exec.args (the default string) so the caller can retain the parameter
* passing style.
*
* @param mapp action mapping.
* @return
*/
public static String doesNotSpecifyCustomExecArgs(NetbeansActionMapping mapp) {
return doesNotSpecifyCustomExecArgs(true, mapp.getProperties());
}
private static boolean equalsOrIncludes(boolean exact, String text, String toFind) {
if (exact) {
return text.equals(toFind);
} else {
return text.contains(toFind);
}
}
/**
* The inexact match is used from RunJarStartupArgs
*/
public static String doesNotSpecifyCustomExecArgs(boolean exact, Map<? extends String, ? extends String> props) {
String execArgs = props.get(RUN_PARAMS);
String replacedMainClass = props.get(RUN_MAIN_CLASS);
String template;
boolean secondTry = replacedMainClass != null && execArgs.contains(PACKAGE_CLASS_NAME_TOKEN);
template = DEFAULT_EXEC_ARGS_CLASSPATH;
if (equalsOrIncludes(exact, execArgs, template)) {
return template;
}
if (secondTry) {
template = template.replace(PACKAGE_CLASS_NAME_TOKEN, replacedMainClass);
if (equalsOrIncludes(exact, execArgs, template)) {
return template;
}
}
template = DEFAULT_EXEC_ARGS_CLASSPATH2;
if (!equalsOrIncludes(exact, execArgs, template)) {
if (!secondTry) {
return null;
}
template = template.replace(PACKAGE_CLASS_NAME_TOKEN, replacedMainClass);
if (!equalsOrIncludes(exact, execArgs, template)) {
return null;
}
}
if (!exact) {
return template;
}
// none of the properties refrenced in DEFAULT_EXEC_ARGS_CLASSPATH2 is defined:
if (isNullOrEmpty(props.get(RUN_APP_PARAMS)) &&
isNullOrEmpty(props.get(RUN_VM_PARAMS))) {
String mainClass = props.get(RUN_MAIN_CLASS);
if (mainClass == null ||
"".equals(mainClass) ||
MavenExecuteUtils.PACKAGE_CLASS_NAME_TOKEN.equals(mainClass)) {
return template;
}
}
return null;
}
public static boolean isEnvRemovedValue(String value) {
return value == ENV_REMOVED; // It's crutial to test the instance identity
}
/**
* Creates a helper to edit the mapping instances. Individual settings can be
* inspected by getters and changed by setters on the helper, changes can be then
* applied back to the mappings.
* @param project the target project
* @param run run action mapping
* @param debug debug action mapping
* @param profile profile action mapping
* @param goalMappings the mapping registry
* @return
*/
public static ExecutionEnvHelper createExecutionEnvHelper(
NbMavenProjectImpl project,
NetbeansActionMapping run,
NetbeansActionMapping debug,
NetbeansActionMapping profile,
ActionToGoalMapping goalMappings) {
return new ExecutionEnvHelper(project, run, debug, profile, goalMappings);
}
/**
* Joins parameters into a single string. Quotes as necessary if parameters contain
* spaces. Checks for already quoted or escaped parameters.
* @param params List of parameters.
* @return single command line.
*/
public static String joinParameters(String... params) {
if (params == null) {
return ""; // NOI18N
}
return joinParameters(Arrays.asList(params));
}
private static boolean isQuoteChar(char c) {
return c == '\'' || c == '"';
}
public static List<String> escapeParameters(List<String> params) {
List<String> ret = new ArrayList<>();
for (String s : params) {
if (s == null) {
continue;
}
if (s.length() > 1) {
char c = s.charAt(0);
if (isQuoteChar(c) && s.charAt(s.length() - 1) == c) {
ret.add(s);
continue;
}
}
// note: does not care about escaped spaces.
if (!s.contains(" ")) {
ret.add(s.replace("'", "\\'").replace("\"", "\\\""));
} else {
ret.add("\"" + s.replace("\"", "\\\"") + "\"");
}
}
return ret;
}
public static String joinParameters(List<String> params) {
return String.join(" ", escapeParameters(params));
}
public static List<String> extractDebugJVMOptions(String argLine) {
Iterable<String> split = propertySplitter(argLine, true);
List<String> toRet = new ArrayList<String>();
for (String arg : split) {
if ("-Xdebug".equals(arg)) { //NOI18N
continue;
}
if ("-Djava.compiler=none".equals(arg)) { //NOI18N
continue;
}
if ("-Xnoagent".equals(arg)) { //NOI18N
continue;
}
if (arg.startsWith("-Xrunjdwp")) { //NOI18N
continue;
}
if (arg.equals("-agentlib:jdwp")) { //NOI18N
continue;
}
if (arg.startsWith("-agentlib:jdwp=")) { //NOI18N
continue;
}
if (arg.trim().length() == 0) {
continue;
}
toRet.add(arg);
}
return toRet;
}
/**
* used by quickrun configuration.
* @param argline
* @return
*/
public static String[] splitAll(String argline, boolean filterClassPath) {
String jvm = argline == null ? null : splitJVMParams(argline, false);
String mainClazz = argline == null ? null : splitMainClass(argline);
String args = argline == null ? null : splitParams(argline);
if (filterClassPath && jvm != null && jvm.contains("-classpath %classpath")) {
jvm = jvm.replace("-classpath %classpath", "");
}
if (mainClazz != null && mainClazz.equals("${packageClassName}")) {
mainClazz = "";
}
return new String[] {
(jvm != null ? jvm : ""),
(mainClazz != null ? mainClazz : ""),
(args != null ? args : "")
};
}
public static String splitJVMParams(String line) {
return splitJVMParams(line, false);
}
@NonNull
public static String splitJVMParams(String line, boolean newLines) {
PropertySplitter ps = new PropertySplitter(line);
ps.setSeparator(' '); //NOI18N
String s = ps.nextPair();
String jvms = ""; //NOI18N
while (s != null) {
if (s.startsWith("-") || /* #199411 */s.startsWith("\"-") || s.contains("%classpath")) { //NOI18N
if(s.contains("%classpath")) {
jvms = jvms + " " + s;
} else {
jvms = jvms + (jvms.isEmpty() ? "" : (newLines ? "\n" : " ")) + s;
}
} else if (s.equals(PACKAGE_CLASS_NAME_TOKEN) || s.equals(RUN_MAIN_CLASS_TOKEN) || s.matches("[\\w]+[\\.]{0,1}[\\w\\.]*")) { //NOI18N
break;
} else {
jvms = jvms + " " + s;
}
s = ps.nextPair();
}
return jvms.trim();
}
@NonNull
public static String splitMainClass(String line) {
PropertySplitter ps = new PropertySplitter(line);
ps.setSeparator(' '); //NOI18N
String s = ps.nextPair();
while (s != null) {
if (s.startsWith("-") || s.contains("%classpath")) { //NOI18N
s = ps.nextPair();
continue;
} else if (s.equals(PACKAGE_CLASS_NAME_TOKEN) || s.equals(RUN_MAIN_CLASS_TOKEN) || s.matches("[\\w]+[\\.]{0,1}[\\w\\.]*")) { //NOI18N
return s;
} else {
Logger.getLogger(RunJarPanel.class.getName()).fine("failed splitting main class from=" + line); //NOI18N
}
s = ps.nextPair();
}
return ""; //NOI18N
}
@NonNull
public static String splitParams(String line) {
int argsIndex = line.indexOf(RunJarStartupArgs.USER_PROGRAM_ARGS_MARKER);
if (argsIndex > -1) {
return line.substring(argsIndex + RunJarStartupArgs.USER_PROGRAM_ARGS_MARKER.length()).trim();
}
String main = splitMainClass(line);
if (main.isEmpty()) {
return "";
}
int i = line.indexOf(main);
if (i > -1) {
return line.substring(i + main.length()).trim();
}
return ""; //NOI18N
}
/**
* Splits the line into sequence of arguments, respects quoting.
* @param line the line as a string
* @return arguments in an iterable
*/
public static Iterable<String> propertySplitter(String line) {
return propertySplitter(line, true);
}
public static Iterable<String> propertySplitter(String line, boolean outputQuotes) {
class SplitIt implements Iterator<String> {
private final PropertySplitter spl = new PropertySplitter(line);
private String nextPair;
public SplitIt() {
spl.setSeparator(' ');
spl.setOutputQuotes(outputQuotes);
}
@Override
public boolean hasNext() {
if (nextPair == null) {
nextPair = spl.nextPair();
}
return nextPair != null;
}
@Override
public String next() {
String s;
if (nextPair == null) {
nextPair = spl.nextPair();
}
s = nextPair;
nextPair = null;
if (s != null) {
return s;
} else {
throw new NoSuchElementException();
}
}
}
return new Iterable<String>() {
@Override
public Iterator<String> iterator() {
return new SplitIt();
}
};
}
}