com.google.api.tools.framework.snippet.SnippetSet.java Source code

Java tutorial

Introduction

Here is the source code for com.google.api.tools.framework.snippet.SnippetSet.java

Source

/*
 * Copyright (C) 2016 Google Inc.
 *
 * 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 com.google.api.tools.framework.snippet;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.api.tools.framework.snippet.Snippet.SnippetKind;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import com.google.common.primitives.Primitives;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

/**
 * Represents a snippet set.
 *
 * <p>
 *
 * <p>Snippets are a templating system tailored for code generation. In contrast to other
 * templating approaches, snippets produce structured documents based on the principles of a
 * Wadler-Lindig pretty printer (see {@link Doc}). They are thus better suited for producing source
 * code with spacing and indentation requirements. The decoupling of layout of the snippet code
 * from the generated source also allows for producing better readable snippet definitions.
 * Furthermore, snippets support smooth interop with Java, and a rich set of control structures.
 *
 * <p>
 *
 * <p>A snippet is defined by a declaration as follows:
 *
 * <p><pre>
 *   {@literal @}snippet add(x,y)
 *      &#123;@x} + &#123;@y}
 *   {@literal @}end
 * </pre>
 *
 * <p>Evaluating {@code add(1,2)} will produce {@code 1 + 2}. More precisely, it will produce
 * {@code Doc.text("1").add(Doc.text(" + ")).add(Doc.text("2"))}.
 *
 * <p>
 *
 * <p>Unless specified otherwise, all elements in a snippet are joined within a vertical group with
 * nesting of 0 and the separator {@link Doc#BREAK} for a line break. This can be overridden in the
 * snippet header, for example:
 *
 * <p><pre>
 *   {@literal @}snippet add(x,y) auto 4
 *      &#123;@x}
 *      + &#123;@y}
 *   {@literal @}end
 * </pre>
 *
 * <p>The layout specification following the parameter list of a snippet has the following general
 * syntax:
 *
 * <p><pre>
 *   [ fill | vertical | horizontal | auto ] [ NUMBER ] [ on EXPR ]
 * </pre>
 *
 * <p>Fill, vertical, horizontal, and auto describe the grouping mode (see
 * {@link Doc#group(Doc.GroupKind, Doc)} for definitions), NUMBER the indentation inserted after
 * the first break, and EXPR the separator to use (expressions are described below). The default is
 * {@code vertical 0 BREAK}. A snippet output group is wrapped with {@link Doc#align()}. See the
 * documentation of {@link Doc} for details, or the papers cited there.
 *
 * <p>
 *
 * <p>Indentation of subsequent lines in a snippet definition will be trimmed based on the
 * indentation of the first line. Therefore, nesting for control structures does not introduce
 * indentation. However, in the subsequent definition the inner block will be indented as expected:
 *
 * <p><pre>
 *   {@literal @}snippet block(stm)
 *      {
 *        &#123;@stm}
 *      }
 *   {@literal @}end
 * </pre>
 *
 * <p>Lines in the snippet source can be joined to avoid overflow. Such joined lines appear as one
 * logical line to the snippet engine. For example:
 *
 * <p><pre>
 *   {@literal @}snippet add(x,y) auto 4
 *      &#123;@x} \
 *        + &#123;@y}
 *   {@literal @}end
 * </pre>
 *
 * <p>Leading space from a line continuation is trimmed, however, any space before the {@code \} is
 * not. In order to escape a backslash, use {@code @\}.
 *
 * <p>Snippet code can be documented with {@code #} comments. These can appear both inside and
 * outside snippet definitions:
 *
 * <p><pre>
 *   # Renders an advanced math equation.
 *   {@literal @}snippet add(x,y)
 *     # This is the equation.
 *      &#123;@x} + &#123;@y}
 *   {@literal @}end
 * </pre>
 *
 * Inline comments (comments on the same line as executed code) are not supported.
 *
 * <p>
 *
 * <p>A snippet expression is a text enclosed as &#123;@expr}. Expressions evaluate to objects and
 * are finally converted to strings before inserted into the snippet output. The following
 * expression forms are supported:
 *
 * <ul>
 *
 * <li>{@code literal}: an integer or a {@code "}-quoted string. A string denotes a value of type
 * {@link Doc}, and {@code Doc} methods can be called on it.
 *
 * <li>{@code @@}: evaluates to {@code @}.
 *
 * <li>{@code var}: a variable reference. Variables are introduced by parameters, iterators, and
 * globals which are passed into a snippet set at construction time. The predefined variables
 * {@code BREAK}, {@code SOFT_BREAK}, {@code EMPTY} correspond to the according constants in
 * {@link Doc}. {@code TRUE} and {@code FALSE} denote boolean values.
 *
 * <li>{@code name(expr1,...,exprN)}: a call to another snippet.
 *
 * <li>{@code expr.name}: selects the value of a public field in the Java object denoted by
 * {@code expr}, or calls a zero-parameter method of this name.
 *
 * <li>{@code expr.name(expr1,...,exprN)}: calls a public method on the Java object denoted by
 * {@code expr}.
 *
 * <li>{@code expr1 R expr2}, where {@code R} is any of {@code ==}, {@code !=}, {@code &lt;},
 * {@code &lt;=}, {@code >}, {@code >=}. Calls the comparison relation on the operands, resulting
 * in either the constant TRUE or FALSE. For comparison, a {@link Doc} or enum value will be first
 * converted to a string. After that, operands are either compared with their equals method or, for
 * metric relations, must have the same type and implement the {@link Comparable} interface.
 *
 * </ul>
 *
 * <p>
 *
 * <p>Values passed and returned to Java methods are converted on the fly to strings, numbers, and
 * enum values as demanded by the context type. When a method is applied on an iterable and the
 * name cannot be resolved, an attempt is made to wrap the iterable in a {@link FluentIterable},
 * making methods like {@code append}, {@code first}, etc. available.
 *
 * <p>
 *
 * <p>A few control structures are supported by a snippet. A conditional is written as follows,
 * where the else-part is optional, and the then-part is taken if the condition expression
 * evaluates to a value depending on its type: {@code true} for boolean, a non-zero value for
 * numbers, a non-empty value for strings, a document which contains more than whitespace, or an
 * iterable which contains at least one element:
 *
 * <p><pre>
 *   {@literal @}if expr
 *      ...
 *   {@literal @}else
 *      ...
 *   {@literal @}end
 * </pre>
 *
 * <p>
 *
 * <p>A join is written as follows: {@code expr1} must evaluate to an iterable. The optional
 * conditional, if specified, forces the iterable to only evaluate on elements where {@code expr2}
 * returns {@code true} or the equivalent (see above). The optional layout specifies how the
 * elements in the body are joined (layout has the same syntax as with snippet definitions):
 *
 * <p><pre>
 *   {@literal @}join var : expr1 [ if expr2 ] [ layout ]
 *      ...
 *   {@literal @}end
 * </pre>
 *
 * <p>
 *
 * <p>A let is written as follows, where {@code var} is bound over the scope of the let:
 *
 * <p><pre>
 *   {@literal @}let var1 = expr1, var2 = expr2, ...
 *      ...
 *   {@literal @}end
 * </pre>
 *
 * <p>
 *
 * <p>Finally, a switch is written as such, where the default-part is optional:
 *
 * <p><pre>
 *   {@literal @}switch expr
 *   {@literal @}case expr
 *      ...
 *   {@literal @}case expr
 *      ...
 *   {@literal @}default
 *      ...
 *   {@literal @}end
 * </pre>
 *
 * <p>
 *
 * <p>Snippet sources can extend other snippet sources using the following syntax:
 *
 * <p><pre>
 *   {@literal @}extends "some/file.snip"
 * </pre>
 *
 * <p>All extends-clauses must be at the beginning of a snippet source.
 *
 * <p>A snippet can be defined private to a file, which is good practice to specify the interface
 * between different files in a snippet set:
 *
 * <p><pre>
 *   {@literal @}private add(x)
 *      ...
 *   {@literal @}end
 * </pre>
 *
 * <p>Definitions coming from extended sources can be overridden if the override-marker is used:
 *
 * <p><pre>
 *   {@literal @}override add(x)
 *      ...
 *   {@literal @}end
 * </pre>
 *
 * <p>It is not possible to override definitions in the same source. Also, a snippet can only be
 * overridden if it is in the direct extension path. For example, if sources A and B are extended
 * by C, and B attempts to override a definition in A (without extending it explicitly), an error
 * will be produced.
 *
 * <p>
 *
 * <p>A snippet can be declared abstract:
 *
 * <p><pre>
 *   {@literal @}abstract add(x)
 * </pre>
 *
 * <p>An abstract snippet has no body. Attempting to evaluate an abstract snippet results in a
 * runtime error.
 *
 * <p>
 *
 * <p>The {@link #bind(Class, Map)} method allows to bind an interface to a snippet set,
 * implementing this interface via the methods in the set. This allows for a typed access to the
 * snippet definitions from Java. The snippet engine attempts to convert to and from types used in
 * the interface, using standard string-based conversion methods if needed.
 *
 * <p>
 *
 * <p>A runtime error is raised if a method in an interface cannot be bound to any snippet. It is
 * allowed to bind an abstract snippet, however. An attempt to evaluate it will result in a runtime
 * error. Hence, abstract snippet are a way to express that certain functionality stays
 * unimplemented in a snippet set.
 *
 * <p>
 *
 * <p>It is good practice to declare a snippet which is not intended to be bound to an interface
 * and not used by other snippet files as private:
 *
 * <p><pre>
 *   {@literal @}private add(x)
 *      ...
 *   {@literal @}end
 * </pre>
 *
 * <p>This snippet will be only available for calls from the same snippet file. Its name is unique
 * to the file and cannot clash with similar named snippets from other files.
 */
