Refactor packages and artifactIds to prepare for official migration to spring-projects repo.
Remove usage of component scan in favor of auto-conf Fixes #61
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A result to be handled by the {@link ResultHandler} when no command could be mapped to user input
|
||||
*/
|
||||
public class CommandNotFound extends RuntimeException {
|
||||
|
||||
private final List<String> words;
|
||||
|
||||
public CommandNotFound(List<String> words) {
|
||||
this.words = words;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return String.format("No command found for '%s'", words.stream().collect(Collectors.joining(" ")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementing this interface allows sub-systems (such as the {@literal help} command) to
|
||||
* discover available commands.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public interface CommandRegistry {
|
||||
|
||||
|
||||
/**
|
||||
* Return the mapping from command trigger keywords to implementation.
|
||||
*/
|
||||
public Map<String, MethodTarget> listCommands();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import org.jline.reader.ParsedLine;
|
||||
|
||||
/**
|
||||
* An extension of {@link ParsedLine} that, being aware of the quoting and escaping rules
|
||||
* of the {@link org.jline.reader.Parser} that produced it, knows if and how a completion candidate
|
||||
* should be escaped/quoted.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CompletingParsedLine {
|
||||
|
||||
public CharSequence emit(CharSequence candidate);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Represents the buffer context in which completion was triggered.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class CompletionContext {
|
||||
|
||||
private final List<String> words;
|
||||
|
||||
private final int wordIndex;
|
||||
|
||||
private final int position;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param words words in the buffer, excluding words for the command name
|
||||
* @param wordIndex the index of the word the cursor is in
|
||||
* @param position the position inside the current word where the cursor is
|
||||
*/
|
||||
public CompletionContext(List<String> words, int wordIndex, int position) {
|
||||
this.words = words;
|
||||
this.wordIndex = wordIndex;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public List<String> getWords() {
|
||||
return words;
|
||||
}
|
||||
|
||||
public int getWordIndex() {
|
||||
return wordIndex;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public String upToCursor() {
|
||||
String start = words.subList(0, wordIndex).stream().collect(Collectors.joining(" "));
|
||||
if (wordIndex < words.size()) {
|
||||
if (!start.isEmpty()) {
|
||||
start += " ";
|
||||
}
|
||||
start += currentWord().substring(0, position);
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the whole word the cursor is in, or {@code null} if the cursor is past the last word.
|
||||
*/
|
||||
public String currentWord() {
|
||||
return wordIndex >= 0 && wordIndex < words.size() ? words.get(wordIndex) : null;
|
||||
}
|
||||
|
||||
public String currentWordUpToCursor() {
|
||||
String currentWord = currentWord();
|
||||
return currentWord != null ? currentWord.substring(0, getPosition()) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy of this context, as if the first {@literal nbWords} were not present
|
||||
*/
|
||||
public CompletionContext drop(int nbWords) {
|
||||
return new CompletionContext(new ArrayList<String>(words.subList(nbWords, words.size())), wordIndex-nbWords, position);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
/**
|
||||
* Represents a proposal for TAB completion, made not only of the text to append, but also metadata about the proposal.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class CompletionProposal {
|
||||
|
||||
/**
|
||||
* The actual value of the proposal.
|
||||
*/
|
||||
private String value;
|
||||
|
||||
/**
|
||||
* The text displayed while the proposal is being considered.
|
||||
*/
|
||||
private String displayText;
|
||||
|
||||
/**
|
||||
* The description for the proposal.
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* The category of the proposal, which may be used to group proposals together.
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* Whether the proposal should bypass escaping and quoting rules. This is useful for command proposals, which can
|
||||
* appear as true multi-word Strings.
|
||||
*/
|
||||
private boolean dontQuote = false;
|
||||
|
||||
public CompletionProposal(String value) {
|
||||
this.value = this.displayText = value;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public CompletionProposal value(String value) {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String displayText() {
|
||||
return displayText;
|
||||
}
|
||||
|
||||
public CompletionProposal displayText(String displayText) {
|
||||
this.displayText = displayText;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String description() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public CompletionProposal description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String category() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public CompletionProposal category(String category) {
|
||||
this.category = category;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CompletionProposal dontQuote(boolean dontQuote) {
|
||||
this.dontQuote = dontQuote;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean dontQuote() {
|
||||
return dontQuote;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
/**
|
||||
* This exception, when thrown and caught, will ask the shell to gracefully shutdown.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ExitRequest extends RuntimeException {
|
||||
|
||||
private final int code;
|
||||
|
||||
public ExitRequest() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
public ExitRequest(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* The exit code to be returned when the process exits.
|
||||
*/
|
||||
public int status() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the input buffer to the shell.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public interface Input {
|
||||
|
||||
Input EMPTY = () -> "";
|
||||
|
||||
/**
|
||||
* Return the input as entered by the user.
|
||||
*/
|
||||
String rawText();
|
||||
|
||||
/**
|
||||
* Return the input as a list of parsed "words", having split the raw input according
|
||||
* to parsing rules (for example, handling quoted portions of the readInput as a single
|
||||
* "word")
|
||||
*/
|
||||
default List<String> words() {return "".equals(rawText()) ? Collections.emptyList() : Arrays.asList(rawText().split(" "));}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Represents a shell command behavior, <em>i.e.</em> code to be executed when a command is requested.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class MethodTarget {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Object bean;
|
||||
|
||||
private final String help;
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help) {
|
||||
Assert.notNull(method, "Method cannot be null");
|
||||
Assert.notNull(bean, "Bean cannot be null");
|
||||
Assert.hasText(help, "Help cannot be blank");
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
this.method = method;
|
||||
this.bean = bean;
|
||||
this.help = help;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, String help) {
|
||||
Set<Method> found = new HashSet<>();
|
||||
ReflectionUtils.doWithMethods(bean.getClass(), found::add, m -> m.getName().equals(name));
|
||||
if (found.size() != 1) {
|
||||
throw new IllegalArgumentException(String.format("Could not find unique method named '%s' on object of class %s. Found %s",
|
||||
name, bean.getClass(), found));
|
||||
}
|
||||
return new MethodTarget(found.iterator().next(), bean, help);
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public Object getBean() {
|
||||
return bean;
|
||||
}
|
||||
|
||||
public String getHelp() {
|
||||
return help;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
MethodTarget that = (MethodTarget) o;
|
||||
|
||||
if (!method.equals(that.method)) return false;
|
||||
if (!bean.equals(that.bean)) return false;
|
||||
return help.equals(that.help);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = method.hashCode();
|
||||
result = 31 * result + bean.hashCode();
|
||||
result = 31 * result + help.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return method.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2015-2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Strategy interface for discovering commands.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Camilo Gonzalez
|
||||
*/
|
||||
public interface MethodTargetResolver {
|
||||
|
||||
/**
|
||||
* Return a mapping from {@literal <command keyword(s)>} to actual behavior.
|
||||
*/
|
||||
public Map<String, MethodTarget> resolve();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
|
||||
/**
|
||||
* Encapsulates information about a shell invokable method parameter, so that it can be documented.
|
||||
*
|
||||
* <p>Instances of this class are constructed by {@link ParameterResolver#describe(MethodParameter)}.</p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ParameterDescription {
|
||||
|
||||
/**
|
||||
* The original method parameter this is describing.
|
||||
*/
|
||||
private final MethodParameter parameter;
|
||||
|
||||
/**
|
||||
* A string representation of the type of the parameter.
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
/**
|
||||
* A string representation of the parameter, as it should appear in a parameter list.
|
||||
* If not provided, this is derived from the parameter type.
|
||||
*/
|
||||
private String formal;
|
||||
|
||||
/**
|
||||
* A string representation of the default value (if the option is left out entirely) for the parameter, if any.
|
||||
*/
|
||||
private Optional<String> defaultValue = Optional.empty();
|
||||
|
||||
/**
|
||||
* A string representation of the default value for this parameter, if it can be used as a mere flag (<em>e.g.</em>
|
||||
* {@literal --force} without a value, being an equivalent to {@literal --force true}).
|
||||
* <p>{@literal Optional.empty()} (the default) means that this parameter cannot be used as a flag.</p>
|
||||
*/
|
||||
private Optional<String> defaultValueWhenFlag = Optional.empty();
|
||||
|
||||
/**
|
||||
* The list of 'keys' that can be used to specify this parameter, if any.
|
||||
*/
|
||||
private List<String> keys = Collections.emptyList();
|
||||
|
||||
/**
|
||||
* Depending on the {@link ParameterResolver}, whether keys are mandatory to identify this parameter.
|
||||
*/
|
||||
private boolean mandatoryKey = true;
|
||||
|
||||
/**
|
||||
* A short description of this parameter.
|
||||
*/
|
||||
private String help = "";
|
||||
|
||||
public ParameterDescription(MethodParameter parameter, String type) {
|
||||
this.parameter = parameter;
|
||||
this.type = type;
|
||||
this.formal = type;
|
||||
}
|
||||
|
||||
public static ParameterDescription outOf(MethodParameter parameter) {
|
||||
Class<?> type = parameter.getParameterType();
|
||||
return new ParameterDescription(parameter, Utils.unCamelify(type.getSimpleName()));
|
||||
}
|
||||
|
||||
public ParameterDescription help(String help) {
|
||||
this.help = help;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean mandatoryKey() {
|
||||
return mandatoryKey;
|
||||
}
|
||||
|
||||
public List<String> keys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public Optional<String> defaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public ParameterDescription defaultValue(String defaultValue) {
|
||||
this.defaultValue = Optional.ofNullable(defaultValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParameterDescription whenFlag(String defaultValue) {
|
||||
this.defaultValueWhenFlag = Optional.of(defaultValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParameterDescription keys(List<String> keys) {
|
||||
this.keys = keys;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParameterDescription mandatoryKey(boolean mandatoryKey) {
|
||||
this.mandatoryKey = mandatoryKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public String type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String formal() {
|
||||
return formal;
|
||||
}
|
||||
|
||||
public String help() {
|
||||
return help;
|
||||
}
|
||||
|
||||
public Optional<String> defaultValueWhenFlag() {
|
||||
return defaultValueWhenFlag;
|
||||
}
|
||||
|
||||
public ParameterDescription formal(String formal) {
|
||||
this.formal = formal;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s %s", keys.isEmpty() ? "" : keys().iterator().next(), formal());
|
||||
}
|
||||
|
||||
public MethodParameter parameter() {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ParameterDescription that = (ParameterDescription) o;
|
||||
return mandatoryKey == that.mandatoryKey &&
|
||||
Objects.equals(parameter, that.parameter) &&
|
||||
Objects.equals(type, that.type) &&
|
||||
Objects.equals(formal, that.formal) &&
|
||||
Objects.equals(defaultValue, that.defaultValue) &&
|
||||
Objects.equals(defaultValueWhenFlag, that.defaultValueWhenFlag) &&
|
||||
Objects.equals(keys, that.keys) &&
|
||||
Objects.equals(help, that.help);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(parameter, type, formal, defaultValue, defaultValueWhenFlag, keys, mandatoryKey, help);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
/**
|
||||
* Thrown by a {@link ParameterResolver} when a parameter that should have been set has been left out altogether.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ParameterMissingResolutionException extends RuntimeException {
|
||||
|
||||
private final ParameterDescription parameterDescription;
|
||||
|
||||
public ParameterMissingResolutionException(ParameterDescription parameterDescription) {
|
||||
this.parameterDescription = parameterDescription;
|
||||
}
|
||||
|
||||
public ParameterDescription getParameterDescription() {
|
||||
return parameterDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return String.format("Parameter '%s' should be specified", parameterDescription);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible, once the command has been identified, of transforming the textual
|
||||
* input to an actual parameter object.
|
||||
*/
|
||||
public interface ParameterResolver {
|
||||
|
||||
/**
|
||||
* Should return true if this resolver recognizes the given method parameter (<em>e.g.</em> it
|
||||
* has the correct annotation or the correct type).
|
||||
*/
|
||||
boolean supports(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Turn the given textual input into an actual object, maybe using some conversion or lookup mechanism.
|
||||
*/
|
||||
ValueResult resolve(MethodParameter methodParameter, List<String> words);
|
||||
|
||||
/**
|
||||
* Describe a supported parameter, so that integrated help can be generated.
|
||||
* <p>Typical implementations will return a one element stream result, but some may return several (for
|
||||
* example if binding several words to a POJO).</p>
|
||||
*/
|
||||
Stream<ParameterDescription> describe(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Invoked during TAB completion. If the {@link CompletionContext} can be interpreted as the start
|
||||
* of a supported {@link MethodParameter} value, one or several proposals should be returned.
|
||||
*/
|
||||
List<CompletionProposal> complete(MethodParameter parameter, CompletionContext context);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
/**
|
||||
* Implementations know how to deal with results of method invocations, whether normal results or exceptions thrown.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public interface ResultHandler<T> {
|
||||
|
||||
/**
|
||||
* Deal with some method execution result, whether it was the normal return value, or some kind
|
||||
* of {@link Throwable}.
|
||||
*/
|
||||
void handleResult(T result);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.executable.ExecutableValidator;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Main class implementing a shell loop.
|
||||
*
|
||||
* <p>Given some textual input, locate the {@link MethodTarget} to invoke and {@link ResultHandler#handleResult(Object) handle}
|
||||
* the result.</p>
|
||||
*
|
||||
* <p>Also provides hooks for code completion</p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class Shell implements CommandRegistry {
|
||||
|
||||
private final InputProvider inputProvider;
|
||||
|
||||
private final ResultHandler resultHandler;
|
||||
|
||||
@Autowired
|
||||
protected ApplicationContext applicationContext;
|
||||
|
||||
protected Map<String, MethodTarget> methodTargets = new HashMap<>();
|
||||
|
||||
@Autowired
|
||||
protected List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Marker object to distinguish unresolved arguments from {@code null}, which is a valid value.
|
||||
*/
|
||||
protected static final Object UNRESOLVED = new Object();
|
||||
|
||||
public Shell(InputProvider inputProvider, ResultHandler resultHandler) {
|
||||
this.inputProvider = inputProvider;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> listCommands() {
|
||||
return methodTargets;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void gatherMethodTargets() throws Exception {
|
||||
for (MethodTargetResolver resolver : applicationContext.getBeansOfType(MethodTargetResolver.class).values()) {
|
||||
methodTargets.putAll(resolver.resolve());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main program loop: acquire input, try to match it to a command and evaluate. Repeat until a
|
||||
* {@link ResultHandler} causes the process to exit.
|
||||
*/
|
||||
public void run() throws IOException {
|
||||
while (true) {
|
||||
Input input;
|
||||
try {
|
||||
input = inputProvider.readInput();
|
||||
}
|
||||
catch (Exception e) {
|
||||
resultHandler.handleResult(e);
|
||||
continue;
|
||||
}
|
||||
if (noInput(input)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
String line = input.words().stream().collect(Collectors.joining(" ")).trim();
|
||||
String command = findLongestCommand(line);
|
||||
|
||||
List<String> words = input.words();
|
||||
Object result;
|
||||
if (command != null) {
|
||||
MethodTarget methodTarget = methodTargets.get(command);
|
||||
List<String> wordsForArgs = wordsForArguments(command, words);
|
||||
Method method = methodTarget.getMethod();
|
||||
|
||||
try {
|
||||
Object[] args = resolveArgs(method, wordsForArgs);
|
||||
validateArgs(args, methodTarget);
|
||||
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
||||
}
|
||||
catch (Exception e) {
|
||||
result = e;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result = new CommandNotFound(words);
|
||||
}
|
||||
resultHandler.handleResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the parsed input ends up being empty (<em>e.g.</em> hitting ENTER on an empty line or blank space)
|
||||
*/
|
||||
private boolean noInput(Input input) {
|
||||
return input.words().isEmpty()
|
||||
|| (input.words().size() == 1 && input.words().get(0).trim().isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of words to be considered for argument resolving. Drops the first N words used for the
|
||||
* command, as well as an optional empty word at the end of the list (which may be present if user added spaces
|
||||
* before submitting the buffer)
|
||||
*/
|
||||
private List<String> wordsForArguments(String command, List<String> words) {
|
||||
int wordsUsedForCommandKey = command.split(" ").length;
|
||||
List<String> args = words.subList(wordsUsedForCommandKey, words.size());
|
||||
int last = args.size() - 1;
|
||||
if (last >= 0 && "".equals(args.get(last))) {
|
||||
args.remove(last);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather completion proposals given some (incomplete) input the user has already typed in.
|
||||
* When and how this method is invoked is implementation specific and decided by the actual user interface.
|
||||
*/
|
||||
public List<CompletionProposal> complete(CompletionContext context) {
|
||||
|
||||
String prefix = context.upToCursor();
|
||||
|
||||
List<CompletionProposal> candidates = new ArrayList<>();
|
||||
candidates.addAll(commandsStartingWith(prefix));
|
||||
|
||||
String best = findLongestCommand(prefix);
|
||||
if (best != null) {
|
||||
CompletionContext argsContext = context.drop(best.split(" ").length);
|
||||
// Try to complete arguments
|
||||
MethodTarget methodTarget = methodTargets.get(best);
|
||||
Method method = methodTarget.getMethod();
|
||||
Arrays.stream(method.getParameters())
|
||||
.map(Utils::createMethodParameter)
|
||||
.flatMap(mp -> findResolver(mp).complete(mp, argsContext).stream())
|
||||
.forEach(candidates::add);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private List<CompletionProposal> commandsStartingWith(String prefix) {
|
||||
return methodTargets.entrySet().stream()
|
||||
.filter(e -> e.getKey().startsWith(prefix))
|
||||
.map(e -> toCommandProposal(e.getKey(), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CompletionProposal toCommandProposal(String command, MethodTarget methodTarget) {
|
||||
return new CompletionProposal(command)
|
||||
.dontQuote(true)
|
||||
.category("Available commands")
|
||||
.description(methodTarget.getHelp());
|
||||
}
|
||||
|
||||
private void validateArgs(Object[] args, MethodTarget methodTarget) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] == UNRESOLVED) {
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(methodTarget.getMethod(), i);
|
||||
throw new IllegalStateException("Could not resolve " + methodParameter);
|
||||
}
|
||||
}
|
||||
ExecutableValidator executableValidator = Validation
|
||||
.buildDefaultValidatorFactory().getValidator().forExecutables();
|
||||
Set<ConstraintViolation<Object>> constraintViolations = executableValidator.validateParameters(methodTarget.getBean(),
|
||||
methodTarget.getMethod(),
|
||||
args);
|
||||
if (constraintViolations.size() > 0) {
|
||||
System.out.println(constraintViolations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use all known {@link ParameterResolver}s to try to compute a value for each parameter of the method to
|
||||
* invoke.
|
||||
* @param method the method for which parameters should be computed
|
||||
* @param wordsForArgs the list of 'words' that should be converted to parameter values.
|
||||
* May include markers for passing parameters 'by name'
|
||||
* @return an array containing resolved parameter values, or {@link #UNRESOLVED} for parameters that could not be
|
||||
* resolved
|
||||
*/
|
||||
private Object[] resolveArgs(Method method, List<String> wordsForArgs) {
|
||||
Parameter[] parameters = method.getParameters();
|
||||
Object[] args = new Object[parameters.length];
|
||||
Arrays.fill(args, UNRESOLVED);
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(method, i);
|
||||
args[i] = findResolver(methodParameter).resolve(methodParameter, wordsForArgs).resolvedValue();
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private ParameterResolver findResolver(MethodParameter parameter) {
|
||||
return parameterResolvers.stream()
|
||||
.filter(resolver -> resolver.supports(parameter))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("resolver not found"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the longest command that can be matched as first word(s) in the given buffer.
|
||||
*
|
||||
* @return a valid command name, or {@literal null} if none matched
|
||||
*/
|
||||
private String findLongestCommand(String prefix) {
|
||||
String result = methodTargets.keySet().stream()
|
||||
.filter(prefix::startsWith)
|
||||
.reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2);
|
||||
return "".equals(result) ? null : result;
|
||||
}
|
||||
|
||||
public interface InputProvider {
|
||||
/**
|
||||
* Return text entered by user to invoke commands.
|
||||
*/
|
||||
Input readInput();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.shell.jline.JLineShell;
|
||||
import org.springframework.shell.result.ResultHandlerConfig;
|
||||
|
||||
/**
|
||||
* Creates supporting beans for running the Shell
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan(basePackageClasses = ResultHandlerConfig.class)
|
||||
@Import(JLineShell.class)
|
||||
public class SpringShellAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ConversionService.class)
|
||||
public ConversionService conversionService() {
|
||||
return new DefaultConversionService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApplicationRunner applicationRunner(Shell shell) {
|
||||
return new ApplicationRunner() {
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
shell.run();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
/**
|
||||
* Thrown during parameter resolution, when a parameter has been identified, but could not be correctly resolved.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class UnfinishedParameterResolutionException extends RuntimeException {
|
||||
|
||||
private final ParameterDescription parameterDescription;
|
||||
|
||||
private final CharSequence input;
|
||||
|
||||
public UnfinishedParameterResolutionException(ParameterDescription parameterDescription, CharSequence input) {
|
||||
this.parameterDescription = parameterDescription;
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
public ParameterDescription getParameterDescription() {
|
||||
return parameterDescription;
|
||||
}
|
||||
|
||||
public CharSequence getInput() {
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return String.format("Error trying to resolve '%s' using [%s]", parameterDescription, input);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
|
||||
/**
|
||||
* Some text utilities.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class Utils {
|
||||
|
||||
/**
|
||||
* Turn CamelCaseText into gnu-style-lowercase.
|
||||
*/
|
||||
public static String unCamelify(CharSequence original) {
|
||||
StringBuilder result = new StringBuilder(original.length());
|
||||
boolean wasLowercase = false;
|
||||
for (int i = 0; i < original.length(); i++) {
|
||||
char ch = original.charAt(i);
|
||||
if (Character.isUpperCase(ch) && wasLowercase) {
|
||||
result.append('-');
|
||||
}
|
||||
wasLowercase = Character.isLowerCase(ch);
|
||||
result.append(Character.toLowerCase(ch));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from JDK {@link Parameter} to Spring {@link MethodParameter}.
|
||||
*/
|
||||
public static MethodParameter createMethodParameter(Parameter parameter) {
|
||||
Parameter[] parameters = parameter.getDeclaringExecutable().getParameters();
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
if (parameters[i].equals(parameter)) {
|
||||
return createMethodParameter(parameter.getDeclaringExecutable(), i);
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Can't happen");
|
||||
}
|
||||
/**
|
||||
* Return a properly initialized MethodParameter for the given executable and index.
|
||||
*/
|
||||
public static MethodParameter createMethodParameter(Executable executable, int i) {
|
||||
MethodParameter methodParameter;
|
||||
if (executable instanceof Method) {
|
||||
methodParameter = new MethodParameter((Method) executable, i);
|
||||
} else if (executable instanceof Constructor){
|
||||
methodParameter = new MethodParameter((Constructor) executable, i);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported Executable: " + executable);
|
||||
}
|
||||
methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
|
||||
return methodParameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return MethodParameters for each parameter of the given method/constructor.
|
||||
*/
|
||||
public static Stream<MethodParameter> createMethodParameters(Executable executable) {
|
||||
return IntStream.range(0, executable.getParameterCount())
|
||||
.mapToObj(i -> createMethodParameter(executable, i));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.naming.spi.ResolveResult;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
|
||||
/**
|
||||
* A {@link ResolveResult} for a successful {@link ParameterResolver#resolve} operation.
|
||||
*
|
||||
* @author Camilo Gonzalez
|
||||
*/
|
||||
public class ValueResult {
|
||||
|
||||
private final MethodParameter methodParameter;
|
||||
|
||||
private final Object resolvedValue;
|
||||
|
||||
private final BitSet wordsUsed;
|
||||
|
||||
private final BitSet wordsUsedForValue;
|
||||
|
||||
public ValueResult(MethodParameter methodParameter, Object resolvedValue) {
|
||||
this(methodParameter, resolvedValue, new BitSet(), new BitSet());
|
||||
}
|
||||
|
||||
public ValueResult(MethodParameter methodParameter, Object resolvedValue, BitSet wordsUsed,
|
||||
BitSet wordsUsedForValue) {
|
||||
|
||||
this.methodParameter = methodParameter;
|
||||
this.resolvedValue = resolvedValue;
|
||||
this.wordsUsed = wordsUsed == null ? new BitSet() : wordsUsed;
|
||||
this.wordsUsedForValue = wordsUsedForValue == null ? new BitSet() : wordsUsedForValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link MethodParameter} that was the target of the {@link ParameterResolver#resolve}
|
||||
* operation.
|
||||
*/
|
||||
public MethodParameter methodParameter() {
|
||||
return methodParameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the resolved value for the {@link MethodParameter} associated with this result.
|
||||
*/
|
||||
public Object resolvedValue() {
|
||||
return resolvedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the full set of words used to resolve the {@link MethodParameter}. This includes
|
||||
* any tags/keys consumed from the input.
|
||||
*/
|
||||
public BitSet wordsUsed() {
|
||||
return wordsUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the full set of words used to resolve the value of this {@link MethodParameter}.
|
||||
*/
|
||||
public BitSet wordsUsedForValue() {
|
||||
return wordsUsedForValue;
|
||||
}
|
||||
|
||||
public List<String> wordsUsed(List<String> words) {
|
||||
return wordsUsed.stream().mapToObj(index -> words.get(index)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<String> wordsUsedForValue(List<String> words) {
|
||||
return wordsUsedForValue.stream().mapToObj(index -> words.get(index)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.jline;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.jline.reader.EOFError;
|
||||
import org.jline.reader.ParsedLine;
|
||||
import org.jline.reader.Parser;
|
||||
|
||||
import org.springframework.shell.CompletingParsedLine;
|
||||
|
||||
/**
|
||||
* Shameful copy-paste of JLine's {@link org.jline.reader.impl.DefaultParser} which
|
||||
* creates {@link CompletingParsedLine}.
|
||||
*
|
||||
* @author Original JLine author
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ExtendedDefaultParser implements Parser {
|
||||
|
||||
private char[] quoteChars = { '\'', '"' };
|
||||
|
||||
private char[] escapeChars = { '\\' };
|
||||
|
||||
private boolean eofOnUnclosedQuote;
|
||||
|
||||
private boolean eofOnEscapedNewLine;
|
||||
|
||||
public void setQuoteChars(final char[] chars) {
|
||||
this.quoteChars = chars;
|
||||
}
|
||||
|
||||
public char[] getQuoteChars() {
|
||||
return this.quoteChars;
|
||||
}
|
||||
|
||||
public void setEscapeChars(final char[] chars) {
|
||||
this.escapeChars = chars;
|
||||
}
|
||||
|
||||
public char[] getEscapeChars() {
|
||||
return this.escapeChars;
|
||||
}
|
||||
|
||||
public void setEofOnUnclosedQuote(boolean eofOnUnclosedQuote) {
|
||||
this.eofOnUnclosedQuote = eofOnUnclosedQuote;
|
||||
}
|
||||
|
||||
public boolean isEofOnUnclosedQuote() {
|
||||
return eofOnUnclosedQuote;
|
||||
}
|
||||
|
||||
public void setEofOnEscapedNewLine(boolean eofOnEscapedNewLine) {
|
||||
this.eofOnEscapedNewLine = eofOnEscapedNewLine;
|
||||
}
|
||||
|
||||
public boolean isEofOnEscapedNewLine() {
|
||||
return eofOnEscapedNewLine;
|
||||
}
|
||||
|
||||
public ParsedLine parse(final String line, final int cursor, ParseContext context) {
|
||||
List<String> words = new LinkedList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
int wordCursor = -1;
|
||||
int wordIndex = -1;
|
||||
int quoteStart = -1;
|
||||
|
||||
for (int i = 0; (line != null) && (i < line.length()); i++) {
|
||||
// once we reach the cursor, set the
|
||||
// position of the selected index
|
||||
if (i == cursor) {
|
||||
wordIndex = words.size();
|
||||
// the position in the current argument is just the
|
||||
// length of the current argument
|
||||
wordCursor = current.length();
|
||||
}
|
||||
|
||||
if (quoteStart < 0 && isQuoteChar(line, i)) {
|
||||
// Start a quote block
|
||||
quoteStart = i;
|
||||
}
|
||||
else if (quoteStart >= 0) {
|
||||
// In a quote block
|
||||
if (line.charAt(quoteStart) == line.charAt(i) && !isEscaped(line, i)) {
|
||||
// End the block; arg could be empty, but that's fine
|
||||
words.add(current.toString());
|
||||
current.setLength(0);
|
||||
quoteStart = -1;
|
||||
}
|
||||
else if (!isEscapeChar(line, i)) {
|
||||
// Take the next character
|
||||
current.append(line.charAt(i));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Not in a quote block
|
||||
if (isDelimiter(line, i)) {
|
||||
if (current.length() > 0) {
|
||||
words.add(current.toString());
|
||||
current.setLength(0); // reset the arg
|
||||
}
|
||||
}
|
||||
else if (!isEscapeChar(line, i)) {
|
||||
current.append(line.charAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current.length() > 0 || cursor == line.length()) {
|
||||
words.add(current.toString());
|
||||
}
|
||||
|
||||
if (cursor == line.length()) {
|
||||
wordIndex = words.size() - 1;
|
||||
wordCursor = words.get(words.size() - 1).length();
|
||||
}
|
||||
|
||||
if (eofOnEscapedNewLine && isEscapeChar(line, line.length() - 1)) {
|
||||
throw new EOFError(-1, -1, "Escaped new line", "newline");
|
||||
}
|
||||
if (eofOnUnclosedQuote && quoteStart >= 0 && context != ParseContext.COMPLETE) {
|
||||
throw new EOFError(-1, -1, "Missing closing quote", line.charAt(quoteStart) == '\'' ? "quote" : "dquote");
|
||||
}
|
||||
|
||||
String openingQuote = quoteStart >= 0 ? line.substring(quoteStart, quoteStart + 1) : null;
|
||||
return new ExtendedArgumentList(line, words, wordIndex, wordCursor, cursor, openingQuote);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specified character is a whitespace parameter. Check to ensure
|
||||
* that the character is not escaped by any of {@link #getQuoteChars}, and is not
|
||||
* escaped by ant of the {@link #getEscapeChars}, and returns true from
|
||||
* {@link #isDelimiterChar}.
|
||||
*
|
||||
* @param buffer The complete command buffer
|
||||
* @param pos The index of the character in the buffer
|
||||
* @return True if the character should be a delimiter
|
||||
*/
|
||||
public boolean isDelimiter(final CharSequence buffer, final int pos) {
|
||||
return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
|
||||
}
|
||||
|
||||
public boolean isQuoted(final CharSequence buffer, final int pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isQuoteChar(final CharSequence buffer, final int pos) {
|
||||
if (pos < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; (quoteChars != null) && (i < quoteChars.length); i++) {
|
||||
if (buffer.charAt(pos) == quoteChars[i]) {
|
||||
return !isEscaped(buffer, pos);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this character is a valid escape char (i.e. one that has not been escaped)
|
||||
*/
|
||||
public boolean isEscapeChar(final CharSequence buffer, final int pos) {
|
||||
if (pos < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; (escapeChars != null) && (i < escapeChars.length); i++) {
|
||||
if (buffer.charAt(pos) == escapeChars[i]) {
|
||||
return !isEscaped(buffer, pos); // escape escape
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a character is escaped (i.e. if the previous character is an escape)
|
||||
*
|
||||
* @param buffer the buffer to check in
|
||||
* @param pos the position of the character to check
|
||||
* @return true if the character at the specified position in the given buffer is an
|
||||
* escape character and the character immediately preceding it is not an escape
|
||||
* character.
|
||||
*/
|
||||
public boolean isEscaped(final CharSequence buffer, final int pos) {
|
||||
if (pos <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isEscapeChar(buffer, pos - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the character at the specified position if a delimiter. This method
|
||||
* will only be called if the character is not enclosed in any of the
|
||||
* {@link #getQuoteChars}, and is not escaped by ant of the {@link #getEscapeChars}.
|
||||
* To perform escaping manually, override {@link #isDelimiter} instead.
|
||||
*/
|
||||
public boolean isDelimiterChar(CharSequence buffer, int pos) {
|
||||
return Character.isWhitespace(buffer.charAt(pos));
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a delimited buffer.
|
||||
*
|
||||
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
|
||||
*/
|
||||
public class ExtendedArgumentList implements ParsedLine, CompletingParsedLine {
|
||||
private final String line;
|
||||
|
||||
private final List<String> words;
|
||||
|
||||
private final int wordIndex;
|
||||
|
||||
private final int wordCursor;
|
||||
|
||||
private final int cursor;
|
||||
|
||||
private final String openingQuote;
|
||||
|
||||
public ExtendedArgumentList(final String line, final List<String> words, final int wordIndex,
|
||||
final int wordCursor, final int cursor, final String openingQuote) {
|
||||
this.line = line;
|
||||
this.words = Collections.unmodifiableList(Objects.requireNonNull(words));
|
||||
this.wordIndex = wordIndex;
|
||||
this.wordCursor = wordCursor;
|
||||
this.cursor = cursor;
|
||||
this.openingQuote = openingQuote;
|
||||
}
|
||||
|
||||
public int wordIndex() {
|
||||
return this.wordIndex;
|
||||
}
|
||||
|
||||
public String word() {
|
||||
// TODO: word() should always be contained in words()
|
||||
if ((wordIndex < 0) || (wordIndex >= words.size())) {
|
||||
return "";
|
||||
}
|
||||
return words.get(wordIndex);
|
||||
}
|
||||
|
||||
public int wordCursor() {
|
||||
return this.wordCursor;
|
||||
}
|
||||
|
||||
public List<String> words() {
|
||||
return this.words;
|
||||
}
|
||||
|
||||
public int cursor() {
|
||||
return this.cursor;
|
||||
}
|
||||
|
||||
public String line() {
|
||||
return line;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence emit(CharSequence candidate) {
|
||||
StringBuilder sb = new StringBuilder(candidate);
|
||||
Predicate<Integer> needToBeEscaped;
|
||||
// Completion is protected by an opening quote:
|
||||
// Delimiters (spaces) don't need to be escaped, nor do other quotes, but everything else does.
|
||||
// Also, close the quote at the end
|
||||
if (openingQuote != null) {
|
||||
needToBeEscaped = i -> isRawEscapeChar(sb.charAt(i)) || String.valueOf(sb.charAt(i)).equals(openingQuote);
|
||||
} // No quote protection, need to escape everything: delimiter chars (spaces), quote chars
|
||||
// and escapes themselves
|
||||
else {
|
||||
needToBeEscaped = i -> isDelimiterChar(sb, i) || isRawEscapeChar(sb.charAt(i)) || isRawQuoteChar(sb.charAt(i));
|
||||
}
|
||||
for (int i = 0; i < sb.length(); i++) {
|
||||
if (needToBeEscaped.test(i)) {
|
||||
sb.insert(i++, escapeChars[0]);
|
||||
}
|
||||
}
|
||||
if (openingQuote != null) {
|
||||
sb.append(openingQuote);
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRawEscapeChar(char key) {
|
||||
for (char e : escapeChars) {
|
||||
if (e == key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isRawQuoteChar(char key) {
|
||||
for (char e : quoteChars) {
|
||||
if (e == key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.jline;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.Completer;
|
||||
import org.jline.reader.Highlighter;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.LineReaderBuilder;
|
||||
import org.jline.reader.ParsedLine;
|
||||
import org.jline.reader.UserInterruptException;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.TerminalBuilder;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell.CompletingParsedLine;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.ExitRequest;
|
||||
import org.springframework.shell.Input;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.shell.Shell;
|
||||
|
||||
/**
|
||||
* Shell implementation using JLine to capture input and trigger completions.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
*/
|
||||
@Configuration
|
||||
public class JLineShell {
|
||||
|
||||
@Autowired
|
||||
@Qualifier("main")
|
||||
private ResultHandler resultHandler;
|
||||
|
||||
@Bean
|
||||
public Terminal terminal() {
|
||||
try {
|
||||
return TerminalBuilder.builder().build();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new BeanCreationException("Could not create Terminal: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Shell shell() {
|
||||
return new Shell(new JLineInputProvider(lineReader()), resultHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CompleterAdapter completer() {
|
||||
return new CompleterAdapter();
|
||||
}
|
||||
|
||||
/*
|
||||
* Using setter injection to work around a circular dependency.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void lateInit() {
|
||||
completer().setShell(shell());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LineReader lineReader() {
|
||||
ExtendedDefaultParser parser = new ExtendedDefaultParser();
|
||||
parser.setEofOnUnclosedQuote(true);
|
||||
parser.setEofOnEscapedNewLine(true);
|
||||
|
||||
LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder()
|
||||
.terminal(terminal())
|
||||
.appName("Foo")
|
||||
.completer(completer())
|
||||
.highlighter(new Highlighter() {
|
||||
|
||||
@Override
|
||||
public AttributedString highlight(LineReader reader, String buffer) {
|
||||
int l = 0;
|
||||
String best = null;
|
||||
for (String command : shell().listCommands().keySet()) {
|
||||
if (buffer.startsWith(command) && command.length() > l) {
|
||||
l = command.length();
|
||||
best = command;
|
||||
}
|
||||
}
|
||||
if (best != null) {
|
||||
return new AttributedStringBuilder(buffer.length()).append(best, AttributedStyle.BOLD).append(buffer.substring(l)).toAttributedString();
|
||||
}
|
||||
else {
|
||||
return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
|
||||
}
|
||||
}
|
||||
})
|
||||
.parser(parser);
|
||||
|
||||
return lineReaderBuilder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the buffer input given the customizations applied to the JLine parser (<em>e.g.</em> support for
|
||||
* line continuations, <em>etc.</em>)
|
||||
*/
|
||||
static private List<String> sanitizeInput(List<String> words) {
|
||||
words = words.stream()
|
||||
.map(s -> s.replaceAll("^\\n+|\\n+$", "")) // CR at beginning/end of line introduced by backslash continuation
|
||||
.map(s -> s.replaceAll("\\n+", " ")) // CR in middle of word introduced by return inside a quoted string
|
||||
.collect(Collectors.toList());
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bridge between JLine's {@link Completer} contract and our own.
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public static class CompleterAdapter implements Completer {
|
||||
|
||||
private Shell shell;
|
||||
|
||||
@Override
|
||||
public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
|
||||
CompletingParsedLine cpl = (line instanceof CompletingParsedLine) ? ((CompletingParsedLine) line) : t -> t;
|
||||
|
||||
CompletionContext context = new CompletionContext(sanitizeInput(line.words()), line.wordIndex(), line.wordCursor());
|
||||
|
||||
List<CompletionProposal> proposals = shell.complete(context);
|
||||
proposals.stream()
|
||||
.map(p -> new Candidate(
|
||||
p.dontQuote() ? p.value() : cpl.emit(p.value()).toString(),
|
||||
p.displayText(),
|
||||
p.category(),
|
||||
p.description(),
|
||||
null,
|
||||
null,
|
||||
true)
|
||||
)
|
||||
.forEach(candidates::add);
|
||||
}
|
||||
|
||||
public void setShell(Shell shell) {
|
||||
this.shell = shell;
|
||||
}
|
||||
}
|
||||
|
||||
public static class JLineInputProvider implements Shell.InputProvider {
|
||||
|
||||
private final LineReader lineReader;
|
||||
|
||||
public JLineInputProvider(LineReader lineReader) {
|
||||
this.lineReader = lineReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Input readInput() {
|
||||
try {
|
||||
lineReader.readLine(new AttributedString("shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)).toAnsi(lineReader.getTerminal()));
|
||||
}
|
||||
catch (UserInterruptException e) {
|
||||
if (e.getPartialLine().isEmpty()) {
|
||||
throw new ExitRequest(1);
|
||||
} else {
|
||||
return Input.EMPTY;
|
||||
}
|
||||
}
|
||||
return new JLineInput(lineReader.getParsedLine());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static class JLineInput implements Input {
|
||||
|
||||
private final ParsedLine parsedLine;
|
||||
|
||||
JLineInput(ParsedLine parsedLine) {
|
||||
this.parsedLine = parsedLine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String rawText() {
|
||||
return parsedLine.line();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> words() {
|
||||
return sanitizeInput(parsedLine.words());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.utils.AttributedCharSequence;
|
||||
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A {@link ResultHandler} that knows how to render JLine's {@link AttributedCharSequence}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class AttributedCharSequenceResultHandler extends TerminalAwareResultHandler implements ResultHandler<AttributedCharSequence> {
|
||||
|
||||
@Override
|
||||
public void handleResult(AttributedCharSequence result) {
|
||||
terminal.writer().println(result.toAnsi(terminal));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.shell.CommandNotFound;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Used when no command can be matched for user input.
|
||||
*
|
||||
* Simply prints an error message, without printing the exception class.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class CommandNotFoundResultHandler extends TerminalAwareResultHandler implements ResultHandler<CommandNotFound> {
|
||||
|
||||
@Override
|
||||
public void handleResult(CommandNotFound result) {
|
||||
terminal.writer().println(new AttributedString(result.getMessage(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A simple {@link ResultHandler} that deals with Objects (hence comes as a last resort)
|
||||
* and prints the {@link Object#toString()} value of results to standard out.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class DefaultResultHandler implements ResultHandler<Object> {
|
||||
|
||||
@Override
|
||||
public void handleResult(Object result) {
|
||||
System.out.println(String.valueOf(result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.shell.ExitRequest;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Intercepts {@link org.springframework.shell.ExitRequest} exceptions and gracefully exits the running process.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class ExitRequestResultHandler implements ResultHandler<ExitRequest> {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void handleResult(ExitRequest result) {
|
||||
if (applicationContext instanceof Closeable) {
|
||||
try {
|
||||
((Closeable) applicationContext).close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
System.exit(result.status());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A {@link ResultHandler} that deals with {@link Iterable}s and delegates to
|
||||
* {@link TypeHierarchyResultHandler} for each element in turn.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class IterableResultHandler implements ResultHandler<Iterable> {
|
||||
|
||||
private ResultHandler delegate;
|
||||
|
||||
// Setter injection to avoid circular dependency at creation time
|
||||
void setDelegate(ResultHandler delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void handleResult(Iterable result) {
|
||||
for (Object o : result) {
|
||||
delegate.handleResult(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
|
||||
/**
|
||||
* Used for explicit configuration of {@link org.springframework.shell.ResultHandler}s.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Configuration
|
||||
public class ResultHandlerConfig {
|
||||
|
||||
@Bean
|
||||
@Qualifier("main")
|
||||
public ResultHandler<?> mainResultHandler() {
|
||||
return new TypeHierarchyResultHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IterableResultHandler iterableResultHandler() {
|
||||
return new IterableResultHandler();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void wireIterableResultHandler() {
|
||||
iterableResultHandler().setDelegate(mainResultHandler());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
/**
|
||||
* Base class for ResultHandlers that rely on JLine's {@link Terminal}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public abstract class TerminalAwareResultHandler {
|
||||
protected Terminal terminal;
|
||||
|
||||
@Autowired @Lazy
|
||||
public void setTerminal(Terminal terminal) {
|
||||
this.terminal = terminal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A {@link ResultHandler} that prints thrown exceptions messages in red.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class ThrowableResultHandler extends TerminalAwareResultHandler implements ResultHandler<Throwable> {
|
||||
|
||||
/**
|
||||
* The name of the command that may be used to print details about the last error.
|
||||
*/
|
||||
public static final String DETAILS_COMMAND_NAME = "stacktrace";
|
||||
|
||||
private Throwable lastError;
|
||||
|
||||
@Autowired @Lazy
|
||||
private CommandRegistry commandRegistry;
|
||||
|
||||
@Override
|
||||
public void handleResult(Throwable result) {
|
||||
lastError = result;
|
||||
terminal.writer().println(new AttributedString(result.toString(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
if (commandRegistry.listCommands().containsKey(DETAILS_COMMAND_NAME)) {
|
||||
terminal.writer().println(
|
||||
new AttributedStringBuilder()
|
||||
.append("Details of the error have been omitted. You can use the ", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED))
|
||||
.append(DETAILS_COMMAND_NAME, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).bold())
|
||||
.append(" command to print the full stacktrace.", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED))
|
||||
.toAnsi()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last error that was dealt with by this result handler.
|
||||
*/
|
||||
public Throwable getLastError() {
|
||||
return lastError;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
|
||||
/**
|
||||
* A delegating {@link ResultHandler} that dispatches handling based on the type of the result.
|
||||
* <p>
|
||||
* If no direct match is found, the type hierarchy of the result is considered, including implemented interfaces.
|
||||
* Auto-populates the handler map based on Generics type declaration of each discovered {@link ResultHandler} in the
|
||||
* ApplicationContext.
|
||||
* </p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class TypeHierarchyResultHandler implements ResultHandler<Object> {
|
||||
|
||||
private Map<Class<?>, ResultHandler<?>> resultHandlers = new HashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void handleResult(Object result) {
|
||||
if (result == null) { // void methods
|
||||
return;
|
||||
}
|
||||
Class<?> clazz = result.getClass();
|
||||
ResultHandler handler = getResultHandler(clazz);
|
||||
handler.handleResult(result);
|
||||
}
|
||||
|
||||
private ResultHandler getResultHandler(Class<?> clazz) {
|
||||
ResultHandler handler = resultHandlers.get(clazz);
|
||||
if (handler != null) {
|
||||
return handler;
|
||||
}
|
||||
else {
|
||||
for (Class type : clazz.getInterfaces()) {
|
||||
handler = getResultHandler(type);
|
||||
if (handler != null) {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
return clazz.getSuperclass() != null ? getResultHandler(clazz.getSuperclass()) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setResultHandlers(Set<ResultHandler<?>> resultHandlers) {
|
||||
for (ResultHandler<?> resultHandler : resultHandlers) {
|
||||
Type type = ((ParameterizedType) resultHandler.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0];
|
||||
registerHandler((Class<?>) type, resultHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerHandler(Class<?> type, ResultHandler<?> resultHandler) {
|
||||
ResultHandler<?> previous = this.resultHandlers.put(type, resultHandler);
|
||||
if (previous != null) {
|
||||
throw new IllegalArgumentException(String.format("Multiple ResultHandlers configured for %s: both %s and %s", type, previous, resultHandler));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains strategies for dealing with results of commands.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
package org.springframework.shell.result;
|
||||
@@ -0,0 +1,3 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.shell.SpringShellAutoConfiguration,\
|
||||
org.springframework.shell.jline.JLineShell
|
||||
5
spring-shell-core/src/main/resources/logback.xml
Normal file
5
spring-shell-core/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<configuration>
|
||||
<!-- Disable most stdout logging by default -->
|
||||
<root level="ERROR">
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Shell}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ShellTest {
|
||||
|
||||
@Rule
|
||||
public MockitoRule mockitoRule = MockitoJUnit.rule();
|
||||
|
||||
@Mock
|
||||
private Shell.InputProvider inputProvider;
|
||||
|
||||
@Mock
|
||||
private ResultHandler resultHandler;
|
||||
|
||||
@Mock
|
||||
private ParameterResolver parameterResolver;
|
||||
|
||||
private ValueResult valueResult;
|
||||
|
||||
@InjectMocks
|
||||
private Shell shell;
|
||||
|
||||
private boolean invoked;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
shell.parameterResolvers = Arrays.asList(parameterResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandMatch() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
|
||||
valueResult = new ValueResult(null, "test");
|
||||
when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any());
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
}
|
||||
|
||||
Assert.assertTrue(invoked);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandNotFound() throws IOException {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any(CommandNotFound.class));
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("bonjour", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noCommand() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "", () -> "hello world how are you doing ?");
|
||||
valueResult = new ValueResult(null, "test");
|
||||
when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any());
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
}
|
||||
|
||||
Assert.assertTrue(invoked);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandThrowingAnException() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "fail");
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any(SomeException.class));
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("fail", MethodTarget.of("failing", this, "Will throw an exception"));
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
}
|
||||
|
||||
Assert.assertTrue(invoked);
|
||||
|
||||
}
|
||||
|
||||
private void helloWorld(String a) {
|
||||
invoked = true;
|
||||
}
|
||||
|
||||
private String failing() {
|
||||
invoked = true;
|
||||
throw new SomeException();
|
||||
}
|
||||
|
||||
|
||||
private static class Exit extends RuntimeException {
|
||||
}
|
||||
|
||||
private static class SomeException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link Utils}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class UtilsTest {
|
||||
|
||||
@Test
|
||||
public void testUnCamelify() throws Exception {
|
||||
assertThat(Utils.unCamelify("HelloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorldHowAreYou")).isEqualTo("hello-world-how-are-you");
|
||||
assertThat(Utils.unCamelify("URL")).isEqualTo("url");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user