package org.inferred.freebuilder.processor.util; import static org.inferred.freebuilder.processor.util.feature.GuavaLibrary.GUAVA; import static org.inferred.freebuilder.processor.util.feature.SourceLevel.SOURCE_LEVEL; import com.google.common.base.Preconditions; import com.google.common.escape.Escaper; import com.google.common.escape.Escapers; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; /** * Code snippets that call or emulate Guava's {@link Preconditions} methods. */ public class PreconditionExcerpts { private static final class GuavaCheckExcerpt extends Excerpt { private final Object[] args; private final Object condition; private final String message; private final String methodName; private final Class<? extends RuntimeException> exceptionType; private GuavaCheckExcerpt(Object[] args, Object condition, String message, String methodName, Class<? extends RuntimeException> exceptionType) { this.args = args; this.condition = condition; this.message = message; this.methodName = methodName; this.exceptionType = exceptionType; } @Override public void addTo(SourceBuilder code) { if (code.feature(GUAVA).isAvailable()) { code.add("%s.%s(%s, \"%s\"", Preconditions.class, methodName, condition, JAVA_STRING_ESCAPER.escape(message)); for (Object arg : args) { code.add(", %s", arg); } code.add(");\n"); } else { List<Excerpt> escapedArgs = new ArrayList<Excerpt>(); for (final Object arg : args) { escapedArgs.add(Excerpts.add("\" + %s + \"", arg)); } String messageConcatenated = code.subBuilder() .add("\"" + JAVA_STRING_ESCAPER.escape(message) + "\"", escapedArgs.toArray()) .toString() .replace("\"\" + ", "") .replace(" + \"\"", ""); code.addLine("if (%s) {", negate(code, condition)) .addLine(" throw new %s(%s);", exceptionType, messageConcatenated) .addLine("}"); } } @Override protected void addFields(FieldReceiver fields) { fields.add("methodName", methodName); fields.add("exceptionType", exceptionType); fields.add("condition", condition); fields.add("message", message); fields.add("args", Arrays.asList(args)); } } private static final class CheckNotNullPreambleExcerpt extends Excerpt { private final Object reference; private CheckNotNullPreambleExcerpt(Object reference) { this.reference = reference; } @Override public void addTo(SourceBuilder code) { if (code.feature(GUAVA).isAvailable()) { // No preamble needed } else if (code.feature(SOURCE_LEVEL).javaUtilObjects().isPresent()) { // No preamble needed } else { code.addLine("if (%s == null) {", reference) .addLine(" throw new NullPointerException();") .addLine("}"); } } @Override protected void addFields(FieldReceiver fields) { fields.add("reference", reference); } } private static final class CheckNotNullInlineExcerpt extends Excerpt { private final Object reference; private CheckNotNullInlineExcerpt(Object reference) { this.reference = reference; } @Override public void addTo(SourceBuilder code) { if (code.feature(GUAVA).isAvailable()) { code.add("%s.checkNotNull(%s)", Preconditions.class, reference); } else if (code.feature(SOURCE_LEVEL).javaUtilObjects().isPresent()) { code.add("%s.requireNonNull(%s)", code.feature(SOURCE_LEVEL).javaUtilObjects().get(), reference); } else { code.add("%s", reference); } } @Override protected void addFields(FieldReceiver fields) { fields.add("reference", reference); } } private static final class CheckNotNullExcerpt extends Excerpt { private final Object reference; private CheckNotNullExcerpt(Object reference) { this.reference = reference; } @Override public void addTo(SourceBuilder code) { if (code.feature(GUAVA).isAvailable()) { code.addLine("%s.checkNotNull(%s);", Preconditions.class, reference); } else if (code.feature(SOURCE_LEVEL).javaUtilObjects().isPresent()) { code.addLine("%s.requireNonNull(%s);", code.feature(SOURCE_LEVEL).javaUtilObjects().get(), reference); } else { code.addLine("if (%s == null) {", reference) .addLine(" throw new NullPointerException();") .addLine("}"); } } @Override protected void addFields(FieldReceiver fields) { fields.add("reference", reference); } } private static final Escaper JAVA_STRING_ESCAPER = Escapers.builder() .addEscape('"', "\"") .addEscape('\\', "\\\\") .addEscape('\n', "\\n") .build(); /** * Matches all operators with a lower precedence than unary negation (!). * * <p>False positives are acceptable, as the only downside is putting unnecessary brackets around * the condition when Guava is not available, so a simple check for offending characters is fine, * even though they might actually be in a string. */ private static final Pattern ANY_OPERATOR = Pattern.compile("[+=<>!&^|?:]|\\binstanceof\\b"); /** * Returns an excerpt of the preamble required to emulate an inline call to Guava's * {@link Preconditions#checkNotNull(Object)} method. * * <p>If Guava or Java 7's Objects are available, no preamble will be generated. Otherwise, the * check will be done out-of-line with an if block in this excerpt. * * <p>If you use this, you <b>must</b> also use {@link #checkNotNullInline} to allow the check to * be performed inline when possible: * * <pre>code.add(checkNotNullPreamble("value")) * .addLine("this.property = %s;", checkNotNullInline("value"));</pre> * * @param reference an excerpt containing the reference to pass to the checkNotNull method */ public static Excerpt checkNotNullPreamble(final Object reference) { return new CheckNotNullPreambleExcerpt(reference); } /** * Returns an excerpt equivalent to an inline call to Guava's * {@link Preconditions#checkNotNull(Object)}. * <ul> * <li>If Guava is available, Preconditions.checkNotNull will be used. * <li>If Objects.requireNonNull is available, it will be used if Guava cannot be. * <li>If neither are available, the check will be done out-of-line with an if block. * </ul> * * <p>If you use this, you <b>must</b> also use {@link #checkNotNullPreamble} to allow the * check to be performed out-of-line if necessary: * * <pre>code.add(checkNotNullPreamble("value")) * .addLine("this.property = %s;", checkNotNullInline("value"));</pre> * * @param reference an excerpt containing the reference to pass to the checkNotNull method */ public static Excerpt checkNotNullInline(final Object reference) { return new CheckNotNullInlineExcerpt(reference); } /** * Returns an excerpt equivalent to Guava's {@link Preconditions#checkNotNull(Object)}. * <ul> * <li>If Guava is available, Preconditions.checkNotNull will be used. * <li>If Objects.requireNonNull is available, it will be used if Guava cannot be. * <li>If neither are available, the check will be done with an if block. * </ul> * * <pre>code.add(checkNotNull("key")) * .add(checkNotNull("value")) * .addLine("map.put(key, value);");</pre> * * @param reference an excerpt containing the reference to pass to the checkNotNull method */ public static Excerpt checkNotNull(final Object reference) { return new CheckNotNullExcerpt(reference); } /** * Returns an excerpt equivalent to Guava's * {@link Preconditions#checkArgument(boolean, String, Object...)}. * <ul> * <li>If Guava is available, Preconditions.checkArgument will be used. * <li>Otherwise, the check will be done with an if block. * </ul> * * <pre>code.add(checkArgument("age >= 0", "age must be non-negative (got %s)", "age"));</pre> * * @param condition an excerpt containing the expression to pass to the checkArgument method * @param message the error message template to pass to the checkArgument method * @param args excerpts containing the error message arguments to pass to the checkArgument method */ public static Excerpt checkArgument( final Object condition, final String message, final Object... args) { return new GuavaCheckExcerpt( args, condition, message, "checkArgument", IllegalArgumentException.class); } /** * Returns an excerpt equivalent to Guava's * {@link Preconditions#checkState(boolean, String, Object...)}. * <ul> * <li>If Guava is available, Preconditions.checkState will be used. * <li>Otherwise, the check will be done with an if block. * </ul> * * <pre>code.add(checkState("start < end", * "start must be before end (got %s and %s)", "start", "end"));</pre> * * @param condition an excerpt containing the expression to pass to the checkState method * @param message the error message template to pass to the checkState method * @param args excerpts containing the error message arguments to pass to the checkState method */ public static Excerpt checkState( final Object condition, final String message, final Object... args) { return new GuavaCheckExcerpt( args, condition, message, "checkState", IllegalStateException.class); } /** * Negates {@code condition}, removing unnecessary brackets and double-negatives if possible. */ private static String negate(SourceBuilder code, Object condition) { SourceStringBuilder subBuilder = code.subBuilder(); subBuilder.add("%s", condition); String conditionText = subBuilder.toString(); if (conditionText.startsWith("!")) { return conditionText.substring(1); } else if (ANY_OPERATOR.matcher(conditionText).find()) { // The condition might already enclosed in a bracket, but we can't simply check for opening // and closing brackets at the start and end of the string, as that misses cases like // (a || b) && (c || d). Attempting to determine if the initial and closing bracket are paired // requires understanding character constants and strings constants, so for simplicity we // just add unnecessary brackets. return "!(" + conditionText + ")"; } else { return "!" + conditionText; } } private PreconditionExcerpts() {} }