Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix bug NPE at ApolloServerInterceptor #146

Merged
merged 8 commits into from
Apr 12, 2019
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
alanvan0502 marked this conversation as resolved.
Show resolved Hide resolved
* Copyright 2018-2019 Amazon.com,
* Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Amazon Software License (the "License").
* You may not use this file except in compliance with the
* License. A copy of the License is located at
*
* http://aws.amazon.com/asl/
*
* or in the "license" file accompanying this file. This file is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, express or implied. See the License
* for the specific language governing permissions and
* limitations under the License.
*/

package com.apollographql.apollo.api;

import javax.annotation.Nonnull;

public interface InputType {
@Nonnull InputFieldMarshaller marshaller();
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ public class ResponseField {
private final boolean optional;
private final List<Condition> conditions;

private static final String VARIABLE_IDENTIFIER_KEY = "kind";
private static final String VARIABLE_IDENTIFIER_VALUE = "Variable";
private static final String VARIABLE_NAME_KEY = "variableName";
public static final String VARIABLE_IDENTIFIER_KEY = "kind";
public static final String VARIABLE_IDENTIFIER_VALUE = "Variable";
public static final String VARIABLE_NAME_KEY = "variableName";

/**
* Factory method for creating a Field instance representing {@link Type#STRING}.
Expand Down Expand Up @@ -250,13 +250,6 @@ public List<Condition> conditions() {
return conditions;
}

public String cacheKey(Operation.Variables variables) {
if (arguments.isEmpty()) {
return fieldName();
}
return String.format("%s(%s)", fieldName(), orderIndependentKey(arguments, variables));
}

/**
* Resolve field argument value by name. If argument represents a references to the variable, it will be resolved from
* provided operation variables values.
Expand All @@ -283,61 +276,12 @@ public String cacheKey(Operation.Variables variables) {
return argumentValue;
}

private String orderIndependentKey(Map<String, Object> objectMap, Operation.Variables variables) {
if (isArgumentValueVariableType(objectMap)) {
return orderIndependentKeyForVariableArgument(objectMap, variables);
}
List<Map.Entry<String, Object>> sortedArguments = new ArrayList<>(objectMap.entrySet());
Collections.sort(sortedArguments, new Comparator<Map.Entry<String, Object>>() {
@Override public int compare(Map.Entry<String, Object> argumentOne, Map.Entry<String, Object> argumentTwo) {
return argumentOne.getKey().compareTo(argumentTwo.getKey());
}
});
StringBuilder independentKey = new StringBuilder();
for (int i = 0; i < sortedArguments.size(); i++) {
Map.Entry<String, Object> argument = sortedArguments.get(i);
if (argument.getValue() instanceof Map) {
//noinspection unchecked
final Map<String, Object> objectArg = (Map<String, Object>) argument.getValue();
boolean isArgumentVariable = isArgumentValueVariableType(objectArg);
independentKey
.append(argument.getKey())
.append(":")
.append(isArgumentVariable ? "" : "[")
.append(orderIndependentKey(objectArg, variables))
.append(isArgumentVariable ? "" : "]");
} else {
independentKey.append(argument.getKey())
.append(":")
.append(argument.getValue().toString());
}
if (i < sortedArguments.size() - 1) {
independentKey.append(",");
}
}
return independentKey.toString();
}

private boolean isArgumentValueVariableType(Map<String, Object> objectMap) {
public static boolean isArgumentValueVariableType(Map<String, Object> objectMap) {
return objectMap.containsKey(VARIABLE_IDENTIFIER_KEY)
&& objectMap.get(VARIABLE_IDENTIFIER_KEY).equals(VARIABLE_IDENTIFIER_VALUE)
&& objectMap.containsKey(VARIABLE_NAME_KEY);
}

private String orderIndependentKeyForVariableArgument(Map<String, Object> objectMap, Operation.Variables variables) {
Object variable = objectMap.get(VARIABLE_NAME_KEY);
//noinspection SuspiciousMethodCalls
Object resolvedVariable = variables.valueMap().get(variable);
if (resolvedVariable == null) {
return null;
} else if (resolvedVariable instanceof Map) {
//noinspection unchecked
return orderIndependentKey((Map<String, Object>) resolvedVariable, variables);
} else {
return resolvedVariable.toString();
}
}

/**
* An abstraction for the field types
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ object ClassNames {
val JAVA_OPTIONAL: ClassName = ClassName.get("java.util", "Optional")
val API_UTILS: ClassName = ClassName.get(Utils::class.java)
val FRAGMENT: ClassName = ClassName.get(GraphqlFragment::class.java)
val INPUT_TYPE: ClassName = ClassName.get(Input::class.java)
val INPUT: ClassName = ClassName.get(Input::class.java)
val BUILDER: ClassName = ClassName.get("", "Builder")
val MUTATOR: ClassName = ClassName.get(Mutator::class.java)
var S3ObjectInput: ClassName = ClassName.get(S3InputObjectInterface::class.java)
var S3Object: ClassName = ClassName.get(S3ObjectInterface::class.java)
val INPUT_TYPE: ClassName = ClassName.get(InputType::class.java)

fun <K : Any> parameterizedListOf(type: Class<K>): TypeName =
ParameterizedTypeName.get(LIST, ClassName.get(type))
Expand Down Expand Up @@ -84,6 +85,6 @@ object ClassNames {
ParameterizedTypeName.get(JAVA_OPTIONAL, type)

fun parameterizedInputType(type: TypeName): TypeName =
ParameterizedTypeName.get(INPUT_TYPE, type)
ParameterizedTypeName.get(INPUT, type)

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class InputTypeSpecBuilder(
TypeSpec.classBuilder(objectClassName)
.addAnnotation(Annotations.GENERATED_BY_APOLLO)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(ClassNames.INPUT_TYPE)
.addConstructor()
.addFields()
.addBuilder()
Expand Down Expand Up @@ -128,6 +129,7 @@ class InputTypeSpecBuilder(
.addMethod(methodSpec)
.build()
return MethodSpec.methodBuilder(MARSHALLER_PARAM_NAME)
.addAnnotation(Annotations.OVERRIDE)
.addModifiers(Modifier.PUBLIC)
.returns(InputFieldMarshaller::class.java)
.addStatement("return \$L", marshallerType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ fun TypeName.isNullable(): Boolean = isOptional() || annotations.contains(Annota
fun TypeName.isOptional(): Boolean {
val rawType = (this as? ParameterizedTypeName)?.rawType ?: this
return rawType == ClassNames.OPTIONAL || rawType == ClassNames.GUAVA_OPTIONAL || rawType == ClassNames.JAVA_OPTIONAL
|| rawType == ClassNames.INPUT_TYPE
|| rawType == ClassNames.INPUT
}

fun TypeName.unwrapOptionalType(withoutAnnotations: Boolean = false): TypeName {
Expand All @@ -272,7 +272,7 @@ fun TypeName.unwrapOptionalType(withoutAnnotations: Boolean = false): TypeName {
fun TypeName.unwrapOptionalValue(varName: String, checkIfPresent: Boolean = true,
transformation: ((CodeBlock) -> CodeBlock)? = null): CodeBlock {
return if (isOptional() && this is ParameterizedTypeName) {
if (rawType == ClassNames.INPUT_TYPE) {
if (rawType == ClassNames.INPUT) {
val valueCode = CodeBlock.of("\$L.value", varName)
if (checkIfPresent) {
CodeBlock.of("\$L != null ? \$L : null", valueCode, transformation?.invoke(valueCode) ?: valueCode)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright 2018-2019 Amazon.com,
* Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Amazon Software License (the "License").
* You may not use this file except in compliance with the
* License. A copy of the License is located at
*
* http://aws.amazon.com/asl/
*
* or in the "license" file accompanying this file. This file is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, express or implied. See the License
* for the specific language governing permissions and
* limitations under the License.
*/

package com.apollographql.apollo.internal.cache.normalized;

import com.apollographql.apollo.api.Operation;
import com.apollographql.apollo.api.ResponseField;

import javax.annotation.Nonnull;

public interface CacheKeyBuilder {
@Nonnull String build(@Nonnull ResponseField field, @Nonnull Operation.Variables variables);
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public final class RealAppSyncStore implements ApolloStore, ReadableStore, Write
private final ReadWriteLock lock;
private final Set<RecordChangeSubscriber> subscribers;
private final Executor dispatcher;
private final CacheKeyBuilder cacheKeyBuilder;
private final ApolloLogger logger;

public RealAppSyncStore(@Nonnull NormalizedCache normalizedCache, @Nonnull CacheKeyResolver cacheKeyResolver,
Expand All @@ -74,6 +75,7 @@ public RealAppSyncStore(@Nonnull NormalizedCache normalizedCache, @Nonnull Cache
this.logger = checkNotNull(logger, "logger == null");
this.lock = new ReentrantReadWriteLock();
this.subscribers = Collections.newSetFromMap(new WeakHashMap<RecordChangeSubscriber, Boolean>());
this.cacheKeyBuilder = new RealCacheKeyBuilder();
}

@Override public ResponseNormalizer<Map<String, Object>> networkResponseNormalizer() {
Expand All @@ -82,6 +84,10 @@ public RealAppSyncStore(@Nonnull NormalizedCache normalizedCache, @Nonnull Cache
@Nonnull Map<String, Object> record) {
return cacheKeyResolver.fromFieldRecordSet(field, record);
}

@Nonnull @Override public CacheKeyBuilder cacheKeyBuilder() {
return cacheKeyBuilder;
}
};
}

Expand All @@ -90,6 +96,10 @@ public RealAppSyncStore(@Nonnull NormalizedCache normalizedCache, @Nonnull Cache
@Nonnull @Override public CacheKey resolveCacheKey(@Nonnull ResponseField field, @Nonnull Record record) {
return CacheKey.from(record.key());
}

@Nonnull @Override public CacheKeyBuilder cacheKeyBuilder() {
return cacheKeyBuilder;
}
};
}

Expand Down Expand Up @@ -357,7 +367,7 @@ private <D extends Operation.Data, T, V extends Operation.Variables> T doRead(fi

ResponseFieldMapper<D> responseFieldMapper = operation.responseFieldMapper();
CacheFieldValueResolver fieldValueResolver = new CacheFieldValueResolver(cache, operation.variables(),
cacheKeyResolver(), CacheHeaders.NONE);
cacheKeyResolver(), CacheHeaders.NONE, cacheKeyBuilder);
//noinspection unchecked
RealResponseReader<Record> responseReader = new RealResponseReader<>(operation.variables(), rootRecord,
fieldValueResolver, scalarTypeAdapters, ResponseNormalizer.NO_OP_NORMALIZER);
Expand All @@ -377,7 +387,7 @@ private <D extends Operation.Data, T, V extends Operation.Variables> Response<T>
}

CacheFieldValueResolver fieldValueResolver = new CacheFieldValueResolver(cache, operation.variables(),
cacheKeyResolver(), cacheHeaders);
cacheKeyResolver(), cacheHeaders, cacheKeyBuilder);
RealResponseReader<Record> responseReader = new RealResponseReader<>(operation.variables(), rootRecord,
fieldValueResolver, scalarTypeAdapters, responseNormalizer);
try {
Expand Down Expand Up @@ -406,7 +416,7 @@ private <F extends GraphqlFragment> F doRead(final ResponseFieldMapper<F> respon
}

CacheFieldValueResolver fieldValueResolver = new CacheFieldValueResolver(cache, variables,
cacheKeyResolver(), CacheHeaders.NONE);
cacheKeyResolver(), CacheHeaders.NONE, cacheKeyBuilder);
//noinspection unchecked
RealResponseReader<Record> responseReader = new RealResponseReader<>(variables, rootRecord,
fieldValueResolver, scalarTypeAdapters, ResponseNormalizer.NO_OP_NORMALIZER);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Copyright 2018-2019 Amazon.com,
* Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Amazon Software License (the "License").
* You may not use this file except in compliance with the
* License. A copy of the License is located at
*
* http://aws.amazon.com/asl/
*
* or in the "license" file accompanying this file. This file is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, express or implied. See the License
* for the specific language governing permissions and
* limitations under the License.
*/

