/* * 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.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; 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.handlers.FilenameTabCompleter; import org.jboss.as.cli.operation.OperationRequestAddress; import org.jboss.as.cli.operation.impl.CapabilityReferenceCompleter; import org.jboss.as.cli.operation.impl.DefaultOperationRequestAddress; import org.jboss.as.cli.parsing.CharacterHandler; import org.jboss.as.cli.parsing.DefaultParsingState; import org.jboss.as.cli.parsing.EscapeCharacterState; 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 ValueTypeCompleter implements CommandLineCompleter { /** * Instance is the Model of the parsed Value. It contains the tree of * instance references. */ private abstract static class Instance { /* A property owned by an Instance. Some Property can have null name e.g.: Instance contained inside a List. */ static class Property { private String name; private Instance value; } // An instance is contained in a Parent. private final Instance parent; private final List<Property> properties = new ArrayList<>(); protected Property current; // An instance is compete if the terminal char has been seen. // Applies to List and Complex. private boolean complete; // The type description of the instance. protected ModelNode type; static Instance newInstance(Instance parent, String c) { switch (c) { case "[": { return new ListInstance(parent); } case "{": { return new ComplexInstance(parent); } default: { return new SimpleInstance(parent, c); } } } Instance(Instance parent) { this.parent = parent; } ModelNode getType() { if (type == null) { return null; } if (type.has(Util.VALUE_TYPE)) { return type.get(Util.VALUE_TYPE); } return type; } abstract boolean isCompliantType(ModelNode t); Instance setComplete(char c) { this.complete = isTerminalChar(c); if (complete) { return parent == null ? this : parent; } else { return this; } } abstract boolean isTerminalChar(char c); boolean isComplete() { return complete; } void newProperty() { current = new Property(); properties.add(current); } public Property getLastProperty() { if (properties.isEmpty()) { return null; } return properties.get(properties.size() - 1); } public void endProperty(String content) { if (current.value == null) { current.value = new SimpleInstance(this, content); } } private Instance newPropertyValue(String c) throws CommandFormatException { if (current == null) { throw new CommandFormatException("Invalid syntax"); } current.value = Instance.newInstance(this, c); // Associates the type to the instance. // This depends on the nature of the current instance current.value.type = retrieveType(); return current.value; } abstract ModelNode retrieveType(); private void setPropertyName(String name) throws CommandFormatException { if (current == null) { throw new CommandFormatException("Invalid syntax"); } current.name = name; } public String asString() { return null; } private boolean contains(String p) { boolean found = false; for (Property prop : properties) { if (prop.name != null && prop.name.equals(p) && prop.value != null) { found = true; break; } } return found; } } private static class ListInstance extends Instance { public ListInstance(Instance parent) { super(parent); } @Override boolean isTerminalChar(char c) { return c == ']'; } @Override boolean isCompliantType(ModelNode t) { return ValueTypeCompleter.typeEquals(t, ModelType.LIST); } @Override ModelNode retrieveType() { if (type.has(Util.VALUE_TYPE)) { return type.get(Util.VALUE_TYPE); } return null; } } private static class ComplexInstance extends Instance { public ComplexInstance(Instance parent) { super(parent); } @Override boolean isTerminalChar(char c) { return c == '}'; } @Override public void endProperty(String content) { // Last property name is null then this is a name. if (current.name == null) { current.name = content; return; } super.endProperty(content); } @Override boolean isCompliantType(ModelNode t) { return ValueTypeCompleter.typeEquals(t, ModelType.OBJECT); } @Override ModelNode retrieveType() { if (current.name != null) { if (type.has(current.name)) { return type.get(current.name); } else if (type.has(Util.VALUE_TYPE)) { ModelNode vt = type.get(Util.VALUE_TYPE); if (vt.has(current.name)) { return vt.get(current.name); } } } return null; } } private static class SimpleInstance extends Instance { private final String value; public SimpleInstance(Instance parent, String value) { super(parent); this.value = value; } @Override public String asString() { return value; } @Override boolean isCompliantType(ModelNode t) { return !ValueTypeCompleter.typeEquals(t, ModelType.OBJECT) && !ValueTypeCompleter.typeEquals(t, ModelType.LIST); } @Override boolean isTerminalChar(char c) { return false; } @Override ModelNode retrieveType() { return null; } } /** * To test capabilities completion without actual connection to server. */ public interface CapabilityCompleterFactory { CapabilityReferenceCompleter newCompleter(OperationRequestAddress address, String staticPart); } private static final List<ModelNode> BOOLEAN_LIST = new ArrayList<ModelNode>(2); static { BOOLEAN_LIST.add(new ModelNode(Boolean.FALSE)); BOOLEAN_LIST.add(new ModelNode(Boolean.TRUE)); } private final ModelNode propDescr; private Instance currentInstance; private CommandContext ctx; private final OperationRequestAddress address; private String buffer; private final CapabilityCompleterFactory factory; public ValueTypeCompleter(ModelNode propDescr) { this(propDescr, new DefaultOperationRequestAddress()); } // Testing purpose public ValueTypeCompleter(ModelNode propDescr, CapabilityCompleterFactory factory) { this(propDescr, new DefaultOperationRequestAddress(), factory); } public ValueTypeCompleter(ModelNode propDescr, OperationRequestAddress address) { this(propDescr, address, null); } public ValueTypeCompleter(ModelNode propDescr, OperationRequestAddress address, CapabilityCompleterFactory factory) { if(propDescr == null || !propDescr.isDefined()) { throw new IllegalArgumentException("property description is null or undefined."); } this.propDescr = propDescr; this.address = address; this.factory = factory == null ? (a, p) -> { return new CapabilityReferenceCompleter(a, p); } : factory; } @Override public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) { // The context is used deep down the completion processes by concrete class implementing // public interfaces. this.ctx = ctx; this.buffer = buffer; /* int nextCharIndex = 0; while (nextCharIndex < buffer.length()) { if (!Character.isWhitespace(buffer.charAt(nextCharIndex))) { break; } ++nextCharIndex; } */ final ValueTypeCallbackHandler handler; try { handler = parse(buffer); } catch (CommandFormatException e) { // TODO add logging here return -1; } final Collection<String> foundCandidates = handler.getCandidates(propDescr); if(foundCandidates.isEmpty()) { return -1; } candidates.addAll(foundCandidates); return handler.getCompletionIndex(); } protected ValueTypeCallbackHandler parse(String line) throws CommandFormatException { final ValueTypeCallbackHandler valueTypeHandler = new ValueTypeCallbackHandler(false); StateParser.parse(line, valueTypeHandler, InitialValueState.INSTANCE); return valueTypeHandler; } private static boolean typeEquals(ModelNode mn, ModelType type) { ModelType mt; try { mt = mn.asType(); } catch (IllegalArgumentException ex) { return false; } return mt.equals(type); } private static boolean isObject(ModelNode mn) { ModelType mt; try { mt = mn.asType(); } catch (IllegalArgumentException ex) { return true; } return false; } private final class ValueTypeCallbackHandler implements ParsingStateCallbackHandler { private static final String offsetStep = " "; private final boolean logging; private int offset; private StringBuilder propBuf = new StringBuilder(); private String lastEnteredState; private int lastStateIndex; private char lastStateChar; private int valLength; // ValueTypeCallbackHandler() { // this(false); // } ValueTypeCallbackHandler(boolean logging) { this.logging = logging; } private List<String> getCandidatesFromMetadata(ModelNode propType, String path) { List<String> candidates = null; if (propType.has(Util.FILESYSTEM_PATH) && propType.get(Util.FILESYSTEM_PATH).asBoolean()) { CommandLineCompleter completer = FilenameTabCompleter.newCompleter(ctx); candidates = new ArrayList<>(); completer.complete(ctx, path, offset, candidates); // candidates only contain the last part of a path. // We need to keep the radical and do the replacement after it // valLength is used when computing the replacement index. // On Windows, the '\' is a state, so the index is already at the end of // the stream. valLength = path.lastIndexOf(File.separator) + 1; if (Util.isWindows()) { int i = path.lastIndexOf(File.separator); if (i >= 0) { // Take into account escape char valLength = 2; } // Must escape separator on Windows. if (candidates.size() == 1) { String candidate = candidates.get(0); if (candidate.endsWith(File.separator)) { candidate = candidate + File.separator; } candidates.set(0, candidate); } } } else if (propType.has(Util.RELATIVE_TO) && propType.get(Util.RELATIVE_TO).asBoolean()) { DeploymentItemCompleter completer = new DeploymentItemCompleter(address); candidates = new ArrayList<>(); valLength = completer.complete(ctx, path, offset, candidates); } else if (propType.has(Util.CAPABILITY_REFERENCE)) { CapabilityReferenceCompleter completer = factory.newCompleter(address, propType.get(Util.CAPABILITY_REFERENCE).asString()); candidates = new ArrayList<>(); completer.complete(ctx, path, offset, candidates); } return candidates; } public int getCompletionIndex() { //System.out.println("\ngetCompletionIndex: " + lastStateChar + " " + lastStateIndex); switch(lastStateChar) { case '{': case '}': case '[': case ']': case '=': case ',': return lastStateIndex + (valLength == 0 ? 1 : valLength); } // Some value completer compute an offset between the lastStateIndex // and the returned candidates (e.g.: file paths completion return lastStateIndex + valLength; } public Collection<String> getCandidates(ModelNode propDescr) { if(propDescr == null || !propDescr.isDefined()) { return Collections.emptyList(); } if(!propDescr.has(Util.VALUE_TYPE)) { return Collections.emptyList(); } // Empty value, returns the first char if (lastEnteredState == null) { if (propDescr.has(Util.TYPE)) { ModelNode mt = propDescr.get(Util.TYPE); // Can be a Map or a complex type ruled by value-type. if (typeEquals(mt, ModelType.OBJECT)) { return Collections.singletonList("{"); } else if (typeEquals(mt, ModelType.LIST)) { return Collections.singletonList("["); } } } // Retrieves the type of the current instance. ModelNode propType = null; if (currentInstance != null) { propType = currentInstance.getType(); } if (propType == null) { return Collections.emptyList(); } // If the currentInstance is the root and is complete must // return an index of 0 and the full value type content. // That is the contract with OperationRequestCompleter to concider // the value has beeing complete and propose ')' to close the // operation if (currentInstance.parent == null && currentInstance.isComplete()) { lastStateIndex = 0; return Collections.singletonList(buffer); } // Retrieve the last property (if any) Instance.Property last = currentInstance.getLastProperty(); // On list separator, or when no property exists, // complete with the next item in the list or the next property // inside an Object if (lastEnteredState.equals(ListItemSeparatorState.ID) || last == null) { return completeNewProperty(propType); } // At this point, we have a property that is not terminated. // Are we completing after the equals? // If yes, complete with possible values. if (lastEnteredState.equals(EqualsState.ID)) { // Wrong syntax, for example for a String value inside a list, user would type // "[{role=<TAB>" if (!isObject(propType)) { return Collections.emptyList(); } ModelNode pType = propType.get(last.name); if (pType.has(Util.TYPE)) { final ModelNode mt = pType.get(Util.TYPE); if (typeEquals(mt, ModelType.OBJECT)) { return Collections.singletonList("{"); } else if (typeEquals(mt, ModelType.LIST)) { return Collections.singletonList("["); } } List<String> candidates = new ArrayList<>(); boolean complete = getSimpleValues(propType, last.name, "", candidates); // If the value is complete, we could return '}' or ','if the object is // complete. if (complete) { return getCompletedValueCandidates(propType); } else { return candidates; } } // a piece of value? if (last.value != null) { if (last.name != null) { // An instance property // Wrong syntax, for example for a String value inside a list, user would type // "[{role=ccc<TAB>" if (!isObject(propType)) { return Collections.emptyList(); } if (last.value.isComplete()) { // a property of type List or Object that is complete return getCompletedValueCandidates(propType); } else { List<String> candidates = new ArrayList<>(); boolean complete = getSimpleValues(propType, last.name, last.value.asString(), candidates); if (complete) { return getCompletedValueCandidates(propType); } else { return candidates; } } } else // An empty name // Could be the end of a list item, propose the next one or end. // The completion index is already at the end of the stream. if ((currentInstance instanceof ListInstance) && !currentInstance.isComplete()) { // A list of capability references // We can do better by analysing its content. if (currentInstance.type.has(Util.CAPABILITY_REFERENCE)) { return getCapabilitiesListContent(last); } List<String> candidates = new ArrayList<>(); getSimpleValues(currentInstance.type, null, last.value.asString(), candidates); // Add separator only for complex types, a simple type, such as a String, // could lead to empty candidates too. // A list of list (propType == LIST), requires separator too. if (candidates.isEmpty() && (isObject(propType) || propType.asType() == ModelType.LIST)) { candidates.add("]"); candidates.add(","); } Collections.sort(candidates); return candidates; } else { return Collections.<String>emptyList(); } } // A piece of name? if (last.name != null) { final List<String> candidates = new ArrayList<>(); // The propType can be an OBJECT, in such a case, no keys. if (!isObject(propType)) { return Collections.<String>emptyList(); } for (String p : propType.keys()) { if (p.startsWith(last.name) && !currentInstance.contains(p)) { candidates.add(p); } } // Inline the equals. if (candidates.size() == 1) { if (last.name.equals(candidates.get(0))) { candidates.set(0, last.name + "="); } } Collections.sort(candidates); return candidates; } return Collections.emptyList(); } private Set<String> getAllCapabilities() { String staticPart = currentInstance.type.get(Util.CAPABILITY_REFERENCE).asString(); List<String> names = factory.newCompleter(address, staticPart).getCapabilityReferenceNames(ctx, address, staticPart); Set<String> allSet = new HashSet<>(); allSet.addAll(names); return allSet; } private Set<String> getPresentCapabilities() { Set<String> presentSet = new HashSet<>(); for (Instance.Property p : currentInstance.properties) { presentSet.add(p.value.asString()); } return presentSet; } private List<String> getCapabilitiesListContent(Instance.Property last) { String val = last == null ? "" : last.value.asString(); String staticPart = currentInstance.type.get(Util.CAPABILITY_REFERENCE).asString(); Set<String> allSet = getAllCapabilities(); Set<String> presentSet = getPresentCapabilities(); if (allSet.equals(presentSet)) { valLength = buffer.length() - lastStateIndex; return Collections.singletonList("]"); } CapabilityReferenceCompleter completer = factory.newCompleter(address, staticPart); List<String> candidates = new ArrayList<>(); completer.complete(ctx, val, offset, candidates); if (candidates.size() == 1) { if (candidates.get(0).equals(val)) { valLength = buffer.length() - lastStateIndex; candidates.set(0, ","); } } // Remove existing. candidates.removeAll(presentSet); Collections.sort(candidates); return candidates; } private List<String> getCompletedValueCandidates(ModelNode propType) { // In this case we need to reach the end of the stream and add separator. valLength = buffer.length() - lastStateIndex; // Do we have some properties to propose? if (propType.getType() == ModelType.OBJECT) { final List<String> props = new ArrayList<>(propType.keys()); // Remove the properties already present for (Instance.Property p : currentInstance.properties) { props.remove(p.name); } if (props.isEmpty()) { return Collections.singletonList("}"); } else { return Collections.singletonList(","); } } else { return Collections.emptyList(); } } // if a value is already present and complete (eg: true/false, allowed // returns true. Otherwise returns false. private boolean getSimpleValues(ModelNode propType, String name, String radical, List<String> candidates) { // name could be null of List properties if (name != null) { propType = propType.get(name); } final List<ModelNode> allowed; if (!propType.has(Util.ALLOWED)) { if (isBoolean(propType)) { allowed = BOOLEAN_LIST; } else { List<String> c = getCandidatesFromMetadata(propType, radical); if (c != null) { candidates.addAll(c); } return false; } } else { allowed = propType.get(Util.ALLOWED).asList(); } boolean isComplete = false; for (ModelNode candidate : allowed) { String c = candidate.asString(); if (c.startsWith(radical)) { if (c.equals(radical)) { isComplete = true; break; } candidates.add(candidate.asString()); } } Collections.sort(candidates); return isComplete; } // Completion for a new property private Collection<String> completeNewProperty(ModelNode propType) { if (currentInstance instanceof ListInstance) { // This is inside a list try { // A list of booleans ModelType mt = propType.asType(); if (mt.equals(ModelType.BOOLEAN)) { List<String> candidates = new ArrayList<>(); for (ModelNode candidate : BOOLEAN_LIST) { candidates.add(candidate.asString()); } Collections.sort(candidates); return candidates; } else { List<String> candidates = null; if (mt.equals(ModelType.OBJECT)) { candidates = getCandidatesFromMetadata(propType, ""); // New instance. if (candidates == null) { candidates = new ArrayList<>(); candidates.add("{"); } } else if (mt.equals(ModelType.LIST)) { return Collections.singletonList("["); } else { if (currentInstance.type.has(Util.CAPABILITY_REFERENCE)) { candidates = getCapabilitiesListContent(null); } else { candidates = getCandidatesFromMetadata(currentInstance.type, ""); } } if (candidates != null) { return candidates; } // We don't know, returns an empty list. return Collections.<String>emptyList(); } } catch (IllegalArgumentException ex) { // This is an Object, returns the start character. return Collections.singletonList("{"); } } else { // This is inside an instance. // 2 cases, an Object with a proper value-type that describes its structure. // or a Map<String, 'propType'>; if (propType.getType() == ModelType.OBJECT) { // This is inside an instance. final List<String> candidates = new ArrayList<>(propType.keys()); // Remove the properties already present for (Instance.Property p : currentInstance.properties) { candidates.remove(p.name); } if (candidates.isEmpty()) { candidates.add("}"); } else { Collections.sort(candidates); } return candidates; } else { return Collections.<String>emptyList(); } } } protected boolean isBoolean(ModelNode propType) { if (propType.has(Util.TYPE)) { return typeEquals(propType.get(Util.TYPE), ModelType.BOOLEAN); } return false; } @Override public void enteredState(ParsingContext ctx) throws CommandFormatException { lastEnteredState = ctx.getState().getId(); lastStateIndex = 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 '" + lastStateChar + "' " + lastEnteredState); System.out.println(buf.toString()); if(lastEnteredState.equals(PropertyListState.ID)) { ++offset; } } switch (lastEnteredState) { case StartListState.ID: case StartObjectState.ID: { if (currentInstance == null) { currentInstance = Instance.newInstance(null, ""+lastStateChar); currentInstance.type = propDescr; } else { currentInstance = currentInstance.newPropertyValue(""+lastStateChar); } break; } case PropertyState.ID: { if (currentInstance == null) { throw new CommandFormatException("Invalid syntax."); } currentInstance.newProperty(); break; } case EqualsState.ID: { if (currentInstance == null) { throw new CommandFormatException("Invalid syntax."); } currentInstance.setPropertyName(propBuf.toString()); propBuf.setLength(0); break; } } } @Override public void leavingState(ParsingContext ctx) throws CommandFormatException { final String id = ctx.getState().getId(); if (logging) { if (id.equals(PropertyListState.ID)) { --offset; } final StringBuilder buf = new StringBuilder(); for (int i = 0; i < offset; ++i) { buf.append(offsetStep); } buf.append("leaving '" + ctx.getCharacter() + "' " + id); System.out.println(buf.toString()); } switch (id) { case TextState.ID: case PropertyState.ID: { // Store the last chunk of value here. if (propBuf.length() > 0) { currentInstance.endProperty(propBuf.toString()); propBuf.setLength(0); } break; } case StartListState.ID: case StartObjectState.ID: { // When leaving the input, some closing brackets could be still there. // and must be skipped. if (!ctx.isEndOfContent()) {// close and skip the '{', '[' currentInstance = currentInstance.setComplete(ctx.getCharacter()); if (ctx.getCharacter() == '}' || ctx.getCharacter() == ']') { ctx.advanceLocation(1); } } break; } } } @Override public void character(ParsingContext ctx) throws CommandFormatException { final String id = ctx.getState().getId(); if(logging) { final StringBuilder buf = new StringBuilder(); for (int i = 0; i < offset; ++i) { buf.append(offsetStep); } buf.append("char '" + ctx.getCharacter() + "' " + id); System.out.println(buf.toString()); } if(id.equals(PropertyState.ID)) { final char ch = ctx.getCharacter(); if(ch != '"' && !Character.isWhitespace(ch)) { propBuf.append(ch); } } else if(id.equals(TextState.ID)) { propBuf.append(ctx.getCharacter()); } else if (id.equals(EscapeCharacterState.ID)) { propBuf.append(ctx.getCharacter()); } } } /* private final class EchoCallbackHandler implements ParsingStateCallbackHandler { private int offset = 0; private String offsetStep = " "; private final StringBuilder parsingBuf; private EchoCallbackHandler(StringBuilder parsingBuf) { this.parsingBuf = parsingBuf; } @Override public void enteredState(ParsingContext ctx) throws CommandFormatException { final String id = ctx.getState().getId(); final StringBuilder buf = new StringBuilder(); for(int i = 0; i < offset; ++i) { buf.append(offsetStep); } buf.append("entered '" + ctx.getCharacter() + "' " + id); System.out.println(buf.toString()); if(id.equals(PropertyState.ID)) { for(int i = 0; i < offset; ++i) { parsingBuf.append(offsetStep); } } else if(id.equals(EqualsState.ID)) { parsingBuf.append('='); } else if(id.equals(PropertyListState.ID)) { ++offset; } else if(id.equals(StartObjectState.ID)) { parsingBuf.append('{').append(Util.LINE_SEPARATOR); } else if(id.equals(StartListState.ID)) { parsingBuf.append('[').append(Util.LINE_SEPARATOR); } } @Override public void leavingState(ParsingContext ctx) throws CommandFormatException { final String id = ctx.getState().getId(); if(id.equals(PropertyListState.ID)) { --offset; } final StringBuilder buf = new StringBuilder(); for(int i = 0; i < offset; ++i) { buf.append(offsetStep); } buf.append("leaving '" + ctx.getCharacter() + "' " + id); System.out.println(buf.toString()); if(id.equals(ListItemSeparatorState.ID)) { parsingBuf.append(","); parsingBuf.append(Util.LINE_SEPARATOR); } else if(id.equals(StartObjectState.ID)) { parsingBuf.append(Util.LINE_SEPARATOR); for(int i = 0; i < offset; ++i) { parsingBuf.append(offsetStep); } parsingBuf.append('}'); } else if(id.equals(StartListState.ID)) { parsingBuf.append(Util.LINE_SEPARATOR); for(int i = 0; i < offset; ++i) { parsingBuf.append(offsetStep); } parsingBuf.append(']'); } } @Override public void character(ParsingContext ctx) throws CommandFormatException { final String id = ctx.getState().getId(); final StringBuilder buf = new StringBuilder(); for(int i = 0; i < offset; ++i) { buf.append(offsetStep); } buf.append("char '" + ctx.getCharacter() + "' " + id); System.out.println(buf.toString()); if(id.equals(PropertyState.ID) || id.equals(TextState.ID)) { parsingBuf.append(ctx.getCharacter()); } } } */ public interface ValueTypeCandidatesProvider { Collection<String> getCandidates(String chunk); } abstract static class ValueTypeCandidatesState extends DefaultParsingState implements ValueTypeCandidatesProvider { private final Collection<String> candidates = new ArrayList<String>(); ValueTypeCandidatesState(String id) { super(id); } protected void addCandidate(String candidate) { candidates.add(candidate); } protected void addCandidates(Collection<String> candidates) { this.candidates.addAll(candidates); } @Override public Collection<String> getCandidates(String chunk) { if(candidates.isEmpty()) { return Collections.emptyList(); } if(chunk == null || chunk.length() == 0) { return candidates; } final List<String> filtered = new ArrayList<String>(candidates.size()); for(String candidate : candidates) { if(candidate.startsWith(chunk)) { filtered.add(candidate); } } return filtered; } } public static class InitialValueState extends ValueTypeCandidatesState { public static final String ID = "INITVAL"; public static final InitialValueState INSTANCE = new InitialValueState(); public InitialValueState() { this(PropertyState.INSTANCE); } public InitialValueState(final PropertyState prop) { super(ID); enterState('{', StartObjectState.INSTANCE); enterState('[', StartListState.INSTANCE); setDefaultHandler(new CharacterHandler() { @Override public void handle(ParsingContext ctx) throws CommandFormatException { //ctx.enterState(prop); ctx.enterState(PropertyListState.INSTANCE); }}); addCandidate("{"); addCandidate("["); addCandidates(prop.getCandidates(null)); } } public static class StartObjectState extends DefaultParsingState { public static final String ID = "OBJ"; private static StartObjectState INSTANCE = new StartObjectState(); public StartObjectState() { super(ID); setDefaultHandler(new CharacterHandler(){ @Override public void handle(ParsingContext ctx) throws CommandFormatException { ctx.enterState(PropertyListState.INSTANCE); }}); setIgnoreWhitespaces(true); setReturnHandler(new CharacterHandler(){ @Override public void handle(ParsingContext ctx) throws CommandFormatException { ctx.leaveState(); }}); } } public static class StartListState extends DefaultParsingState { public static final String ID = "LST"; private static StartListState INSTANCE = new StartListState(); public StartListState() { super(ID); setDefaultHandler(new CharacterHandler(){ @Override public void handle(ParsingContext ctx) throws CommandFormatException { ctx.enterState(PropertyListState.INSTANCE); }}); setIgnoreWhitespaces(true); setReturnHandler(new CharacterHandler(){ @Override public void handle(ParsingContext ctx) throws CommandFormatException { // Location is advanced in the CallbackHandler (leavingState) //if(!ctx.isEndOfContent()) { // ctx.advanceLocation(1); //} ctx.leaveState(); }}); } } public static class PropertyListState extends DefaultParsingState { public static final String ID = "PROPLIST"; public static final PropertyListState INSTANCE = new PropertyListState(); public PropertyListState() { super(ID); setEnterHandler(new CharacterHandler() { @Override public void handle(ParsingContext ctx) throws CommandFormatException { ctx.enterState(PropertyState.INSTANCE); }}); setDefaultHandler(new CharacterHandler(){ @Override public void handle(ParsingContext ctx) throws CommandFormatException { ctx.enterState(PropertyState.INSTANCE); }}); setReturnHandler(new CharacterHandler(){ @Override public void handle(ParsingContext ctx) throws CommandFormatException { if(ctx.isEndOfContent()) { ctx.leaveState(); return; } final char ch = ctx.getCharacter(); getHandler(ch).handle(ctx); } }); enterState(',', ListItemSeparatorState.INSTANCE); leaveState(']'); leaveState('}'); setIgnoreWhitespaces(true); } } public static class ListItemSeparatorState extends DefaultParsingState implements ValueTypeCandidatesProvider { public static final String ID = "ITMSEP"; public static final ListItemSeparatorState INSTANCE = new ListItemSeparatorState(); public ListItemSeparatorState() { super(ID); setEnterHandler(new CharacterHandler(){ @Override public void handle(ParsingContext ctx) throws CommandFormatException { if(!ctx.isEndOfContent()) { ctx.advanceLocation(1); } ctx.leaveState(); }}); } @Override public Collection<String> getCandidates(String chunk) { return Collections.emptyList(); } } public static class PropertyState extends DefaultParsingState implements ValueTypeCandidatesProvider { public static final String ID = "PROP"; public static final PropertyState INSTANCE = new PropertyState(); private final Collection<String> candidates = new ArrayList<String>(2); public PropertyState() { 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); } }); enterState('{', StartObjectState.INSTANCE); // Used to be PropertyListState but when '[' is encountered we should // move to List state first. enterState('[', StartListState.INSTANCE); setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_ON); enterState('=', EqualsState.INSTANCE); setReturnHandler(new CharacterHandler(){ @Override public void handle(ParsingContext ctx) throws CommandFormatException { ctx.leaveState(); }}); leaveState(','); leaveState(']'); leaveState('}'); candidates.add("="); } @Override public Collection<String> getCandidates(String chunk) { // TODO and on the '=' I should add value candidates return candidates; } } public static class EqualsState extends ValueTypeCandidatesState { public static final String ID = "EQ"; public static final EqualsState INSTANCE = new EqualsState(); public EqualsState() { super(ID); setIgnoreWhitespaces(true); setDefaultHandler(new CharacterHandler() { @Override public void handle(ParsingContext ctx) throws CommandFormatException { ctx.enterState(TextState.INSTANCE); }}); putHandler('>', GlobalCharacterHandlers.NOOP_CHARACTER_HANDLER); enterState('{', StartObjectState.INSTANCE); enterState('[', StartListState.INSTANCE); addCandidate("{"); addCandidate("["); setReturnHandler(GlobalCharacterHandlers.LEAVE_STATE_HANDLER); } } public static class TextState extends ValueTypeCandidatesState { public static final String ID = "TEXT"; public static final TextState INSTANCE = new TextState(); public TextState() { super(ID); setHandleEntrance(true); setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_ON); leaveState(','); leaveState('='); leaveState('}'); leaveState(']'); } } }