BuiltinShellCommandRegistryFactory.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.apache.maven.cling.invoker.mvnsh.builtin;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.apache.maven.api.Lifecycle;
import org.apache.maven.api.cli.InvokerException;
import org.apache.maven.api.cli.ParserRequest;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.services.LifecycleRegistry;
import org.apache.maven.api.services.LookupException;
import org.apache.maven.cling.invoker.LookupContext;
import org.apache.maven.cling.invoker.mvn.MavenInvoker;
import org.apache.maven.cling.invoker.mvn.MavenParser;
import org.apache.maven.cling.invoker.mvnenc.EncryptInvoker;
import org.apache.maven.cling.invoker.mvnenc.EncryptParser;
import org.apache.maven.cling.invoker.mvnenc.Goal;
import org.apache.maven.cling.invoker.mvnsh.ShellCommandRegistryFactory;
import org.apache.maven.cling.invoker.mvnup.UpgradeInvoker;
import org.apache.maven.cling.invoker.mvnup.UpgradeParser;
import org.apache.maven.impl.util.Os;
import org.jline.builtins.Completers;
import org.jline.console.CmdDesc;
import org.jline.console.CommandInput;
import org.jline.console.CommandMethods;
import org.jline.console.CommandRegistry;
import org.jline.console.impl.JlineCommandRegistry;
import org.jline.reader.Completer;
import org.jline.reader.impl.completer.ArgumentCompleter;
import org.jline.reader.impl.completer.StringsCompleter;
import static java.util.Objects.requireNonNull;
@Named("builtin")
@Singleton
public class BuiltinShellCommandRegistryFactory implements ShellCommandRegistryFactory {
@Override
public CommandRegistry createShellCommandRegistry(LookupContext context) {
return new BuiltinShellCommandRegistry(context);
}
private static class BuiltinShellCommandRegistry extends JlineCommandRegistry implements AutoCloseable {
private final LookupContext shellContext;
private final MavenInvoker shellMavenInvoker;
private final MavenParser mavenParser;
private final EncryptInvoker shellEncryptInvoker;
private final EncryptParser encryptParser;
private final UpgradeInvoker shellUpgradeInvoker;
private final UpgradeParser upgradeParser;
private BuiltinShellCommandRegistry(LookupContext shellContext) {
this.shellContext = requireNonNull(shellContext, "shellContext");
this.shellMavenInvoker = new MavenInvoker(shellContext.invokerRequest.lookup(), contextCopier());
this.mavenParser = new MavenParser();
this.shellEncryptInvoker = new EncryptInvoker(shellContext.invokerRequest.lookup(), contextCopier());
this.encryptParser = new EncryptParser();
this.shellUpgradeInvoker = new UpgradeInvoker(shellContext.invokerRequest.lookup(), contextCopier());
this.upgradeParser = new UpgradeParser();
Map<String, CommandMethods> commandExecute = new HashMap<>();
commandExecute.put("!", new CommandMethods(this::shell, this::defaultCompleter));
commandExecute.put("cd", new CommandMethods(this::cd, this::cdCompleter));
commandExecute.put("pwd", new CommandMethods(this::pwd, this::defaultCompleter));
commandExecute.put("mvn", new CommandMethods(this::mvn, this::mvnCompleter));
commandExecute.put("mvnenc", new CommandMethods(this::mvnenc, this::mvnencCompleter));
commandExecute.put("mvnup", new CommandMethods(this::mvnup, this::mvnupCompleter));
registerCommands(commandExecute);
}
private Consumer<LookupContext> contextCopier() {
return result -> {
result.logger = shellContext.logger;
result.loggerFactory = shellContext.loggerFactory;
result.slf4jConfiguration = shellContext.slf4jConfiguration;
result.loggerLevel = shellContext.loggerLevel;
result.coloredOutput = shellContext.coloredOutput;
result.terminal = shellContext.terminal;
result.writer = shellContext.writer;
result.installationSettingsPath = shellContext.installationSettingsPath;
result.projectSettingsPath = shellContext.projectSettingsPath;
result.userSettingsPath = shellContext.userSettingsPath;
result.interactive = shellContext.interactive;
result.localRepositoryPath = shellContext.localRepositoryPath;
result.effectiveSettings = shellContext.effectiveSettings;
result.containerCapsule = shellContext.containerCapsule;
result.lookup = shellContext.lookup;
result.eventSpyDispatcher = shellContext.eventSpyDispatcher;
};
}
@Override
public void close() throws Exception {
shellMavenInvoker.close();
shellEncryptInvoker.close();
shellUpgradeInvoker.close();
}
@Override
public List<String> commandInfo(String command) {
return List.of();
}
@Override
public CmdDesc commandDescription(List<String> args) {
return null;
}
@Override
public String name() {
return "Builtin Maven Shell commands";
}
private void shell(CommandInput input) {
if (input.args().length > 0) {
try {
ProcessBuilder builder = new ProcessBuilder();
List<String> processArgs = new ArrayList<>();
if (Os.IS_WINDOWS) {
processArgs.add("cmd.exe");
processArgs.add("/c");
} else {
processArgs.add("sh");
processArgs.add("-c");
}
processArgs.add(String.join(" ", Arrays.asList(input.args())));
builder.command(processArgs);
builder.directory(shellContext.cwd.get().toFile());
Process process = builder.start();
Thread out = new Thread(new StreamGobbler(process.getInputStream(), shellContext.writer));
Thread err = new Thread(new StreamGobbler(process.getErrorStream(), shellContext.logger::error));
out.start();
err.start();
int exitCode = process.waitFor();
out.join();
err.join();
if (exitCode != 0) {
shellContext.logger.error("Shell command exited with code " + exitCode);
}
} catch (Exception e) {
saveException(e);
}
}
}
private void cd(CommandInput input) {
try {
if (input.args().length == 1) {
shellContext.cwd.change(input.args()[0]);
} else {
shellContext.logger.error("Command accepts only one argument");
}
} catch (Exception e) {
saveException(e);
}
}
private List<Completer> cdCompleter(String name) {
return List.of(new ArgumentCompleter(new Completers.DirectoriesCompleter(shellContext.cwd)));
}
private void pwd(CommandInput input) {
try {
shellContext.writer.accept(shellContext.cwd.get().toString());
} catch (Exception e) {
saveException(e);
}
}
private void mvn(CommandInput input) {
try {
shellMavenInvoker.invoke(mavenParser.parseInvocation(
ParserRequest.mvn(input.args(), shellContext.invokerRequest.messageBuilderFactory())
.cwd(shellContext.cwd.get())
.build()));
} catch (InvokerException.ExitException e) {
shellContext.logger.error("mvn command exited with exit code " + e.getExitCode());
} catch (Exception e) {
saveException(e);
}
}
private List<Completer> mvnCompleter(String name) {
List<String> names;
try {
List<String> phases = shellContext.lookup.lookup(LifecycleRegistry.class).stream()
.flatMap(Lifecycle::allPhases)
.map(Lifecycle.Phase::name)
.toList();
// TODO: add goals dynamically
List<String> goals = List.of("wrapper:wrapper");
names = Stream.concat(phases.stream(), goals.stream()).toList();
} catch (LookupException e) {
names = List.of(
"clean",
"validate",
"compile",
"test",
"package",
"verify",
"install",
"deploy",
"wrapper:wrapper");
}
return List.of(new ArgumentCompleter(new StringsCompleter(names)));
}
private void mvnenc(CommandInput input) {
try {
shellEncryptInvoker.invoke(encryptParser.parseInvocation(
ParserRequest.mvnenc(input.args(), shellContext.invokerRequest.messageBuilderFactory())
.cwd(shellContext.cwd.get())
.build()));
} catch (InvokerException.ExitException e) {
shellContext.logger.error("mvnenc command exited with exit code " + e.getExitCode());
} catch (Exception e) {
saveException(e);
}
}
private List<Completer> mvnencCompleter(String name) {
return List.of(new ArgumentCompleter(new StringsCompleter(
shellContext.lookup.lookupMap(Goal.class).keySet())));
}
private void mvnup(CommandInput input) {
try {
shellUpgradeInvoker.invoke(upgradeParser.parseInvocation(
ParserRequest.mvnup(input.args(), shellContext.invokerRequest.messageBuilderFactory())
.cwd(shellContext.cwd.get())
.build()));
} catch (InvokerException.ExitException e) {
shellContext.logger.error("mvnup command exited with exit code " + e.getExitCode());
} catch (Exception e) {
saveException(e);
}
}
private List<Completer> mvnupCompleter(String name) {
return List.of(new ArgumentCompleter(new StringsCompleter(shellContext
.lookup
.lookupMap(org.apache.maven.cling.invoker.mvnup.Goal.class)
.keySet())));
}
}
private static class StreamGobbler implements Runnable {
private final InputStream inputStream;
private final Consumer<String> consumer;
private StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}
@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
.lines()
.forEach(consumer);
}
}
}