package com.apollographql.apollo.internal.cache.normalized;

import com.apollographql.apollo.api.InputType;
import com.apollographql.apollo.api.Operation;
import com.apollographql.apollo.api.ResponseField;
import com.apollographql.apollo.internal.json.JsonWriter;
import com.apollographql.apollo.internal.json.SortedInputFieldMapWriter;
import com.apollographql.apollo.internal.json.Utils;

import javax.annotation.Nonnull;

import java.io.IOException;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

import okio.Buffer;

import static com.apollographql.apollo.api.internal.Utils.checkNotNull;

public class RealCacheKeyBuilder implements CacheKeyBuilder {
private final Comparator<String> argumentNameComparator = new Comparator<String>() {
@Override public int compare(String first, String second) {
return first.compareTo(second);
}
};

@Nonnull @Override
public String build(@Nonnull ResponseField field, @Nonnull Operation.Variables variables) {
checkNotNull(field, "field == null");
checkNotNull(variables, "variables == null");

if (field.arguments().isEmpty()) {
return field.fieldName();
}

Object resolvedArguments = resolveArguments(field.arguments(), variables);
try {
Buffer buffer = new Buffer();
JsonWriter jsonWriter = JsonWriter.of(buffer);
jsonWriter.setSerializeNulls(true);
Utils.writeToJson(resolvedArguments, jsonWriter);
jsonWriter.close();
return String.format("%s(%s)", field.fieldName(), buffer.readUtf8());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private Map<String, Object> resolveArguments(Map<String, Object> objectMap, Operation.Variables variables) {
Map<String, Object> result = new TreeMap<>(argumentNameComparator);
for (Map.Entry<String, Object> entry : objectMap.entrySet()) {
if (entry.getValue() instanceof Map) {
Map<String, Object> nestedObjectMap = (Map<String, Object>) entry.getValue();
if (ResponseField.isArgumentValueVariableType(nestedObjectMap)) {
result.put(entry.getKey(), resolveVariableArgument(nestedObjectMap, variables));
} else {
result.put(entry.getKey(), resolveArguments(nestedObjectMap, variables));
}
} else {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}

private Object resolveVariableArgument(Map<String, Object> objectMap, Operation.Variables variables) {
Object variable = objectMap.get(ResponseField.VARIABLE_NAME_KEY);
//noinspection SuspiciousMethodCalls
Object resolvedVariable = variables.valueMap().get(variable);
if (resolvedVariable == null) {
return null;
} else if (resolvedVariable instanceof Map) {
//noinspection unchecked
return resolveArguments((Map<String, Object>) resolvedVariable, variables);
} else if (resolvedVariable instanceof InputType) {
try {
SortedInputFieldMapWriter inputFieldMapWriter = new SortedInputFieldMapWriter(argumentNameComparator);
((InputType) resolvedVariable).marshaller().marshal(inputFieldMapWriter);
return resolveArguments(inputFieldMapWriter.map(), variables);
} catch (IOException e) {
// should never happen
throw new RuntimeException(e);
}
} else {
return resolvedVariable;
}
}
}
Loading