/* * 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.parsing; import java.util.ArrayDeque; import java.util.Deque; import org.jboss.as.cli.CommandContext; import org.jboss.as.cli.CommandFormatException; import org.jboss.as.cli.Util; import org.jboss.as.cli.util.CLIExpressionResolver; /** * * @author Alexey Loubyansky */ public class StateParser { private final DefaultParsingState initialState = new DefaultParsingState("INITIAL"); public void addState(char ch, ParsingState state) { initialState.enterState(ch, state); } /** * Returns the string which was actually parsed with all the substitutions performed */ public String parse(String str, ParsingStateCallbackHandler callbackHandler) throws CommandFormatException { return parse(str, callbackHandler, initialState); } /** * Returns the string which was actually parsed with all the substitutions performed */ public static String parse(String str, ParsingStateCallbackHandler callbackHandler, ParsingState initialState) throws CommandFormatException { return parse(str, callbackHandler, initialState, true); } /** * Returns the string which was actually parsed with all the substitutions performed */ public static String parse(String str, ParsingStateCallbackHandler callbackHandler, ParsingState initialState, boolean strict) throws CommandFormatException { return parse(str, callbackHandler, initialState, strict, null); } /** * Returns the string which was actually parsed with all the substitutions performed */ public static String parse(String str, ParsingStateCallbackHandler callbackHandler, ParsingState initialState, boolean strict, CommandContext ctx) throws CommandFormatException { try { return doParse(str, callbackHandler, initialState, strict, ctx); } catch(CommandFormatException e) { throw e; } catch(Throwable t) { throw new CommandFormatException("Failed to parse '" + str + "'", t); } } /** * Returns the string which was actually parsed with all the substitutions performed */ protected static String doParse(String str, ParsingStateCallbackHandler callbackHandler, ParsingState initialState, boolean strict) throws CommandFormatException { return doParse(str, callbackHandler, initialState, strict, null); } /** * Returns the string which was actually parsed with all the substitutions performed */ protected static String doParse(String str, ParsingStateCallbackHandler callbackHandler, ParsingState initialState, boolean strict, CommandContext cmdCtx) throws CommandFormatException { if (str == null || str.isEmpty()) { return str; } ParsingContextImpl ctx = new ParsingContextImpl(); ctx.initialState = initialState; ctx.callbackHandler = callbackHandler; ctx.input = str; ctx.strict = strict; ctx.cmdCtx = cmdCtx; return ctx.parse(); } static class ParsingContextImpl implements ParsingContext { private final Deque<ParsingState> stack = new ArrayDeque<ParsingState>(); String input; String originalInput; int location; char ch; ParsingStateCallbackHandler callbackHandler; ParsingState initialState; boolean strict; CommandFormatException error; CommandContext cmdCtx; private final Deque<Character> lookFor = new ArrayDeque<Character>(); /** to not meet the same character at the same position multiple times */ int lastMetLookForIndex = -1; private char deactivated; String parse() throws CommandFormatException { ch = input.charAt(0); originalInput = input; location = 0; initialState.getEnterHandler().handle(this); while (location < input.length()) { ch = input.charAt(location); final CharacterHandler handler = getState().getHandler(ch); handler.handle(this); ++location; } ParsingState state = getState(); while(state != initialState) { state.getEndContentHandler().handle(this); leaveState(); state = getState(); } initialState.getEndContentHandler().handle(this); initialState.getLeaveHandler().handle(this); return input; } @Override public void resolveExpression(boolean systemProperty, boolean exceptionIfNotResolved) throws UnresolvedExpressionException { final int inputLength = input.length(); if(inputLength - location < 2) { return; } final char firstChar = input.charAt(location); if(firstChar == '$') { if (input.charAt(location + 1) == '{') { if (systemProperty) { input = CLIExpressionResolver.resolveProperty(input, location, exceptionIfNotResolved); ch = input.charAt(location); } } else { substituteVariable(exceptionIfNotResolved); } } else if(firstChar == '`') { substituteCommand(exceptionIfNotResolved); } } private void substituteCommand(boolean exceptionIfNotResolved) throws CommandSubstitutionException { if(location + 1 == input.length()) { throw new CommandSubstitutionException("", "Command is missing after `"); } final int endQuote = firstNotEscaped('`', location + 1); if(endQuote - location <= 1) { throw new CommandSubstitutionException(input.substring(location + 1), "Closing ` is missing for " + input.substring(location, Math.min(location + 5, input.length())) + "..."); } final String cmd = input.substring(location + 1, endQuote); final String resolved = Util.getResult(cmdCtx, cmd); final StringBuilder buf = new StringBuilder(input.length() - cmd.length() - 2 + resolved.length()); buf.append(input.substring(0, location)).append(resolved); if (endQuote < input.length() - 1) { buf.append(input.substring(endQuote + 1)); } input = buf.toString(); ch = input.charAt(location); } private int firstNotEscaped(char ch, int start) { final int index = input.indexOf(ch, start); if(index < 0) { return index; } // make sure ch is not escape if(input.charAt(index - 1) == '\\') { int i = index - 2; boolean escaped = true; while(i - start >= 0 && input.charAt(i) == '\\') { --i; escaped = !escaped; } if(escaped) { if(index + 1 < input.length() - 1) { return firstNotEscaped(ch, index + 1); } return -1; } } return index; } private void substituteVariable(boolean exceptionIfNotResolved) throws UnresolvedVariableException { int endIndex = location + 1; char c = input.charAt(endIndex); if(endIndex >= input.length() || !(Character.isJavaIdentifierStart(c) && c != '$')) { // simply '$' return; } while(++endIndex < input.length()) { c = input.charAt(endIndex); if(!(Character.isJavaIdentifierPart(c) && c != '$')) { break; } } final String name = input.substring(location+1, endIndex); final String value = cmdCtx == null ? null : cmdCtx.getVariable(name); if(value == null) { if (exceptionIfNotResolved) { throw new UnresolvedVariableException(name, "Unrecognized variable " + name); } } else { StringBuilder buf = new StringBuilder(input.length() - name.length() + value.length()); buf.append(input.substring(0, location)).append(value); if (endIndex < input.length()) { buf.append(input.substring(endIndex)); } input = buf.toString(); ch = input.charAt(location); } } @Override public boolean replaceSpecialChars() { if(location == 0) { return false; } if(input.charAt(location - 1) != '\\') { return false; } switch(ch) { case 'n': ch = '\n'; break; case 't': ch = '\t'; break; case 'b': ch = '\b'; break; case 'r': ch = '\r'; break; case 'f': ch = '\f'; break; default: return false; } return true; } @Override public boolean isStrict() { return strict; } @Override public ParsingState getState() { return stack.isEmpty() ? initialState : stack.peek(); } @Override public void enterState(ParsingState state) throws CommandFormatException { stack.push(state); callbackHandler.enteredState(this); state.getEnterHandler().handle(this); } @Override public ParsingState leaveState() throws CommandFormatException { stack.peek().getLeaveHandler().handle(this); callbackHandler.leavingState(this); ParsingState pop = stack.pop(); if(!stack.isEmpty()) { stack.peek().getReturnHandler().handle(this); } else { initialState.getReturnHandler().handle(this); } return pop; } @Override public ParsingStateCallbackHandler getCallbackHandler() { return callbackHandler; } @Override public char getCharacter() { return ch; } @Override public int getLocation() { return location; } @Override public void reenterState() throws CommandFormatException { callbackHandler.leavingState(this); ParsingState state = stack.peek(); state.getLeaveHandler().handle(this); callbackHandler.enteredState(this); state.getEnterHandler().handle(this); } @Override public boolean isEndOfContent() { return location >= input.length(); } @Override public String getInput() { return input; } @Override public void advanceLocation(int offset) throws IndexOutOfBoundsException { if(isEndOfContent()) { throw new IndexOutOfBoundsException("Location=" + location + ", offset=" + offset + ", length=" + input.length()); } location += offset; if(location < input.length()) { ch = input.charAt(location); } } @Override public CommandFormatException getError() { return error; } @Override public void setError(CommandFormatException e) { if(error == null) { error = e; } } @Override public void lookFor(char ch) { lookFor.push(ch); } @Override public boolean meetIfLookedFor(char ch) { if(lastMetLookForIndex == location || lookFor.isEmpty() || lookFor.peek() != ch) { return false; } lookFor.pop(); lastMetLookForIndex = location; return true; } @Override public boolean isLookingFor(char c) { return !lookFor.isEmpty() && lookFor.peek() == c; } @Override public void deactivateControl(char c) { if(deactivated != '\u0000') { // just not to use java.util.Set when only '=' is expected // to be deactivated at the moment... throw new IllegalStateException( "Current implementation supports only one deactivated character at a time."); } deactivated = c; } @Override public void activateControl(char c) { if(deactivated == c) { deactivated = '\u0000'; } } @Override public boolean isDeactivated(char c) { return deactivated == c; } } }