/*
* Copyright 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.template.soy.jbcsrc;
import static com.google.template.soy.jbcsrc.BytecodeUtils.SANITIZED_CONTENT_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.SOY_VALUE_PROVIDER_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.STRING_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;
import static com.google.template.soy.jbcsrc.BytecodeUtils.isPrimitive;
import static com.google.template.soy.jbcsrc.BytecodeUtils.numericConversion;
import static com.google.template.soy.jbcsrc.BytecodeUtils.unboxUnchecked;
import static com.google.template.soy.types.proto.JavaQualifiedNames.getFieldName;
import static com.google.template.soy.types.proto.JavaQualifiedNames.underscoresToCamelCase;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.html.types.SafeHtmlProto;
import com.google.common.html.types.SafeScriptProto;
import com.google.common.html.types.SafeStyleProto;
import com.google.common.html.types.SafeStyleSheetProto;
import com.google.common.html.types.SafeUrlProto;
import com.google.common.html.types.TrustedResourceUrlProto;
import com.google.common.io.BaseEncoding;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.EnumDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
import com.google.protobuf.Descriptors.FileDescriptor.Syntax;
import com.google.protobuf.Descriptors.OneofDescriptor;
import com.google.protobuf.Extension;
import com.google.protobuf.ExtensionLite;
import com.google.protobuf.GeneratedMessage.ExtendableBuilder;
import com.google.protobuf.GeneratedMessage.ExtendableMessage;
import com.google.protobuf.GeneratedMessage.GeneratedExtension;
import com.google.protobuf.Message;
import com.google.protobuf.ProtocolMessageEnum;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.exprtree.FieldAccessNode;
import com.google.template.soy.exprtree.ProtoInitNode;
import com.google.template.soy.jbcsrc.Expression.Feature;
import com.google.template.soy.jbcsrc.TemplateVariableManager.Scope;
import com.google.template.soy.jbcsrc.TemplateVariableManager.Variable;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.aggregate.ListType;
import com.google.template.soy.types.primitive.SanitizedType;
import com.google.template.soy.types.proto.JavaQualifiedNames;
import com.google.template.soy.types.proto.Protos;
import com.google.template.soy.types.proto.SoyProtoType;
import java.util.List;
import javax.annotation.Nullable;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
/**
* Utilities for dealing with protocol buffers.
*
* <p>TODO(user): Consider moving this back into ExpressionCompiler.
*/
final class ProtoUtils {
private static final Type BYTE_STRING_TYPE = Type.getType(ByteString.class);
private static final Type EXTENDABLE_BUILDER_TYPE = Type.getType(ExtendableBuilder.class);
private static final Type EXTENSION_TYPE = Type.getType(GeneratedExtension.class);
private static final Type[] NO_METHOD_ARGS = {};
private static final Type[] ONE_INT_ARG = {Type.INT_TYPE};
private static final MethodRef BASE_ENCODING_BASE_64 =
MethodRef.create(BaseEncoding.class, "base64").asNonNullable().asCheap();
private static final MethodRef BASE_ENCODING_DECODE =
MethodRef.create(BaseEncoding.class, "decode", CharSequence.class).asNonNullable().asCheap();
private static final MethodRef BASE_ENCODING_ENCODE =
MethodRef.create(BaseEncoding.class, "encode", byte[].class).asNonNullable().asCheap();
private static final MethodRef BYTE_STRING_COPY_FROM =
MethodRef.create(ByteString.class, "copyFrom", byte[].class).asNonNullable();
private static final MethodRef BYTE_STRING_TO_BYTE_ARRAY =
MethodRef.create(ByteString.class, "toByteArray").asNonNullable();
private static final MethodRef EXTENDABLE_BUILDER_ADD_EXTENSION =
MethodRef.create(ExtendableBuilder.class, "addExtension", ExtensionLite.class, Object.class)
.asNonNullable();
private static final MethodRef EXTENDABLE_BUILDER_SET_EXTENSION =
MethodRef.create(ExtendableBuilder.class, "setExtension", ExtensionLite.class, Object.class)
.asNonNullable();
private static final MethodRef EXTENDABLE_MESSAGE_GET_EXTENSION =
MethodRef.create(ExtendableMessage.class, "getExtension", ExtensionLite.class)
.asNonNullable()
.asCheap();
private static final MethodRef EXTENDABLE_MESSAGE_HAS_EXTENSION =
MethodRef.create(ExtendableMessage.class, "hasExtension", ExtensionLite.class)
.asNonNullable()
.asCheap();
private static final MethodRef PROTO_ENUM_GET_NUMBER =
MethodRef.create(ProtocolMessageEnum.class, "getNumber").asCheap();
// We use the full name as the key instead of the descriptor, since descriptors use identity
// semantics for equality and we may load the descriptors for these protos from multiple sources
// depending on our configuration.
private static final ImmutableMap<String, MethodRef> SAFE_PROTO_TO_ACCESSOR =
ImmutableMap.<String, MethodRef>builder()
.put(SafeHtmlProto.getDescriptor().getFullName(), createSafeAccessor(SafeHtmlProto.class))
.put(
SafeScriptProto.getDescriptor().getFullName(),
createSafeAccessor(SafeScriptProto.class))
.put(
SafeStyleProto.getDescriptor().getFullName(),
createSafeAccessor(SafeStyleProto.class))
.put(
SafeStyleSheetProto.getDescriptor().getFullName(),
createSafeAccessor(SafeStyleSheetProto.class))
.put(SafeUrlProto.getDescriptor().getFullName(), createSafeAccessor(SafeUrlProto.class))
.put(
TrustedResourceUrlProto.getDescriptor().getFullName(),
createSafeAccessor(TrustedResourceUrlProto.class))
.build();
private static MethodRef createSafeAccessor(Class<?> clazz) {
// All the safe web types have the same format for their access method names:
// getPrivateDoNotAccessOrElse + name + WrappedValue where name is the prefix of the message
// type.
String simpleName = clazz.getSimpleName();
simpleName = simpleName.substring(0, simpleName.length() - "Proto".length());
return MethodRef.create(clazz, "getPrivateDoNotAccessOrElse" + simpleName + "WrappedValue")
.asNonNullable()
.asCheap();
}
private static final ImmutableMap<String, MethodRef> SANITIZED_CONTENT_TO_PROTO =
ImmutableMap.<String, MethodRef>builder()
.put(
SafeHtmlProto.getDescriptor().getFullName(),
MethodRef.create(SanitizedContent.class, "toSafeHtmlProto"))
.put(
SafeScriptProto.getDescriptor().getFullName(),
MethodRef.create(SanitizedContent.class, "toSafeScriptProto"))
.put(
SafeStyleProto.getDescriptor().getFullName(),
MethodRef.create(SanitizedContent.class, "toSafeStyleProto"))
.put(
SafeStyleSheetProto.getDescriptor().getFullName(),
MethodRef.create(SanitizedContent.class, "toSafeStyleSheetProto"))
.put(
SafeUrlProto.getDescriptor().getFullName(),
MethodRef.create(SanitizedContent.class, "toSafeUrlProto"))
.put(
TrustedResourceUrlProto.getDescriptor().getFullName(),
MethodRef.create(SanitizedContent.class, "toTrustedResourceUrlProto"))
.build();
/**
* Returns a {@link SoyExpression} for accessing a field of a proto.
*
* @param protoType The type of the proto being accessed
* @param baseExpr The proto being accessed
* @param node The field access operation
* @param renderContext The render context
*/
static SoyExpression accessField(
SoyProtoType protoType,
SoyExpression baseExpr,
FieldAccessNode node,
Expression renderContext) {
return new AccessorGenerator(protoType, baseExpr, node, renderContext).generate();
}
/**
* A simple class to encapsulate all the parameters shared between our different accessor
* generation strategies
*/
private static final class AccessorGenerator {
final SoyRuntimeType unboxedRuntimeType;
final SoyExpression baseExpr;
final FieldAccessNode node;
final Expression renderContext;
final FieldDescriptor descriptor;
final boolean isProto3;
AccessorGenerator(
SoyProtoType protoType,
SoyExpression baseExpr,
FieldAccessNode node,
Expression renderContext) {
this.unboxedRuntimeType = SoyRuntimeType.getUnboxedType(protoType).get();
this.baseExpr = baseExpr;
this.node = node;
this.renderContext = renderContext;
this.descriptor = protoType.getFieldDescriptor(node.getFieldName());
this.isProto3 = descriptor.getFile().getSyntax() == Syntax.PROTO3;
}
SoyExpression generate() {
if (descriptor.isRepeated()) {
return handleRepeated();
}
SoyExpression typedBaseExpr;
if (baseExpr.isBoxed()) {
typedBaseExpr =
SoyExpression.forProto(
unboxedRuntimeType,
baseExpr
.invoke(MethodRef.SOY_PROTO_VALUE_GET_PROTO)
// this cast is required because getProto() is generic, so it basically returns
// 'Message'
.checkedCast(unboxedRuntimeType.runtimeType()),
renderContext);
} else if (baseExpr.soyRuntimeType().equals(unboxedRuntimeType)) {
typedBaseExpr = baseExpr;
} else {
throw new AssertionError("should be impossible");
}
if (descriptor.isExtension()) {
return handleExtension(typedBaseExpr);
} else {
return handleNormalField(typedBaseExpr);
}
}
private SoyExpression handleNormalField(final SoyExpression typedBaseExpr) {
// TODO(lukes): consider adding a cache for the method lookups.
// To implement jspb semantics for proto nullability we need to call has<Field>() methods for
// a subset of fields as specified in SoyProtoType. Though, we should probably actually be
// testing against jspb semantics. The best way forward is probably to first invest in
// support for protos in our integration tests.
final MethodRef getMethodRef = getGetterMethod(descriptor);
if (!shouldCheckForFieldPresence()) {
// Simple case, just call .get and interpret the result
return interpretField(typedBaseExpr.invoke(getMethodRef));
} else {
final Label hasFieldLabel = new Label();
final BytecodeProducer hasCheck;
// if oneof, check the value of getFooCase() enum
OneofDescriptor containingOneof = descriptor.getContainingOneof();
if (containingOneof != null) {
final MethodRef getCaseRef = getOneOfCaseMethod(containingOneof);
final Expression fieldNumber = constant(descriptor.getNumber());
// this basically just calls getFooCase().getNumber() == field_number
hasCheck =
new BytecodeProducer() {
@Override
void doGen(CodeBuilder adapter) {
getCaseRef.invokeUnchecked(adapter);
adapter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
getCaseRef.returnType().getInternalName(),
"getNumber",
"()I",
false /* not an interface */);
fieldNumber.gen(adapter);
adapter.ifCmp(Type.INT_TYPE, GeneratorAdapter.EQ, hasFieldLabel);
}
};
} else {
// otherwise just call the has* method
final MethodRef hasMethodRef = getHasserMethod(descriptor);
hasCheck =
new BytecodeProducer() {
@Override
void doGen(CodeBuilder adapter) {
hasMethodRef.invokeUnchecked(adapter);
adapter.ifZCmp(Opcodes.IFNE, hasFieldLabel);
}
};
}
// TODO(lukes): this violates the expression contract since we jump to a label outside the
// scope of the expression
final Label endLabel = new Label();
// If the field doesn't have an explicit default then we need to call .has<Field> and return
// null if it isn't present.
SoyExpression interpretedField =
interpretField(
new Expression(
getMethodRef.returnType(),
getMethodRef.features().minus(Feature.NON_NULLABLE)) {
@Override
void doGen(CodeBuilder adapter) {
typedBaseExpr.gen(adapter);
// Call .has<Field>().
adapter.dup();
hasCheck.gen(adapter);
// The field is missing, substitute null.
adapter.pop();
adapter.visitInsn(Opcodes.ACONST_NULL);
adapter.goTo(endLabel);
// The field exists, call .get<Field>().
adapter.mark(hasFieldLabel);
getMethodRef.invokeUnchecked(adapter);
}
});
if (isPrimitive(interpretedField.resultType())) {
interpretedField = interpretedField.box();
}
// TODO(b/22389927): This is another place where the soy type system lies to us, so make
// sure to mark the type as nullable.
return interpretedField.labelEnd(endLabel).asNullable();
}
}
/**
* TODO(lukes): when jspb nullability semantics get fixed, we should be able to simplify this as
* well.
*/
private boolean shouldCheckForFieldPresence() {
if (descriptor.hasDefaultValue()) {
return false; // No need to check for presence if the field has a explicit default value.
} else if (!isProto3) {
return true; // Always check for presence in proto2.
} else {
// For proto3, only check for field presence for message subtypes
return descriptor.getType() == FieldDescriptor.Type.MESSAGE;
}
}
private SoyExpression interpretField(Expression field) {
// Depending on types we may need to do a trivial conversion
// (e.g. int->long, float->double, enum->int)
switch (descriptor.getJavaType()) {
case FLOAT:
return SoyExpression.forFloat(numericConversion(field, Type.DOUBLE_TYPE));
case DOUBLE:
return SoyExpression.forFloat(field);
case ENUM:
if (isProto3EnumField(descriptor)) {
// it already is an integer, cast to long
return SoyExpression.forInt(numericConversion(field, Type.LONG_TYPE));
}
// otherwise it is proto2 and we need to extract the number.
return SoyExpression.forInt(
numericConversion(field.invoke(PROTO_ENUM_GET_NUMBER), Type.LONG_TYPE));
case INT:
return SoyExpression.forInt(numericConversion(field, Type.LONG_TYPE));
case LONG:
if (shouldConvertBetweenStringAndLong(descriptor)) {
return SoyExpression.forString(MethodRef.LONG_TO_STRING.invoke(field));
}
return SoyExpression.forInt(field);
case BOOLEAN:
return SoyExpression.forBool(field);
case STRING:
return SoyExpression.forString(field);
case MESSAGE:
return messageToSoyExpression(field);
case BYTE_STRING:
return byteStringToBase64String(field);
default:
throw new AssertionError("unsupported field type: " + descriptor);
}
}
private SoyExpression handleExtension(final SoyExpression typedBaseExpr) {
// extensions are a little weird since we need to look up the extension object and then call
// message.getExtension(Extension) and then cast and (maybe) unbox the result.
// The reason we need to cast is because .getExtension is a generic api and that is just how
// stupid java generics work.
FieldRef extensionField = getExtensionField(descriptor);
final Expression extensionFieldAccessor = extensionField.accessor();
if (!descriptor.hasDefaultValue()) {
final Label endLabel = new Label();
SoyExpression interpretedField =
interpretExtensionField(
new Expression(
EXTENDABLE_MESSAGE_GET_EXTENSION.returnType(),
EXTENDABLE_MESSAGE_GET_EXTENSION.features().minus(Feature.NON_NULLABLE)) {
@Override
void doGen(CodeBuilder adapter) {
typedBaseExpr.gen(adapter);
// call hasExtension()
adapter.dup();
extensionFieldAccessor.gen(adapter);
EXTENDABLE_MESSAGE_HAS_EXTENSION.invokeUnchecked(adapter);
Label hasFieldLabel = new Label();
adapter.ifZCmp(Opcodes.IFNE, hasFieldLabel);
// The field is missing, substitute null.
adapter.pop();
adapter.visitInsn(Opcodes.ACONST_NULL);
adapter.goTo(endLabel);
// The field exists, call getExtension()
adapter.mark(hasFieldLabel);
extensionFieldAccessor.gen(adapter);
EXTENDABLE_MESSAGE_GET_EXTENSION.invokeUnchecked(adapter);
}
});
// Box primitives to allow it to be compatible with null.
if (isPrimitive(interpretedField.resultType())) {
interpretedField = interpretedField.box();
}
// TODO(b/22389927): This is another place where the soy type system lies to us, so make
// sure to mark the type as nullable.
// TODO(lukes): this violates the expression contract since we jump to a label outside the
// scope of the expression
return interpretedField.labelEnd(endLabel).asNullable();
} else {
// An extension with a default value is pretty rare, but we still need to support it.
return interpretExtensionField(
typedBaseExpr.invoke(EXTENDABLE_MESSAGE_GET_EXTENSION, extensionFieldAccessor));
}
}
private SoyExpression interpretExtensionField(Expression field) {
switch (descriptor.getJavaType()) {
case FLOAT:
case DOUBLE:
return SoyExpression.forFloat(
field.checkedCast(Number.class).invoke(MethodRef.NUMBER_DOUBLE_VALUE));
case ENUM:
return SoyExpression.forInt(
numericConversion(
field.checkedCast(ProtocolMessageEnum.class).invoke(PROTO_ENUM_GET_NUMBER),
Type.LONG_TYPE));
case INT:
case LONG:
if (shouldConvertBetweenStringAndLong(descriptor)) {
return SoyExpression.forString(field.invoke(MethodRef.OBJECT_TO_STRING));
}
return SoyExpression.forInt(
field.checkedCast(Number.class).invoke(MethodRef.NUMBER_LONG_VALUE));
case BOOLEAN:
return SoyExpression.forBool(
field.checkedCast(Boolean.class).invoke(MethodRef.BOOLEAN_VALUE));
case STRING:
return SoyExpression.forString(field.checkedCast(String.class));
case MESSAGE:
return messageToSoyExpression(field);
case BYTE_STRING:
// Current tofu support for ByteString is to base64 encode it.
return byteStringToBase64String(field.checkedCast(ByteString.class));
default:
throw new AssertionError("unsupported field type: " + descriptor);
}
}
private SoyExpression byteStringToBase64String(Expression byteString) {
byteString.checkAssignableTo(BYTE_STRING_TYPE);
final Expression byteArray = byteString.invoke(BYTE_STRING_TO_BYTE_ARRAY);
return SoyExpression.forString(
new Expression(STRING_TYPE, Feature.NON_NULLABLE) {
@Override
void doGen(CodeBuilder adapter) {
byteArray.gen(adapter);
BASE_ENCODING_BASE_64.invokeUnchecked(adapter);
// swap the two top items of the stack.
// This ensures that the base expression is gen'd at a stack depth of zero, which is
// important if it contains a conditional logic that branches out of the byteArray
// Expression
// TODO(lukes): fix this by changing the null checking logic in
// handle{Extension|Normal}Field.
adapter.swap();
BASE_ENCODING_ENCODE.invokeUnchecked(adapter);
}
});
}
private SoyExpression messageToSoyExpression(Expression field) {
if (node.getType().getKind() == SoyType.Kind.PROTO) {
SoyProtoType fieldProtoType = (SoyProtoType) node.getType();
SoyRuntimeType protoRuntimeType = SoyRuntimeType.getUnboxedType(fieldProtoType).get();
return SoyExpression.forProto(
protoRuntimeType,
field.checkedCast(protoRuntimeType.runtimeType()), // cast needed for extensions
renderContext);
} else {
// All other are special sanitized types
ContentKind kind = ((SanitizedType) node.getType()).getContentKind();
Descriptor messageType = descriptor.getMessageType();
MethodRef methodRef = SAFE_PROTO_TO_ACCESSOR.get(messageType.getFullName());
return SoyExpression.forSanitizedString(
field
.checkedCast(methodRef.owner().type()) // cast needed for extensions
.invoke(methodRef),
kind);
}
}
private SoyExpression handleRepeated() {
// For repeated fields we delegate to the tofu implementation. This is because the proto
// will return a List<Integer> which we will need to turn into a List<IntegerData> and so on.
// we could handle this by
// 1. generating Runtime.java helpers to do this kind of collection boxing conversion
// 2. enhancing SoyExpression to be able to understand a 'partially unboxed collection'
// 3. fallback to tofu (which already supports this)
// 4. Add new SoyList implementations that can do this kind of lazy resolving transparently
// (I think SoyEasyList is supposed to support this)
// For now we will do #3. #2 would be ideal (least overhead) but would be very complex. #1 or
// #4 would both be reasonable compromises.
SoyRuntimeType boxedType = SoyRuntimeType.getBoxedType(node.getType());
return SoyExpression.forSoyValue(
node.getType(),
MethodRef.SOY_PROTO_VALUE_GET_FIELD
.invoke(baseExpr.box(), constant(node.getFieldName()))
// We can immediately resolve because we know proto fields don't need detach logic.
// they are always immediately available.
.invoke(MethodRef.SOY_VALUE_PROVIDER_RESOLVE)
.checkedCast(boxedType.runtimeType()));
}
}
/**
* Returns a {@link SoyExpression} for initializing a new proto.
*
* @param node The proto initialization node
* @param args Args for the proto initialization call
* @param renderContext The render context
* @param varManager Local variables manager
*/
static SoyExpression createProto(
ProtoInitNode node,
List<SoyExpression> args,
Expression renderContext,
Supplier<? extends ExpressionDetacher> detacher,
TemplateVariableManager varManager) {
return new ProtoInitGenerator(node, args, renderContext, detacher, varManager).generate();
}
private static final class ProtoInitGenerator {
private final ProtoInitNode node;
private final List<SoyExpression> args;
private final Expression renderContext;
private final Supplier<? extends ExpressionDetacher> detacher;
private final TemplateVariableManager varManager;
private final SoyProtoType protoType;
private final Descriptor descriptor;
ProtoInitGenerator(
ProtoInitNode node,
List<SoyExpression> args,
Expression renderContext,
Supplier<? extends ExpressionDetacher> detacher,
TemplateVariableManager varManager) {
this.node = node;
this.args = args;
this.renderContext = renderContext;
this.detacher = detacher;
this.varManager = varManager;
this.protoType = (SoyProtoType) node.getType();
this.descriptor = protoType.getDescriptor();
}
SoyExpression generate() {
// For cases with no field assignments, return proto.defaultInstance().
if (args.isEmpty()) {
final Expression defaultInstance = getDefaultInstanceMethod(descriptor).invoke();
return SoyExpression.forProto(
SoyRuntimeType.getUnboxedType(protoType).get(), defaultInstance, renderContext);
}
final Expression newBuilderCall = getBuilderMethod(descriptor).invoke();
final ImmutableList<Statement> setters = getFieldSetters();
final MethodRef buildCall = getBuildMethod(descriptor);
Expression expression =
new Expression(messageRuntimeType(descriptor).type()) {
@Override
void doGen(CodeBuilder cb) {
newBuilderCall.gen(cb);
for (Statement setter : setters) {
setter.gen(cb);
}
// builder is already on the stack from newBuilder() / set<Field>()
buildCall.invokeUnchecked(cb);
}
}.asNonNullable();
return SoyExpression.forProto(
SoyRuntimeType.getUnboxedType(protoType).get(), expression, renderContext);
}
private ImmutableList<Statement> getFieldSetters() {
ImmutableList.Builder<Statement> setters = ImmutableList.builder();
for (int i = 0; i < args.size(); i++) {
FieldDescriptor field = protoType.getFieldDescriptor(node.getParamName(i));
SoyExpression baseArg = args.get(i).withRenderContext(renderContext);
Statement setter;
if (field.isRepeated()) {
setter = handleRepeated(baseArg, field);
} else {
if (field.isExtension()) {
setter = handleExtension(baseArg, field);
} else {
setter = handleNormalSetter(baseArg, field);
}
}
setters.add(setter);
}
return setters.build();
}
/**
* Returns a Statement that handles a single proto builder setFoo() call.
*
* <p>The Statement assumes that just before .gen(), there is an instance of the proto builder
* at the top of the stack. After .gen() it is guaranteed to leave an instance of the builder at
* the top of the stack, without changing stack heights.
*/
private Statement handleNormalSetter(final SoyExpression baseArg, final FieldDescriptor field) {
final MethodRef setterMethod = getSetOrAddMethod(field);
final boolean isNullable = !baseArg.isNonNullable();
return new Statement() {
@Override
void doGen(CodeBuilder cb) {
baseArg.gen(cb);
Label argIsNull = null;
Label end = null;
if (isNullable) {
argIsNull = new Label();
end = new Label();
// perform null check
cb.dup();
cb.ifNull(argIsNull);
}
// arg is not null; unbox, coerce, set<Field>().
unboxAndCoerce(cb, baseArg, field);
setterMethod.invokeUnchecked(cb);
if (isNullable) {
cb.goTo(end);
// arg is null; pop it off stack.
cb.mark(argIsNull);
cb.pop();
cb.mark(end);
}
}
};
}
private Statement handleRepeated(final SoyExpression listArg, FieldDescriptor field) {
// If the list arg is definitely an empty list, do nothing
if (listArg.soyType().equals(ListType.EMPTY_LIST)) {
return Statement.NULL_STATEMENT;
}
if (listArg.isNonNullable()) {
return handleRepeatedNotNull(listArg, field);
}
final Label listIsNonNull = new Label();
final Label end = new Label();
// perform null check
SoyExpression nonNull =
listArg
.withSource(
new Expression(listArg.resultType(), listArg.features()) {
@Override
void doGen(CodeBuilder cb) {
listArg.gen(cb);
cb.dup();
cb.ifNonNull(listIsNonNull);
cb.pop(); // pop null off list, skip to end
// TODO(user): This violates Expression contract, as it jumps out of itself
cb.goTo(end);
cb.mark(listIsNonNull);
}
})
.asNonNullable();
final Statement handle = handleRepeatedNotNull(nonNull, field);
return new Statement() {
@Override
void doGen(CodeBuilder cb) {
handle.gen(cb);
cb.mark(end); // jump here if listArg is null
}
};
}
private Statement handleRepeatedNotNull(final SoyExpression listArg, FieldDescriptor field) {
Preconditions.checkArgument(listArg.isNonNullable());
// Unbox listArg as List<SoyValueProvider> and wait until all items are done
SoyExpression unboxed = listArg.unboxAs(List.class);
Expression resolved = detacher.get().resolveSoyValueProviderList(unboxed);
// Enter new scope
Scope scope = varManager.enterScope();
final Statement scopeExit = scope.exitScope();
// Create local variables: list, loop index, list size
final Variable list = scope.createTemporary(field.getName() + "__list", resolved);
final Variable index = scope.createTemporary(field.getName() + "__index", constant(0));
final Variable listSize =
scope.createTemporary(
field.getName() + "__size", MethodRef.LIST_SIZE.invoke(list.local()));
// Expected type info of the list element
SoyType elementSoyType = ((ListType) unboxed.soyType()).getElementType();
SoyRuntimeType elementType = SoyRuntimeType.getBoxedType(elementSoyType);
// Call list.get(i).resolveSoyValueProvider(), then cast to the expected subtype of SoyValue
Expression getAndResolve =
list.local() // list
.invoke(MethodRef.LIST_GET, index.local()) // .get(i)
.checkedCast(SOY_VALUE_PROVIDER_TYPE) // cast Object to SoyValueProvider
.invoke(MethodRef.SOY_VALUE_PROVIDER_RESOLVE) // .resolve()
.checkedCast(elementType.runtimeType()); // cast SoyValue to appropriate subtype
SoyExpression soyValue =
SoyExpression.forSoyValue(elementType.soyType(), getAndResolve)
.withRenderContext(renderContext)
// Set soyValue as a non-nullable, even though it is possible for templates to receive
// lists with null elements. Lists with null elements will result in a
// NullPointerException thrown in .handleNormalSetter() / .handleExtension().
//
// Note: This is different from jspb implementation. Jspb will happily accept nulls as
// part of a repeated field, however said proto will error out at server-side
// deserialization time. Hence, here we throw an NPE rather than copying jspb.
.asNonNullable();
// Call into .handleNormalSetter() or .handleExtension(), which will call add<Field>()
final Statement getAndAddOne =
field.isExtension()
? handleExtension(soyValue, field)
: handleNormalSetter(soyValue, field);
// Put the entire for-loop together
return new Statement() {
@Override
void doGen(CodeBuilder cb) {
list.initializer().gen(cb);
listSize.initializer().gen(cb);
// if list.size() == 0, skip loop
listSize.local().gen(cb);
Label listIsEmpty = new Label();
cb.ifZCmp(Opcodes.IFEQ, listIsEmpty);
// i = 0
index.initializer().gen(cb);
// Begin loop
Label loopStart = cb.mark();
// loop body
getAndAddOne.gen(cb);
// i++
cb.iinc(index.local().index(), 1);
// if i < list.size(), goto loopStart
index.local().gen(cb);
listSize.local().gen(cb);
cb.ifICmp(Opcodes.IFLT, loopStart);
// End loop
cb.mark(listIsEmpty);
scopeExit.gen(cb);
}
};
}
private Statement handleExtension(final SoyExpression baseArg, final FieldDescriptor field) {
// .setExtension() requires an extension identifier object
final Expression extensionIdentifier = getExtensionField(field).accessor();
// Call .setExtension() for regular extensions, .addExtension() for repeated extensions
final MethodRef setterMethod =
field.isRepeated() ? EXTENDABLE_BUILDER_ADD_EXTENSION : EXTENDABLE_BUILDER_SET_EXTENSION;
final boolean isNullable = !baseArg.isNonNullable();
return new Statement() {
@Override
void doGen(CodeBuilder cb) {
cb.checkCast(EXTENDABLE_BUILDER_TYPE);
// Put baseArg on stack
baseArg.gen(cb);
Label argIsNull = null;
Label end = null;
if (isNullable) {
argIsNull = new Label();
end = new Label();
// Null check
cb.dup();
cb.ifNull(argIsNull);
}
// Arg is not null; unbox, coerce, run .valueOf(), add extension id, call .setExtension()
unboxAndCoerce(cb, baseArg, field);
// Put extension identifier on stack, swap to the right order
extensionIdentifier.gen(cb);
cb.swap();
// Call .setExtension() / .addExtension(), skip to end
setterMethod.invokeUnchecked(cb);
// Arg is null; pop it off stack
if (isNullable) {
cb.goTo(end);
cb.mark(argIsNull);
cb.pop();
// Done; cast back to MyProto.Builder
cb.mark(end);
}
cb.checkCast(builderRuntimeType(descriptor).type());
}
};
}
/**
* Assuming that the value of {@code baseArg} is on the top of the stack. unbox and coerce it to
* be compatible with the given field descriptor.
*/
private static void unboxAndCoerce(
CodeBuilder cb, SoyExpression baseArg, FieldDescriptor field) {
Type currentType;
if (!isSafeProto(field)) {
if (baseArg.isBoxed()) {
currentType = unboxUnchecked(cb, baseArg.soyRuntimeType(), classToUnboxTo(field));
} else {
currentType = baseArg.resultType();
}
} else {
// currently we unbox everything but safe proto fields
currentType = SANITIZED_CONTENT_TYPE;
}
coerce(cb, currentType, field);
}
@Nullable
private static Class<?> classToUnboxTo(FieldDescriptor field) {
switch (field.getJavaType()) {
case BOOLEAN:
return boolean.class;
case FLOAT:
case DOUBLE:
return double.class;
case INT:
case ENUM:
return long.class;
case LONG:
return shouldConvertBetweenStringAndLong(field) ? String.class : long.class;
case STRING:
case BYTE_STRING:
return String.class;
case MESSAGE:
if (isSafeProto(field)) {
throw new IllegalStateException("SanitizedContent objects shouldn't be unboxed");
}
return Message.class;
default:
throw new AssertionError("unsupported field type: " + field);
}
}
/**
* Generate bytecode that coerces the top of stack to the correct type for the given field
* setter.
*/
private static void coerce(CodeBuilder cb, Type currentType, FieldDescriptor field) {
// TODO(user): This might be a good place to do some extra type-checking, by
// running comparisons between currentType to getRuntimeType(field).
switch (field.getJavaType()) {
case BOOLEAN:
case DOUBLE:
case STRING:
break; // no coercion necessary
case FLOAT:
if (!currentType.equals(Type.FLOAT_TYPE)) {
cb.cast(currentType, Type.FLOAT_TYPE);
}
break;
case INT:
if (!currentType.equals(Type.INT_TYPE)) {
cb.cast(currentType, Type.INT_TYPE);
}
break;
case LONG:
if (shouldConvertBetweenStringAndLong(field)) {
MethodRef.LONG_PARSE_LONG.invokeUnchecked(cb);
}
break;
case BYTE_STRING:
BASE_ENCODING_BASE_64.invokeUnchecked(cb);
cb.swap();
BASE_ENCODING_DECODE.invokeUnchecked(cb);
BYTE_STRING_COPY_FROM.invokeUnchecked(cb);
break;
case MESSAGE:
coerceToMessage(cb, field);
break;
case ENUM:
if (!currentType.equals(Type.INT_TYPE)) {
cb.cast(currentType, Type.INT_TYPE);
}
// for proto 3 enums we call the setValue function which accepts an int so we don't need
// to grab the actual enum value.
if (!isProto3EnumField(field)) {
getForNumberMethod(field.getEnumType()).invokeUnchecked(cb);
}
return;
default:
throw new AssertionError("unsupported field type: " + field);
}
if (field.isExtension()) {
// primitive extensions need to be boxed since the api is generic
Type fieldType = getRuntimeType(field);
if (isPrimitive(fieldType)) {
cb.valueOf(fieldType);
}
}
}
private static void coerceToMessage(CodeBuilder cb, FieldDescriptor field) {
if (isSafeProto(field)) {
MethodRef toProto = SANITIZED_CONTENT_TO_PROTO.get(field.getMessageType().getFullName());
toProto.invokeUnchecked(cb);
}
cb.checkCast(getRuntimeType(field));
}
// TODO(user): Consider consolidating all the safe proto references to a single place.
private static boolean isSafeProto(FieldDescriptor field) {
return field.getJavaType() == JavaType.MESSAGE
&& SAFE_PROTO_TO_ACCESSOR.containsKey(field.getMessageType().getFullName());
}
}
private static boolean shouldConvertBetweenStringAndLong(FieldDescriptor descriptor) {
if (Protos.hasJsType(descriptor)) {
Protos.JsType jsType = Protos.getJsType(descriptor);
if (jsType == Protos.JsType.STRING) {
return true;
}
}
return false;
}
// TODO(lukes): Consider caching? in SoyRuntimeType?
private static TypeInfo messageRuntimeType(Descriptor descriptor) {
String className = JavaQualifiedNames.getClassName(descriptor);
return TypeInfo.create(className);
}
private static TypeInfo enumRuntimeType(EnumDescriptor descriptor) {
String className = JavaQualifiedNames.getClassName(descriptor);
return TypeInfo.create(className);
}
private static TypeInfo builderRuntimeType(Descriptor descriptor) {
String className = JavaQualifiedNames.getClassName(descriptor);
return TypeInfo.create(className + "$Builder");
}
private static Type getRuntimeType(FieldDescriptor field) {
switch (field.getJavaType()) {
case BOOLEAN:
return Type.BOOLEAN_TYPE;
case BYTE_STRING:
return BYTE_STRING_TYPE;
case DOUBLE:
return Type.DOUBLE_TYPE;
case ENUM:
return isProto3EnumField(field)
? Type.INT_TYPE
: TypeInfo.create(JavaQualifiedNames.getClassName(field.getEnumType())).type();
case FLOAT:
return Type.FLOAT_TYPE;
case INT:
return Type.INT_TYPE;
case LONG:
return Type.LONG_TYPE;
case MESSAGE:
return TypeInfo.create(JavaQualifiedNames.getClassName(field.getMessageType())).type();
case STRING:
return STRING_TYPE;
default:
throw new AssertionError("unexpected type");
}
}
/** Returns the {@link MethodRef} for the generated getter method. */
private static MethodRef getGetterMethod(FieldDescriptor descriptor) {
Preconditions.checkArgument(
!descriptor.isExtension(), "extensions do not have getter methods. %s", descriptor);
TypeInfo message = messageRuntimeType(descriptor.getContainingType());
boolean isProto3Enum = isProto3EnumField(descriptor);
return MethodRef.createInstanceMethod(
message,
new Method(
"get"
+ getFieldName(descriptor, true)
// For proto3 enums we access the Value field
+ (isProto3Enum ? "Value" : ""),
isProto3Enum ? Type.INT_TYPE : getRuntimeType(descriptor),
NO_METHOD_ARGS))
// All protos are guaranteed to never return null
.asNonNullable()
.asCheap();
}
/**
* Proto3 enums fields can accept and return unknown values via the get<Field>Value() methods, we
* use those methods instead of the methods that deal with the enum constants in order to support
* unknown enum values. If we didn't, any field with an unknown enum value would throw an
* exception when we call {@code getNumber()} on the enum.
*
* <p>For comparison, in proto2 unknown values always get mapped to 0, so this problem doesn't
* exist. Also, in proto2, the 'Value' functions don't exist, so we can't use them.
*/
private static boolean isProto3EnumField(FieldDescriptor descriptor) {
return descriptor.getType() == Descriptors.FieldDescriptor.Type.ENUM
&& descriptor.getFile().getSyntax() == Syntax.PROTO3;
}
/** Returns the {@link MethodRef} for the generated hasser method. */
private static MethodRef getHasserMethod(FieldDescriptor descriptor) {
TypeInfo message = messageRuntimeType(descriptor.getContainingType());
return MethodRef.createInstanceMethod(
message,
new Method("has" + getFieldName(descriptor, true), Type.BOOLEAN_TYPE, NO_METHOD_ARGS))
.asCheap();
}
/** Returns the {@link MethodRef} for the get*Case method for oneof fields. */
private static MethodRef getOneOfCaseMethod(OneofDescriptor descriptor) {
TypeInfo message = messageRuntimeType(descriptor.getContainingType());
return MethodRef.createInstanceMethod(
message,
new Method(
"get" + underscoresToCamelCase(descriptor.getName(), true) + "Case",
TypeInfo.create(JavaQualifiedNames.getCaseEnumClassName(descriptor)).type(),
NO_METHOD_ARGS))
.asCheap();
}
/** Returns the {@link MethodRef} for the generated newBuilder method. */
private static MethodRef getBuilderMethod(Descriptor descriptor) {
TypeInfo message = messageRuntimeType(descriptor);
TypeInfo builder = builderRuntimeType(descriptor);
return MethodRef.createStaticMethod(
message, new Method("newBuilder", builder.type(), NO_METHOD_ARGS))
.asNonNullable();
}
/** Returns the {@link MethodRef} for the generated defaultInstance method. */
private static MethodRef getDefaultInstanceMethod(Descriptor descriptor) {
TypeInfo message = messageRuntimeType(descriptor);
return MethodRef.createStaticMethod(
message, new Method("getDefaultInstance", message.type(), NO_METHOD_ARGS))
.asNonNullable();
}
/** Returns the {@link MethodRef} for the generated setter/adder method. */
private static MethodRef getSetOrAddMethod(FieldDescriptor descriptor) {
TypeInfo builder = builderRuntimeType(descriptor.getContainingType());
String prefix = descriptor.isRepeated() ? "add" : "set";
boolean isProto3EnumField = isProto3EnumField(descriptor);
String suffix = isProto3EnumField ? "Value" : "";
return MethodRef.createInstanceMethod(
builder,
new Method(
prefix + getFieldName(descriptor, true) + suffix,
builder.type(),
new Type[] {isProto3EnumField ? Type.INT_TYPE : getRuntimeType(descriptor)}))
.asNonNullable();
}
/** Returns the {@link MethodRef} for the generated build method. */
private static MethodRef getBuildMethod(Descriptor descriptor) {
TypeInfo message = messageRuntimeType(descriptor);
TypeInfo builder = builderRuntimeType(descriptor);
return MethodRef.createInstanceMethod(
builder, new Method("build", message.type(), NO_METHOD_ARGS))
.asNonNullable();
}
/** Returns the {@link MethodRef} for the generated forNumber method. */
private static MethodRef getForNumberMethod(EnumDescriptor descriptor) {
TypeInfo enumType = enumRuntimeType(descriptor);
return MethodRef.createStaticMethod(
enumType, new Method("forNumber", enumType.type(), ONE_INT_ARG))
// Note: Enum.forNumber() returns null if there is no corresponding enum. If a bad value is
// passed in (via unknown types), the generated bytecode will NPE.
.asNonNullable()
.asCheap();
}
/** Returns the {@link FieldRef} for the generated {@link Extension} field. */
private static FieldRef getExtensionField(FieldDescriptor descriptor) {
Preconditions.checkArgument(descriptor.isExtension(), "%s is not an extension", descriptor);
String extensionFieldName = getFieldName(descriptor, false);
if (descriptor.getExtensionScope() != null) {
TypeInfo owner = messageRuntimeType(descriptor.getExtensionScope());
return FieldRef.createPublicStaticField(owner, extensionFieldName, EXTENSION_TYPE);
}
// else we have a 'top level extension'
String containingClass =
JavaQualifiedNames.getPackage(descriptor.getFile())
+ "."
+ JavaQualifiedNames.getOuterClassname(descriptor.getFile());
return FieldRef.createPublicStaticField(
TypeInfo.create(containingClass), extensionFieldName, EXTENSION_TYPE);
}
}