/*
* JBoss, Home of Professional Open Source.
* Copyright 2015, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.cli.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.jboss.as.cli.CommandContext;
import org.jboss.as.cli.CommandFormatException;
import org.jboss.as.cli.CommandLineCompleter;
import org.jboss.as.cli.Util;
import org.jboss.as.cli.operation.OperationRequestAddress;
import org.jboss.as.cli.parsing.CharacterHandler;
import org.jboss.as.cli.parsing.DefaultParsingState;
import org.jboss.as.cli.parsing.GlobalCharacterHandlers;
import org.jboss.as.cli.parsing.ParsingContext;
import org.jboss.as.cli.parsing.ParsingStateCallbackHandler;
import org.jboss.as.cli.parsing.StateParser;
import org.jboss.as.cli.parsing.WordCharacterHandler;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
/**
*
* @author Alexey Loubyansky
*/
public class AttributeNamePathCompleter implements CommandLineCompleter {
/**
* A filter to accept or not completion candidates.
*/
public interface AttributeFilter {
boolean accept(ModelNode descr);
}
/**
* Accep all candidates.
*/
private static final AttributeFilter DEFAULT_FILTER = (ModelNode descr) -> {
return true;
};
/**
* A list can be: an attribute of type LIST or inside an attribute of type
* OBJECT (complex or Map of list)
*/
public static final AttributeFilter LIST_FILTER = (descr) -> {
ModelNode mn = descr.has(Util.TYPE) ? descr.get(Util.TYPE) : null;
if (mn == null) {
// We don't know, invalid case, keep the attribute.
return true;
}
ModelType type;
try {
type = mn.asType();
} catch (Exception ex) {
// Invalid. TYPE is a complexType, keep the attribute.
return true;
}
// LIST
if (type == ModelType.LIST) {
return true;
}
// Can be a path to a LIST inside a Map of LIST
// or a complexType.
if (type == ModelType.OBJECT) {
if (descr.has(Util.VALUE_TYPE)) {
ModelNode vt = descr.get(Util.VALUE_TYPE);
try {
ModelType mt = vt.asType();
// A map of LIST
if (mt.equals(ModelType.LIST)) {
return true;
}
} catch (Exception ex) {
// This is a complex.
// Would be too much to analyze complete data
// structure. Keep it simple, we could
// have a list somewhere.
return true;
}
} else {
// An OBJECT without value type, can't do
// a lot with it. Anyway, keep it.
return true;
}
}
return false;
};
/**
* A map can be: an attribute of type OBJECT or inside an attribute of type
* LIST.
*/
public static final AttributeFilter MAP_FILTER = (descr) -> {
ModelNode mn = descr.has(Util.TYPE) ? descr.get(Util.TYPE) : null;
if (mn == null) {
// We don't know, invalid case, keep the attribute.
return true;
}
ModelType type;
try {
type = mn.asType();
} catch (Exception ex) {
// Invalid. TYPE is a complexType, keep the attribute.
return true;
}
// A map or inside a complex type
if (type == ModelType.OBJECT) {
return true;
}
// A List of complexType or LIST?
if (type == ModelType.LIST) {
if (descr.has(Util.VALUE_TYPE)) {
ModelNode vt = descr.get(Util.VALUE_TYPE);
try {
ModelType mt = vt.asType();
// A map of LIST
if (mt.equals(ModelType.LIST)) {
return true;
}
} catch (Exception ex) {
// This is a complex.
// Would be too much to analyze complete data
// structure. Keep it simple, we could
// have a map somewhere.
return true;
}
} else {
// A LIST without value type, can't do
// a lot with it. Anyway, keep it.
return true;
}
}
return false;
};
public static final AttributeNamePathCompleter INSTANCE = new AttributeNamePathCompleter();
private final OperationRequestAddress address;
private final ModelNode attrDescr;
private final boolean writeOnly;
private final AttributeFilter filter;
private AttributeNamePathCompleter() {
this.address = null;
this.attrDescr = null;
writeOnly = false;
filter = DEFAULT_FILTER;
}
public AttributeNamePathCompleter(OperationRequestAddress address, AttributeFilter filter) {
this(address, false, filter);
}
public AttributeNamePathCompleter(OperationRequestAddress address) {
this(address, null);
}
public AttributeNamePathCompleter(OperationRequestAddress address, boolean writeOnly) {
this(address, writeOnly, null);
}
public AttributeNamePathCompleter(OperationRequestAddress address, boolean writeOnly, AttributeFilter filter) {
if (address == null) {
throw new IllegalArgumentException("address is null");
}
this.address = address;
this.attrDescr = null;
this.writeOnly = writeOnly;
this.filter = filter == null ? DEFAULT_FILTER : filter;
}
public AttributeNamePathCompleter(ModelNode typeDescr) {
this(typeDescr, null);
}
public AttributeNamePathCompleter(ModelNode typeDescr, AttributeFilter filter) {
if (typeDescr == null) {
throw new IllegalArgumentException("typeDescr is null");
}
this.attrDescr = typeDescr;
this.address = null;
this.writeOnly = false;
this.filter = filter == null ? DEFAULT_FILTER : filter;
}
/* (non-Javadoc)
* @see org.jboss.as.cli.CommandLineCompleter#complete(org.jboss.as.cli.CommandContext, java.lang.String, int, java.util.List)
*/
@Override
public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) {
final ModelNode descr = attrDescr == null ? getAttributeDescription(ctx) : attrDescr;
if(descr == null) {
return -1;
}
return complete(buffer, candidates, descr, writeOnly);
}
public int complete(String buffer, List<String> candidates, final ModelNode descr) {
return complete(buffer, candidates, descr, writeOnly);
}
public int complete(String buffer, List<String> candidates, final ModelNode descr, boolean writeOnly) {
final AttributeNamePathCallbackHandler handler;
try {
handler = parse(buffer);
} catch (CommandFormatException e) {
e.printStackTrace();
return -1;
}
final Collection<String> foundCandidates = handler.getCandidates(descr, writeOnly);
if(foundCandidates.isEmpty()) {
return -1;
}
candidates.addAll(foundCandidates);
return handler.getCandidateIndex();
}
protected AttributeNamePathCallbackHandler parse(String line) throws CommandFormatException {
final AttributeNamePathCallbackHandler namePathHandler = new AttributeNamePathCallbackHandler(false);
StateParser.parse(line, namePathHandler, InitialValueState.INSTANCE);
return namePathHandler;
}
private class AttributeNamePathCallbackHandler implements ParsingStateCallbackHandler {
private static final String offsetStep = " ";
private final boolean logging;
private int offset;
private String lastEnteredState;
private char lastStateChar;
private int candidateIndex;
private List<String> path = Collections.emptyList();
private StringBuilder buf = new StringBuilder();
AttributeNamePathCallbackHandler(boolean logging) {
this.logging = logging;
}
public Collection<String> getCandidates(ModelNode attrsDescr, boolean writeOnly) {
if(attrsDescr == null || !attrsDescr.isDefined()) {
return Collections.emptyList();
}
ModelNode typeDescr = attrsDescr;
for(String name : path) {
if(!typeDescr.has(name)) {
return Collections.emptyList();
}
final ModelNode descr = typeDescr.get(name);
if(writeOnly && !isWritable(descr)) {
return Collections.emptyList();
}
if(!descr.hasDefined(Util.VALUE_TYPE)) {
return Collections.emptyList();
}
typeDescr = descr.get(Util.VALUE_TYPE);
if(typeDescr.getType() != ModelType.OBJECT &&
typeDescr.getType() != ModelType.PROPERTY) {
return Collections.emptyList();
}
}
Collection<String> attrNames = typeDescr.keys();
final List<String> candidates = new ArrayList<String>(attrNames.size());
if(DotState.ID.equals(lastEnteredState)) {
if(writeOnly) {
for(String name : attrNames) {
if (isWritable(typeDescr.get(name)) && filter.accept(typeDescr.get(name))) {
candidates.add(name);
}
}
} else {
for (String name : attrNames) {
if (filter.accept(typeDescr.get(name))) {
candidates.add(name);
}
}
}
Collections.sort(candidates);
return candidates;
}
if(OpenBracketState.ID.equals(lastEnteredState) || ListIndexState.ID.equals(lastEnteredState)) {
return Collections.emptyList();
}
if(CloseBracketState.ID.equals(lastEnteredState)) {
return Arrays.asList(".", "=");
}
final String chunk = buf.length() == 0 ? null : buf.toString();
ModelNode chunkDescr = null;
for (String candidate : attrNames) {
if (chunk == null || candidate.startsWith(chunk)) {
if (writeOnly && !isWritable(typeDescr.get(candidate))) {
continue;
}
if (chunk != null && chunk.length() == candidate.length()) {
chunkDescr = typeDescr.get(candidate);
continue;
}
if (filter.accept(typeDescr.get(candidate))) {
candidates.add(candidate);
}
}
}
if (chunkDescr != null) {
if (chunkDescr.hasDefined(Util.TYPE)) {
final ModelType modelType = chunkDescr.get(Util.TYPE).asType();
if (modelType.equals(ModelType.OBJECT)) {
if (candidates.isEmpty()) {
candidateIndex += chunk.length();
}
candidates.add(".");
} else if (modelType.equals(ModelType.LIST)) {
if (candidates.isEmpty()) {
candidateIndex += chunk.length();
}
candidates.add("[");
}
}
}
Collections.sort(candidates);
return candidates;
}
private boolean isWritable(ModelNode attrDescr) {
return !isReadOnly(attrDescr);
}
private boolean isReadOnly(ModelNode attrDescr) {
return attrDescr.has(Util.ACCESS_TYPE) && (Util.READ_ONLY.equals(attrDescr.get(Util.ACCESS_TYPE).asString()) || Util.METRIC.equals(attrDescr.get(Util.ACCESS_TYPE).asString()));
}
public int getCandidateIndex() {
switch(lastStateChar) {
case '.':
case '[':
case ']':
return candidateIndex + 1;
}
return candidateIndex;
}
@Override
public void enteredState(ParsingContext ctx) throws CommandFormatException {
final String prevEnteredState = lastEnteredState;
lastEnteredState = ctx.getState().getId();
candidateIndex = ctx.getLocation();
lastStateChar = ctx.getCharacter();
if(logging) {
final StringBuilder buf = new StringBuilder();
for (int i = 0; i < offset; ++i) {
buf.append(offsetStep);
}
buf.append("entered '" + ctx.getCharacter() + "' " + ctx.getState().getId());
System.out.println(buf.toString());
}
if(!CloseBracketState.ID.equals(prevEnteredState) && isNameSeparator()) {
if(buf.length() == 0) {
throw new CommandFormatException("Attribute name is missing before " + lastStateChar + " at index " + ctx.getLocation() + " in '" + ctx.getInput() + "'");
}
switch(path.size()) {
case 0:
path = Collections.singletonList(buf.toString());
break;
case 1:
path = new ArrayList<String>(path);
default:
path.add(buf.toString());
}
buf.setLength(0);
}
}
private boolean isNameSeparator() {
return lastEnteredState.equals(DotState.ID) ||
lastEnteredState.equals(OpenBracketState.ID);
}
@Override
public void leavingState(ParsingContext ctx) throws CommandFormatException {
if (logging) {
final StringBuilder buf = new StringBuilder();
for (int i = 0; i < offset; ++i) {
buf.append(offsetStep);
}
buf.append("leaving '" + ctx.getCharacter() + "' " + ctx.getState().getId());
System.out.println(buf.toString());
}
if(ctx.getState().getId().equals(ListIndexState.ID)) {
buf.setLength(0);
}
}
@Override
public void character(ParsingContext ctx) throws CommandFormatException {
if(logging) {
final StringBuilder buf = new StringBuilder();
for (int i = 0; i < offset; ++i) {
buf.append(offsetStep);
}
buf.append("char '" + ctx.getCharacter() + "' " + ctx.getState().getId());
System.out.println(buf.toString());
}
buf.append(ctx.getCharacter());
}
}
private static boolean isAttributeNameChar(final char c) {
return Character.isLetterOrDigit(c) || c == '_' || c == '-';
}
public static class InitialValueState extends DefaultParsingState {
public static final String ID = "INITVAL";
public static final InitialValueState INSTANCE = new InitialValueState();
public InitialValueState() {
this(AttributeNameState.INSTANCE);
}
public InitialValueState(final AttributeNameState attrName) {
super(ID);
setDefaultHandler(new CharacterHandler() {
@Override
public void handle(ParsingContext ctx) throws CommandFormatException {
ctx.enterState(attrName);
}});
enterState('.', DotState.INSTANCE);
enterState('[', OpenBracketState.INSTANCE);
setReturnHandler(new CharacterHandler() {
@Override
public void handle(ParsingContext ctx) throws CommandFormatException {
if(!ctx.isEndOfContent()) {
final char c = ctx.getCharacter();
if(isAttributeNameChar(c) || c == '.' || c == '[' || c == ']') {
getHandler(c).handle(ctx);
}
}
}});
}
}
public static class AttributeNameState extends DefaultParsingState {
public static final String ID = "ATTR_NAME";
public static final AttributeNameState INSTANCE = new AttributeNameState();
public AttributeNameState() {
super(ID);
this.setEnterHandler(new CharacterHandler(){
@Override
public void handle(ParsingContext ctx) throws CommandFormatException {
final char ch = ctx.getCharacter();
final CharacterHandler handler = getHandler(ch);
handler.handle(ctx);
}});
setDefaultHandler(new CharacterHandler() {
@Override
public void handle(ParsingContext ctx) throws CommandFormatException {
final char c = ctx.getCharacter();
WordCharacterHandler.IGNORE_LB_ESCAPE_ON.handle(ctx);
}
});
putHandler('.', GlobalCharacterHandlers.LEAVE_STATE_HANDLER);
putHandler('[', GlobalCharacterHandlers.LEAVE_STATE_HANDLER);
putHandler(']', GlobalCharacterHandlers.LEAVE_STATE_HANDLER);
}
}
public static class DotState extends DefaultParsingState {
public static final String ID = "DOT";
public static final DotState INSTANCE = new DotState();
public DotState() {
super(ID);
setDefaultHandler(GlobalCharacterHandlers.LEAVE_STATE_HANDLER);
}
}
public static class ListIndexState extends DefaultParsingState {
public static final String ID = "LIST_IND";
public static final ListIndexState INSTANCE = new ListIndexState();
public ListIndexState() {
super(ID);
setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_ON);
enterState(']', CloseBracketState.INSTANCE);
setReturnHandler(GlobalCharacterHandlers.LEAVE_STATE_HANDLER);
}
}
public static class OpenBracketState extends DefaultParsingState {
public static final String ID = "OPN_BR";
public static final OpenBracketState INSTANCE = new OpenBracketState();
OpenBracketState() {
super(ID);
setDefaultHandler(new CharacterHandler() {
@Override
public void handle(ParsingContext ctx) throws CommandFormatException {
ctx.enterState(ListIndexState.INSTANCE);
}});
setReturnHandler(GlobalCharacterHandlers.LEAVE_STATE_HANDLER);
}
}
public static class CloseBracketState extends DefaultParsingState {
public static final String ID = "CLS_BR";
public static final CloseBracketState INSTANCE = new CloseBracketState();
CloseBracketState() {
super(ID);
setDefaultHandler(GlobalCharacterHandlers.LEAVE_STATE_HANDLER);
}
}
private ModelNode getAttributeDescription(CommandContext ctx) {
final ModelNode req = new ModelNode();
final ModelNode addrNode = req.get(Util.ADDRESS);
for(OperationRequestAddress.Node node : address) {
addrNode.add(node.getType(), node.getName());
}
req.get(Util.OPERATION).set(Util.READ_RESOURCE_DESCRIPTION);
final ModelNode response;
try {
response = ctx.getModelControllerClient().execute(req);
} catch (Exception e) {
return null;
}
final ModelNode result = response.get(Util.RESULT);
if(!result.isDefined()) {
return null;
}
final ModelNode attrs;
if(result.getType().equals(ModelType.LIST)) {
ModelNode wildcardResult = null;
// wildcard address
for(ModelNode item : result.asList()) {
final ModelNode addr = item.get(Util.ADDRESS);
if(!addr.getType().equals(ModelType.LIST)) {
return null;
}
for(ModelNode node : addr.asList()) {
if(!node.getType().equals(ModelType.PROPERTY)) {
throw new IllegalArgumentException(node.getType().toString());
}
if("*".equals(node.asProperty().getValue().asString())) {
wildcardResult = item;
break;
}
}
if(wildcardResult != null) {
break;
}
}
if(wildcardResult == null) {
throw new IllegalStateException("Failed to locate the wildcard result.");
}
wildcardResult = wildcardResult.get(Util.RESULT);
if(!wildcardResult.isDefined()) {
return null;
}
attrs = wildcardResult.get(Util.ATTRIBUTES);
} else {
attrs = result.get(Util.ATTRIBUTES);
}
if(!attrs.isDefined()) {
return null;
}
return attrs;
}
}