public class SnippetSet {

    /**
     * An interface supplying source input for the {@literal @}extends command.
     */
    public interface InputSupplier {

        /**
         * Resolve the given snippet set name into a sequence of source lines.
         */
        @Nullable
        Iterable<String> readInput(String snippetSetName) throws IOException;
    }

    /**
     * Represents an issue (error) with either snippet parsing or runtime evaluation.
     */
    @AutoValue
    public abstract static class Issue {

        /**
         * The location of the issue.
         */
        public abstract Location location();

        /**
         * The message describing the issue.
         */
        public abstract String message();

        static Issue create(Location location, String message, Object... args) {
            return new AutoValue_SnippetSet_Issue(location, String.format(message, args));
        }

        @Override
        public String toString() {
            return String.format("%s:%s: %s", location().baseInputName(), location().lineNo(), message());
        }
    }

    /**
     * Represents a parsing error, with a collection of issues.
     */
    public static class ParseException extends Exception {
        private final ImmutableList<Issue> issues;

        ParseException(Iterable<Issue> issues) {
            super(String.format("snippet parsing error(s):%n%s", Joiner.on(String.format("%n")).join(issues)));
            this.issues = ImmutableList.copyOf(issues);
        }

        /**
         * The issues associated with this exception.
         */
        public List<Issue> getIssues() {
            return issues;
        }
    }

