All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.javapoet.CodeBlock Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright (C) 2015 Square, 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 org.springframework.javapoet;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collector;
import java.util.stream.StreamSupport;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;

import static org.springframework.javapoet.Util.checkArgument;

/**
 * A fragment of a .java file, potentially containing declarations, statements, and documentation.
 * Code blocks are not necessarily well-formed Java code, and are not validated. This class assumes
 * javac will check correctness later!
 *
 * 

Code blocks support placeholders like {@link java.text.Format}. Where {@link String#format} * uses percent {@code %} to reference target values, this class uses dollar sign {@code $} and has * its own set of permitted placeholders: * *

    *
  • {@code $L} emits a literal value with no escaping. Arguments for literals may be * strings, primitives, {@linkplain TypeSpec type declarations}, {@linkplain AnnotationSpec * annotations} and even other code blocks. *
  • {@code $N} emits a name, using name collision avoidance where necessary. Arguments * for names may be strings (actually any {@linkplain CharSequence character sequence}), * {@linkplain ParameterSpec parameters}, {@linkplain FieldSpec fields}, {@linkplain * MethodSpec methods}, and {@linkplain TypeSpec types}. *
  • {@code $S} escapes the value as a string, wraps it with double quotes, and emits * that. For example, {@code 6" sandwich} is emitted {@code "6\" sandwich"}. *
  • {@code $T} emits a type reference. Types will be imported if possible. Arguments * for types may be {@linkplain Class classes}, {@linkplain javax.lang.model.type.TypeMirror ,* type mirrors}, and {@linkplain javax.lang.model.element.Element elements}. *
  • {@code $$} emits a dollar sign. *
  • {@code $W} emits a space or a newline, depending on its position on the line. This prefers * to wrap lines before 100 columns. *
  • {@code $Z} acts as a zero-width space. This prefers to wrap lines before 100 columns. *
  • {@code $>} increases the indentation level. *
  • {@code $<} decreases the indentation level. *
  • {@code $[} begins a statement. For multiline statements, every line after the first line * is double-indented. *
  • {@code $]} ends a statement. *
*/ public final class CodeBlock { private static final Pattern NAMED_ARGUMENT = Pattern.compile("\\$(?[\\w_]+):(?[\\w]).*"); private static final Pattern LOWERCASE = Pattern.compile("[a-z]+[\\w_]*"); /** A heterogeneous list containing string literals and value placeholders. */ final List formatParts; final List args; private CodeBlock(Builder builder) { this.formatParts = Util.immutableList(builder.formatParts); this.args = Util.immutableList(builder.args); } public boolean isEmpty() { return formatParts.isEmpty(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null) return false; if (getClass() != o.getClass()) return false; return toString().equals(o.toString()); } @Override public int hashCode() { return toString().hashCode(); } @Override public String toString() { StringBuilder out = new StringBuilder(); try { new CodeWriter(out).emit(this); return out.toString(); } catch (IOException e) { throw new AssertionError(); } } public static CodeBlock of(String format, Object... args) { return new Builder().add(format, args).build(); } /** * Joins {@code codeBlocks} into a single {@link CodeBlock}, each separated by {@code separator}. * For example, joining {@code String s}, {@code Object o} and {@code int i} using {@code ", "} * would produce {@code String s, Object o, int i}. */ public static CodeBlock join(Iterable codeBlocks, String separator) { return StreamSupport.stream(codeBlocks.spliterator(), false).collect(joining(separator)); } /** * A {@link Collector} implementation that joins {@link CodeBlock} instances together into one * separated by {@code separator}. For example, joining {@code String s}, {@code Object o} and * {@code int i} using {@code ", "} would produce {@code String s, Object o, int i}. */ public static Collector joining(String separator) { return Collector.of( () -> new CodeBlockJoiner(separator, builder()), CodeBlockJoiner::add, CodeBlockJoiner::merge, CodeBlockJoiner::join); } /** * A {@link Collector} implementation that joins {@link CodeBlock} instances together into one * separated by {@code separator}. For example, joining {@code String s}, {@code Object o} and * {@code int i} using {@code ", "} would produce {@code String s, Object o, int i}. */ public static Collector joining( String separator, String prefix, String suffix) { Builder builder = builder().add("$N", prefix); return Collector.of( () -> new CodeBlockJoiner(separator, builder), CodeBlockJoiner::add, CodeBlockJoiner::merge, joiner -> { builder.add(CodeBlock.of("$N", suffix)); return joiner.join(); }); } public static Builder builder() { return new Builder(); } public Builder toBuilder() { Builder builder = new Builder(); builder.formatParts.addAll(formatParts); builder.args.addAll(args); return builder; } public static final class Builder { final List formatParts = new ArrayList<>(); final List args = new ArrayList<>(); private Builder() { } public boolean isEmpty() { return formatParts.isEmpty(); } /** * Adds code using named arguments. * *

Named arguments specify their name after the '$' followed by : and the corresponding type * character. Argument names consist of characters in {@code a-z, A-Z, 0-9, and _} and must * start with a lowercase character. * *

For example, to refer to the type {@link java.lang.Integer} with the argument name {@code * clazz} use a format string containing {@code $clazz:T} and include the key {@code clazz} with * value {@code java.lang.Integer.class} in the argument map. */ public Builder addNamed(String format, Map arguments) { int p = 0; for (String argument : arguments.keySet()) { checkArgument(LOWERCASE.matcher(argument).matches(), "argument '%s' must start with a lowercase character", argument); } while (p < format.length()) { int nextP = format.indexOf("$", p); if (nextP == -1) { formatParts.add(format.substring(p)); break; } if (p != nextP) { formatParts.add(format.substring(p, nextP)); p = nextP; } Matcher matcher = null; int colon = format.indexOf(':', p); if (colon != -1) { int endIndex = Math.min(colon + 2, format.length()); matcher = NAMED_ARGUMENT.matcher(format.substring(p, endIndex)); } if (matcher != null && matcher.lookingAt()) { String argumentName = matcher.group("argumentName"); checkArgument(arguments.containsKey(argumentName), "Missing named argument for $%s", argumentName); char formatChar = matcher.group("typeChar").charAt(0); addArgument(format, formatChar, arguments.get(argumentName)); formatParts.add("$" + formatChar); p += matcher.regionEnd(); } else { checkArgument(p < format.length() - 1, "dangling $ at end"); checkArgument(isNoArgPlaceholder(format.charAt(p + 1)), "unknown format $%s at %s in '%s'", format.charAt(p + 1), p + 1, format); formatParts.add(format.substring(p, p + 2)); p += 2; } } return this; } /** * Add code with positional or relative arguments. * *

Relative arguments map 1:1 with the placeholders in the format string. * *

Positional arguments use an index after the placeholder to identify which argument index * to use. For example, for a literal to reference the 3rd argument: "$3L" (1 based index) * *

Mixing relative and positional arguments in a call to add is invalid and will result in an * error. */ public Builder add(String format, Object... args) { boolean hasRelative = false; boolean hasIndexed = false; int relativeParameterCount = 0; int[] indexedParameterCount = new int[args.length]; for (int p = 0; p < format.length(); ) { if (format.charAt(p) != '$') { int nextP = format.indexOf('$', p + 1); if (nextP == -1) nextP = format.length(); formatParts.add(format.substring(p, nextP)); p = nextP; continue; } p++; // '$'. // Consume zero or more digits, leaving 'c' as the first non-digit char after the '$'. int indexStart = p; char c; do { checkArgument(p < format.length(), "dangling format characters in '%s'", format); c = format.charAt(p++); } while (c >= '0' && c <= '9'); int indexEnd = p - 1; // If 'c' doesn't take an argument, we're done. if (isNoArgPlaceholder(c)) { checkArgument( indexStart == indexEnd, "$$, $>, $<, $[, $], $W, and $Z may not have an index"); formatParts.add("$" + c); continue; } // Find either the indexed argument, or the relative argument. (0-based). int index; if (indexStart < indexEnd) { index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1; hasIndexed = true; if (args.length > 0) { indexedParameterCount[index % args.length]++; // modulo is needed, checked below anyway } } else { index = relativeParameterCount; hasRelative = true; relativeParameterCount++; } checkArgument(index >= 0 && index < args.length, "index %d for '%s' not in range (received %s arguments)", index + 1, format.substring(indexStart - 1, indexEnd + 1), args.length); checkArgument(!hasIndexed || !hasRelative, "cannot mix indexed and positional parameters"); addArgument(format, c, args[index]); formatParts.add("$" + c); } if (hasRelative) { checkArgument(relativeParameterCount >= args.length, "unused arguments: expected %s, received %s", relativeParameterCount, args.length); } if (hasIndexed) { List unused = new ArrayList<>(); for (int i = 0; i < args.length; i++) { if (indexedParameterCount[i] == 0) { unused.add("$" + (i + 1)); } } String s = unused.size() == 1 ? "" : "s"; checkArgument(unused.isEmpty(), "unused argument%s: %s", s, String.join(", ", unused)); } return this; } private boolean isNoArgPlaceholder(char c) { return c == '$' || c == '>' || c == '<' || c == '[' || c == ']' || c == 'W' || c == 'Z'; } private void addArgument(String format, char c, Object arg) { switch (c) { case 'N': this.args.add(argToName(arg)); break; case 'L': this.args.add(argToLiteral(arg)); break; case 'S': this.args.add(argToString(arg)); break; case 'T': this.args.add(argToType(arg)); break; default: throw new IllegalArgumentException( String.format("invalid format string: '%s'", format)); } } private String argToName(Object o) { if (o instanceof CharSequence) return o.toString(); if (o instanceof ParameterSpec) return ((ParameterSpec) o).name; if (o instanceof FieldSpec) return ((FieldSpec) o).name; if (o instanceof MethodSpec) return ((MethodSpec) o).name; if (o instanceof TypeSpec) return ((TypeSpec) o).name; throw new IllegalArgumentException("expected name but was " + o); } private Object argToLiteral(Object o) { return o; } private String argToString(Object o) { return o != null ? String.valueOf(o) : null; } private TypeName argToType(Object o) { if (o instanceof TypeName) return (TypeName) o; if (o instanceof TypeMirror) return TypeName.get((TypeMirror) o); if (o instanceof Element) return TypeName.get(((Element) o).asType()); if (o instanceof Type) return TypeName.get((Type) o); throw new IllegalArgumentException("expected type but was " + o); } /** * @param controlFlow the control flow construct and its code, such as "if (foo == 5)". * Shouldn't contain braces or newline characters. */ public Builder beginControlFlow(String controlFlow, Object... args) { add(controlFlow + " {\n", args); indent(); return this; } /** * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". * Shouldn't contain braces or newline characters. */ public Builder nextControlFlow(String controlFlow, Object... args) { unindent(); add("} " + controlFlow + " {\n", args); indent(); return this; } public Builder endControlFlow() { unindent(); add("}\n"); return this; } /** * @param controlFlow the optional control flow construct and its code, such as * "while(foo == 20)". Only used for "do/while" control flows. */ public Builder endControlFlow(String controlFlow, Object... args) { unindent(); add("} " + controlFlow + ";\n", args); return this; } public Builder addStatement(String format, Object... args) { add("$["); add(format, args); add(";\n$]"); return this; } public Builder addStatement(CodeBlock codeBlock) { return addStatement("$L", codeBlock); } public Builder add(CodeBlock codeBlock) { formatParts.addAll(codeBlock.formatParts); args.addAll(codeBlock.args); return this; } public Builder indent() { this.formatParts.add("$>"); return this; } public Builder unindent() { this.formatParts.add("$<"); return this; } public Builder clear() { formatParts.clear(); args.clear(); return this; } public CodeBlock build() { return new CodeBlock(this); } } private static final class CodeBlockJoiner { private final String delimiter; private final Builder builder; private boolean first = true; CodeBlockJoiner(String delimiter, Builder builder) { this.delimiter = delimiter; this.builder = builder; } CodeBlockJoiner add(CodeBlock codeBlock) { if (!first) { builder.add(delimiter); } first = false; builder.add(codeBlock); return this; } CodeBlockJoiner merge(CodeBlockJoiner other) { CodeBlock otherBlock = other.builder.build(); if (!otherBlock.isEmpty()) { add(otherBlock); } return this; } CodeBlock join() { return builder.build(); } } }