/* * Copyright 2015 S. Webber * * 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.oakgp.serialize; import static org.oakgp.Arguments.createArguments; import static org.oakgp.Type.arrayType; import static org.oakgp.Type.bigDecimalType; import static org.oakgp.Type.bigIntegerType; import static org.oakgp.Type.doubleType; import static org.oakgp.Type.integerType; import static org.oakgp.Type.longType; import static org.oakgp.Type.stringType; import static org.oakgp.util.Utils.FALSE_NODE; import static org.oakgp.util.Utils.TRUE_NODE; import static org.oakgp.util.Utils.copyOf; import static org.oakgp.util.Void.VOID_CONSTANT; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; import org.oakgp.Type; import org.oakgp.function.Function; import org.oakgp.function.Signature; import org.oakgp.node.ConstantNode; import org.oakgp.node.FunctionNode; import org.oakgp.node.Node; import org.oakgp.node.VariableNode; import org.oakgp.primitive.VariableSet; /** * Creates {@code Node} instances from {@code String} representations. * <p> * e.g. The {@code String}: * * <pre> * (+ 9 5) * </pre> * * will produce the {@code Node}: * * <pre> * new FunctionNode(new Add(), createArguments(new ConstantNode(9), new ConstantNode(5)) * </pre> */ public final class NodeReader implements Closeable { private static final char FUNCTION_START_CHAR = '('; private static final String FUNCTION_START_STRING = "" + FUNCTION_START_CHAR; private static final char FUNCTION_END_CHAR = ')'; private static final String FUNCTION_END_STRING = "" + FUNCTION_END_CHAR; private static final char STRING_CHAR = '\"'; private static final String STRING_STRING = "" + STRING_CHAR; private static final char ARRAY_START_CHAR = '['; private static final String ARRAY_START_STRING = "" + ARRAY_START_CHAR; private static final char ARRAY_END_CHAR = ']'; private static final String ARRAY_END_STRING = "" + ARRAY_END_CHAR; private final Map<Predicate<String>, java.util.function.Function<String, Node>> readers; private final CharReader cr; private final Function[] functions; private final ConstantNode[] constants; private final VariableSet variableSet; /** * Creates a {@code NodeReader} that reads from the given {@code java.lang.String}. * * @param input * contains the s-expressions, representing programs as tree structures, that will be read to construct new {@code Node} instances * @param functions * a collection of {@code Function}s to use to represent functions read from {@code input} * @param constants * a collection of {@code ConstantNode}s to use to represent constants read from {@code input} * @param variableSet * the variable set to use to represent variables read from {@code input} */ public NodeReader(String input, Function[] functions, ConstantNode[] constants, VariableSet variableSet) { StringReader sr = new StringReader(input); this.cr = new CharReader(new BufferedReader(sr)); this.functions = copyOf(functions); this.constants = copyOf(constants); this.variableSet = variableSet; this.readers = createReaders(); } private Map<Predicate<String>, java.util.function.Function<String, Node>> createReaders() { Map<Predicate<String>, java.util.function.Function<String, Node>> m = new LinkedHashMap<>(); m.put(NodeReader::isVariable, this::getVariable); m.put(s -> "true".equals(s), s -> TRUE_NODE); m.put(s -> "false".equals(s), s -> FALSE_NODE); m.put(s -> "void".equals(s), s -> VOID_CONSTANT); m.put(NodeReader::isLong, this::createLongConstant); m.put(NodeReader::isBigInteger, this::createBigIntegerConstant); m.put(NodeReader::isBigDecimal, this::createBigDecimalConstant); m.put(NodeReader::isDecimalNumber, this::createDoubleConstant); m.put(NodeReader::isNumber, this::createIntegerConstant); m.put(this::isConstant, this::getConstant); m.put(s -> true, this::createFunctionConstant); return m; } /** Creates and returns a {@code Node} which represents the next s-expression read from the input. */ public Node readNode() throws IOException { return nextNode(nextToken()); } private String nextToken() throws IOException { cr.skipWhitespace(); int c = cr.next(); switch (c) { case FUNCTION_START_CHAR: return FUNCTION_START_STRING; case FUNCTION_END_CHAR: return FUNCTION_END_STRING; case STRING_CHAR: return STRING_STRING; case ARRAY_START_CHAR: return ARRAY_START_STRING; case ARRAY_END_CHAR: return ARRAY_END_STRING; default: assertNotEndOfStream(c); StringBuilder sb = new StringBuilder(); do { sb.append((char) c); } while ((c = cr.next()) != -1 && isFunctionIdentifierPart(c)); cr.rewind(c); return sb.toString(); } } private Node nextNode(String firstToken) throws IOException { switch (firstToken) { case FUNCTION_START_STRING: return createFunctionNode(); case STRING_STRING: return createStringConstantNode(); case ARRAY_START_STRING: return createArrayConstantNode(); default: return createNode(firstToken); } } private Node createFunctionNode() throws IOException { String functionName = nextToken(); List<Node> arguments = new ArrayList<>(); List<Type> types = new ArrayList<>(); String nextToken; while (notFunctionEnd(nextToken = nextToken())) { Node n = nextNode(nextToken); arguments.add(n); types.add(n.getType()); } Function function = getFunction(functionName, types); return new FunctionNode(function, createArguments(arguments)); } private boolean notFunctionEnd(String token) { return !FUNCTION_END_STRING.equals(token); } private Function getFunction(String functionName, List<Type> types) { for (Function f : functions) { if (isMatch(functionName, types, f)) { return f; } } throw new IllegalArgumentException("Could not find version of function: " + functionName + " for: " + types + " in: " + Arrays.toString(functions)); } private boolean isMatch(String functionName, List<Type> types, Function f) { return functionName.equals(f.getDisplayName()) && types.equals(f.getSignature().getArgumentTypes()); } private Node createStringConstantNode() throws IOException { StringBuilder sb = new StringBuilder(); int next; while ((next = cr.next()) != STRING_CHAR) { assertNotEndOfStream(next); sb.append((char) next); } return new ConstantNode(sb.toString(), stringType()); } private Node createArrayConstantNode() throws IOException { List<Node> arguments = new ArrayList<>(); String nextToken; Type t = null; while (notArrayEnd(nextToken = nextToken())) { Node n = nextNode(nextToken); if (t == null) { t = n.getType(); } else if (t != n.getType()) { throw new IllegalStateException("Mixed type array elements: " + t + " and " + n.getType()); } arguments.add(n); } return new ConstantNode(createArguments(arguments), arrayType(t)); } private boolean notArrayEnd(String token) { return !ARRAY_END_STRING.equals(token); } private Node createNode(String token) { return readers.entrySet().stream().filter(e -> e.getKey().test(token)).map(Map.Entry::getValue).findFirst() .orElseThrow(() -> new IllegalArgumentException(token)).apply(token); } private boolean isConstant(String token) { return getConstant(token) != null; } private ConstantNode getConstant(String token) { for (ConstantNode n : constants) { if (token.equals(n.toString())) { return n; } } return null; } private VariableNode getVariable(String firstToken) { int id = Integer.parseInt(firstToken.substring(1)); return variableSet.getById(id); } private ConstantNode createIntegerConstant(String token) { return new ConstantNode(Integer.valueOf(token), integerType()); } private ConstantNode createLongConstant(String token) { return new ConstantNode(Long.valueOf(token.substring(0, token.length() - 1)), longType()); } private ConstantNode createDoubleConstant(String token) { return new ConstantNode(Double.valueOf(token.substring(0, token.length())), doubleType()); } private ConstantNode createBigIntegerConstant(String token) { return new ConstantNode(toBigInteger(token.substring(0, token.length() - 1)), bigIntegerType()); } private ConstantNode createBigDecimalConstant(String token) { return new ConstantNode(toBigDecimal(token.substring(0, token.length() - 1)), bigDecimalType()); } private ConstantNode createFunctionConstant(String token) { Function function = getFunction(token); return new ConstantNode(function, getFunctionType(function)); } private Function getFunction(String token) { for (Function f : functions) { if (token.equals(f.getDisplayName())) { return f; } } throw new IllegalArgumentException("Could not find version of function: " + token + " in: " + Arrays.toString(functions)); } private static void assertNotEndOfStream(int c) { if (c == -1) { throw new IllegalStateException(); } } @Override public void close() throws IOException { cr.close(); } /** Returns {@code true} if there is no more data to read from the underlying stream, else {@code false}. */ public boolean isEndOfStream() throws IOException { return cr.isEndOfStream(); } private static Type getFunctionType(Function function) { Signature signature = function.getSignature(); Type[] types = new Type[signature.getArgumentTypesLength() + 1]; types[0] = signature.getReturnType(); for (int i = 1; i < types.length; i++) { types[i] = signature.getArgumentType(i - 1); } return Type.functionType(types); } private static boolean isVariable(String token) { return token.startsWith("v") && isNumber(token.substring(1)); } private static boolean isLong(String token) { return isNumber(token) && token.endsWith("L"); } private static boolean isBigInteger(String token) { return isNumber(token) && token.endsWith("I"); } private static boolean isBigDecimal(String token) { return isNumber(token) && token.endsWith("D"); } private static boolean isDecimalNumber(String token) { return isNumber(token) && token.contains("."); } private static boolean isNumber(String token) { char firstChar = token.charAt(0); if (firstChar == '-') { return token.length() > 1 && isNumber(token.charAt(1)); } else { return isNumber(firstChar); } } private static boolean isNumber(char c) { return c >= '0' && c <= '9'; } /** * Returns {@code true} if the given {@code String} is suitable for use as the display name of a {@code Function}, else {code false}. * <p> * A {@code String} is considered suitable as a display name for a {@code Function} - as returned from {@link Function#getDisplayName()} - if it can be * successfully parsed by a {@code NodeReader}. Suitable function names do not start with a number or contain any of the special characters used to represent * the start or end of functions (i.e. {@code (} and {@code )}), arrays (i.e. {@code [} and {@code ]}) or strings (i.e. {@code "}). */ public static boolean isValidDisplayName(String displayName) { if (displayName == null || displayName.length() == 0 || isNumber(displayName)) { return false; } for (char c : displayName.toCharArray()) { if (!isFunctionIdentifierPart(c)) { return false; } } return true; } private static boolean isFunctionIdentifierPart(int c) { return c != FUNCTION_END_CHAR && c != FUNCTION_START_CHAR && c != ARRAY_START_CHAR && c != ARRAY_END_CHAR && c != STRING_CHAR && !Character.isWhitespace(c); } private static BigInteger toBigInteger(String number) { switch (number) { case "0": return BigInteger.ZERO; case "1": return BigInteger.ONE; case "10": return BigInteger.TEN; default: return new BigInteger(number); } } private static BigDecimal toBigDecimal(String number) { switch (number) { case "0": return BigDecimal.ZERO; case "1": return BigDecimal.ONE; case "10": return BigDecimal.TEN; default: return new BigDecimal(number); } } private static final class CharReader implements Closeable { private final BufferedReader br; private int previous = -1; CharReader(BufferedReader br) { this.br = br; } boolean isEndOfStream() throws IOException { skipWhitespace(); return previous == -1 && (previous = br.read()) == -1; } void skipWhitespace() throws IOException { int next = previous == -1 ? br.read() : previous; while (next != -1 && Character.isWhitespace(next)) { next = br.read(); } rewind(next); } int next() throws IOException { if (previous == -1) { return br.read(); } else { int result = previous; previous = -1; return result; } } void rewind(int c) { previous = c; } @Override public void close() throws IOException { br.close(); } } }