    /**
     * Represents an evaluation error, with an issue describing it.
     */
    public static class EvalException extends RuntimeException {
        private final Issue issue;

        EvalException(Issue issue) {
            this(issue, null);
        }

        EvalException(Location location, String message, Object... args) {
            this(Issue.create(location, message, args), null);
        }

        EvalException(Issue issue, Throwable cause) {
            super("snippet evaluation error: " + issue.toString(), cause);
            this.issue = issue;
        }

        /**
         * Returns the issue describing this error.
         */
        public Issue getIssue() {
            return issue;
        }
    }

    /**
     * Represents a key to identify a snippet by its name and number of parameters.
     */
    @AutoValue
    abstract static class SnippetKey {
        abstract String name();

        abstract int arity();

        static SnippetKey create(String name, int arity) {
            return new AutoValue_SnippetSet_SnippetKey(name, arity);
        }
    }

    /**
     * Parses the input and returns a snippet set.
     */
    public static SnippetSet parse(InputSupplier supplier, String inputName) throws ParseException {
        SnippetParser parser = new SnippetParser(supplier, inputName);
        parser.parse();
        if (!parser.errors().isEmpty()) {
            throw new ParseException(parser.errors());
        }
        return parser.result();
    }

    /**
     * Returns an input supplier which works on the given root file. All input names are interpreted
     * relative to this directory, unless absolute.
     */
    public static InputSupplier fileInputSupplier(final File root) {
        return new InputSupplier() {
            @Override
            public Iterable<String> readInput(String snippetSetName) throws IOException {
                File snippetFile = new File(snippetSetName);
                if (!snippetFile.isAbsolute()) {
                    snippetFile = new File(root, snippetSetName);
                }
                return Files.readLines(snippetFile, UTF_8);
            }
        };
    }

    /**
     * Returns an input supplier which works on the given resource root path. All input names are
     * resolved as resource paths relative to this root, and read from the class path.
     */
    public static InputSupplier resourceInputSupplier(final String root) {
        return new InputSupplier() {
            @Override
            public Iterable<String> readInput(String snippetSetName) throws IOException {
                try {
                    return Resources.readLines(Resources.getResource(root + "/" + snippetSetName), UTF_8);
                } catch (Exception e) {
                    throw new IOException(e);
                }
            }
        };
    }

    /**
     * Parses the input from a file and returns a snippet set.
     */
    public static SnippetSet parse(final File file) throws ParseException {
        return parse(fileInputSupplier(file.getParentFile()), file.getName());
    }

    /**
     * Returns the given interface type bound to an instance of {@link SnippetSet} parsed from the
     * given snippet resource file.
     */
    public static <T> T createSnippetInterface(Class<T> interfaceType, String snippetResourceRoot,
            String snippetResource) {
        return createSnippetInterface(interfaceType, snippetResourceRoot, snippetResource, null);
    }

    /**
     * Returns the given interface type bound to an instance of {@link SnippetSet} parsed from the
     * given snippet resource file. The passed map is used when binding the snippet set,
     * providing the globals for snippet execution.
     */
    public static <T> T createSnippetInterface(Class<T> interfaceType, String snippetResourceRoot,
            String snippetResource, @Nullable Map<String, Object> globals) {
        Preconditions.checkNotNull(interfaceType);
        Preconditions.checkNotNull(snippetResourceRoot);
        Preconditions.checkNotNull(snippetResource);

        try {
            SnippetSet snippets = SnippetSet.parse(SnippetSet.resourceInputSupplier(snippetResourceRoot),
                    snippetResource);
            return globals != null ? snippets.bind(interfaceType, globals) : snippets.bind(interfaceType);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Called by the parser.
     */
    SnippetSet() {
    }

    private final Map<SnippetKey, Snippet> definitions = Maps.newLinkedHashMap();

    /**
     * Binds the snippet set to the given interface. For all methods in the interface,
     * a snippet of matching name must exist. The execution of methods on the returned
     * proxy object may throw the runtime exception {@link EvalException}.
     *
     * @throws IllegalArgumentException if not all methods are bound.
     */
    public <T> T bind(Class<T> interfaceType) {
        return bind(interfaceType, ImmutableMap.<String, Object>of());
    }

    /**
     * Like {@link #bind(Class)}, but allows to provide a map which defines
     * global variables accessible by the snippets.
     *
     * @throws IllegalArgumentException if not all methods are bound.
     */
    public <T> T bind(Class<T> interfaceType, Map<String, Object> context) {
        Preconditions.checkArgument(interfaceType.isInterface(), "%s is not a interface", interfaceType);
        return interfaceType.cast(Proxy.newProxyInstance(interfaceType.getClassLoader(),
                new Class<?>[] { interfaceType }, new ProxyHandler(interfaceType, context, this)));
    }

    private static class ProxyHandler implements InvocationHandler {

        private final Class<?> interfaceType;
        private final Map<Method, Snippet> bindings = Maps.newLinkedHashMap();
        private final Context context;

        private ProxyHandler(Class<?> interfaceType, Map<String, Object> globals, SnippetSet snippets) {
            this.interfaceType = interfaceType;
            List<String> errors = Lists.newArrayList();
            for (Method method : interfaceType.getMethods()) {
                SnippetKey key = SnippetKey.create(method.getName(), method.getParameterTypes().length);
                Snippet snippet = snippets.definitions.get(key);
                if (snippet != null) {
                    if (!snippet.isBindable()) {
                        errors.add(String.format("attempt to bind private snippet '%s' against '%s'",
                                snippet.displayName(), method.toString()));
                    }
                    bindings.put(method, snippet);
                } else {
                    errors.add(String.format("no snippet '%s(%s)' found for method '%s'", key.name(), key.arity(),
                            method.toString()));
                }
            }
            if (!errors.isEmpty()) {
                throw new IllegalArgumentException(String.format("Errors binding interface '%s':%n%s",
                        interfaceType, Joiner.on(String.format("%n")).join(errors)));
            }
            this.context = new Context(snippets.definitions, globals);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return tryEval(Location.create("program (via " + interfaceType.getSimpleName() + ")", 1),
                    Primitives.wrap(method.getReturnType()), bindings.get(method), context,
                    args == null ? ImmutableList.of() : ImmutableList.copyOf(args));
        }
    }

    /**
     * Returns a context for snippet evaluation which binds all snippets in this set and
     * the given globals.
     */
    public Context baseContext(Map<String, Object> globals) {
        return new Context(definitions, globals);
    }

    /**
     * Returns a context for snippet evaluation which binds all snippets in this set.
     */
    public Context baseContext() {
        return new Context(definitions);
    }

    /**
     * Evaluates the named snippet with given context and requested result type. Returns
     * a value of the requested type.
     *
     * @throws EvalException
     */
    public <T> T eval(Class<T> requestedType, String name, Context context, List<Object> args) {
        Snippet snippet = definitions.get(SnippetKey.create(name, args.size()));
        if (snippet == null) {
            throw new EvalException(Location.create("program (via eval)", 1), "snippet '%s(%s)' undefined", name,
                    args.size());
        }
        return tryEval(Location.create("program", 1), requestedType, snippet, context, args);
    }

    /**
     * Shortcut for evaluating a snippet with the {@link #baseContext()} into a {@link Doc}.
     *
     * @throws EvalException
     */
    public Doc eval(String name, Object... args) {
        return eval(Doc.class, name, baseContext(), ImmutableList.copyOf(args));
    }

    /**
     * Helper for snippet evaluation.
     *
     * @throws EvalException
     */
    static <T> T tryEval(Location location, Class<T> requestedType, Snippet snippet, Context context,
            List<?> args) {
        if (snippet.kind() == SnippetKind.ABSTRACT) {
            throw new EvalException(location, "attempt to evaluate abstract snippet '%s'", snippet.displayName());
        }

        Doc result = snippet.eval(context, args);
        return requestedType.cast(Values.convert(location, requestedType, result));
    }

    /**
     * Adds the snippet to the set.
     */
    void add(Snippet def) {
        definitions.put(SnippetKey.create(def.name(), def.params().size()), def);
    }

    /**
     * Returns the current definition of the snippet in the set.
     */
    @Nullable
    Snippet get(String name, int size) {
        return definitions.get(SnippetKey.create(name, size));
    }
}