Skip to content

Commit 2b175b7

Browse files
committed
1 parent bfc801a commit 2b175b7

File tree

6 files changed

+410
-7
lines changed

6 files changed

+410
-7
lines changed

src/main/java/graphql/servlet/AbstractGraphQLHttpServlet.java

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import graphql.introspection.IntrospectionQuery;
77
import graphql.schema.GraphQLFieldDefinition;
88
import graphql.servlet.internal.GraphQLRequest;
9+
import graphql.servlet.internal.VariableMapper;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
1112

@@ -16,8 +17,18 @@
1617
import javax.servlet.http.HttpServletRequest;
1718
import javax.servlet.http.HttpServletResponse;
1819
import javax.servlet.http.Part;
19-
import java.io.*;
20-
import java.util.*;
20+
import java.io.BufferedInputStream;
21+
import java.io.ByteArrayOutputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.Writer;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.Objects;
31+
import java.util.Optional;
2132
import java.util.function.BiConsumer;
2233
import java.util.function.Consumer;
2334
import java.util.function.Function;
@@ -109,7 +120,37 @@ public AbstractGraphQLHttpServlet(List<GraphQLServletListener> listeners, boolea
109120
Collections::singletonList,
110121
(l1, l2) -> Stream.concat(l1.stream(), l2.stream()).collect(Collectors.toList())));
111122

112-
if (fileItems.containsKey("graphql")) {
123+
if(fileItems.containsKey("operations")) {
124+
final Optional<Part> queryItem = getFileItem(fileItems, "operations");
125+
if (queryItem.isPresent()) {
126+
InputStream inputStream = queryItem.get().getInputStream();
127+
128+
if (!inputStream.markSupported()) {
129+
inputStream = new BufferedInputStream(inputStream);
130+
}
131+
132+
final Optional<Map<String, List<String>>> variablesMap =
133+
getFileItem(fileItems, "map")
134+
.map(graphQLObjectMapper::deserializeMultipartMap);
135+
136+
if (isBatchedQuery(inputStream)) {
137+
List<GraphQLRequest> graphQLRequests = graphQLObjectMapper.readBatchedGraphQLRequest(inputStream);
138+
variablesMap.ifPresent(map -> graphQLRequests.forEach(r -> mapMultipartVariables(r, map, fileItems)));
139+
GraphQLBatchedInvocationInput invocationInput = invocationInputFactory.create(graphQLRequests, request);
140+
invocationInput.getContext().setFiles(fileItems);
141+
queryBatched(queryInvoker, graphQLObjectMapper, invocationInput, response);
142+
return;
143+
} else {
144+
GraphQLRequest graphQLRequest = graphQLObjectMapper.readGraphQLRequest(inputStream);
145+
variablesMap.ifPresent(m -> mapMultipartVariables(graphQLRequest, m, fileItems));
146+
GraphQLSingleInvocationInput invocationInput =
147+
invocationInputFactory.create(graphQLRequest, request);
148+
invocationInput.getContext().setFiles(fileItems);
149+
query(queryInvoker, graphQLObjectMapper, invocationInput, response);
150+
return;
151+
}
152+
}
153+
} else if (fileItems.containsKey("graphql")) {
113154
final Optional<Part> graphqlItem = getFileItem(fileItems, "graphql");
114155
if (graphqlItem.isPresent()) {
115156
InputStream inputStream = graphqlItem.get().getInputStream();
@@ -190,6 +231,22 @@ public AbstractGraphQLHttpServlet(List<GraphQLServletListener> listeners, boolea
190231
};
191232
}
192233

234+
private void mapMultipartVariables(GraphQLRequest request,
235+
Map<String, List<String>> variablesMap,
236+
Map<String, List<Part>> fileItems)
237+
{
238+
Map<String, Object> variables = request.getVariables();
239+
240+
variablesMap.forEach((partName, objectPaths) -> {
241+
Part part = getFileItem(fileItems, partName)
242+
.orElseThrow(() -> new RuntimeException("unable to find part name " +
243+
partName +
244+
" as referenced in the variables map"));
245+
246+
objectPaths.forEach(objectPath -> VariableMapper.mapVariable(objectPath, variables, part));
247+
});
248+
}
249+
193250
public void addListener(GraphQLServletListener servletListener) {
194251
listeners.add(servletListener);
195252
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package graphql.servlet;
2+
3+
import graphql.schema.Coercing;
4+
import graphql.schema.CoercingParseLiteralException;
5+
import graphql.schema.CoercingParseValueException;
6+
import graphql.schema.CoercingSerializeException;
7+
import graphql.schema.GraphQLScalarType;
8+
9+
import javax.servlet.http.Part;
10+
11+
public class ApolloScalars {
12+
public static final GraphQLScalarType Upload =
13+
new GraphQLScalarType("Upload",
14+
"A file part in a multipart request",
15+
new Coercing<Part, Void>() {
16+
@Override
17+
public Void serialize(Object dataFetcherResult) {
18+
throw new CoercingSerializeException("Upload is an input-only type");
19+
}
20+
21+
@Override
22+
public Part parseValue(Object input) {
23+
if (input instanceof Part) {
24+
return (Part) input;
25+
} else if (null == input) {
26+
return null;
27+
} else {
28+
throw new CoercingParseValueException("Expected type " +
29+
Part.class.getName() +
30+
" but was " +
31+
input.getClass().getName());
32+
}
33+
}
34+
35+
@Override
36+
public Part parseLiteral(Object input) {
37+
throw new CoercingParseLiteralException(
38+
"Must use variables to specify Upload values");
39+
}
40+
});
41+
}

src/main/java/graphql/servlet/GraphQLObjectMapper.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
package graphql.servlet;
22

33
import com.fasterxml.jackson.core.JsonProcessingException;
4-
import com.fasterxml.jackson.databind.InjectableValues;
4+
import com.fasterxml.jackson.core.type.TypeReference;
55
import com.fasterxml.jackson.databind.MappingIterator;
66
import com.fasterxml.jackson.databind.ObjectMapper;
77
import com.fasterxml.jackson.databind.ObjectReader;
8-
import com.fasterxml.jackson.databind.SerializationFeature;
9-
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
108
import graphql.ExecutionResult;
119
import graphql.ExecutionResultImpl;
1210
import graphql.GraphQLError;
1311
import graphql.servlet.internal.GraphQLRequest;
1412
import graphql.servlet.internal.VariablesDeserializer;
1513

14+
import javax.servlet.http.Part;
1615
import java.io.IOException;
1716
import java.io.InputStream;
1817
import java.util.ArrayList;
@@ -25,6 +24,9 @@
2524
* @author Andrew Potter
2625
*/
2726
public class GraphQLObjectMapper {
27+
private static final TypeReference<Map<String, List<String>>>
28+
MULTIPART_MAP_TYPE_REFERENCE = new TypeReference<Map<String, List<String>>>() {
29+
};
2830
private final ObjectMapperProvider objectMapperProvider;
2931
private final Supplier<GraphQLErrorHandler> graphQLErrorHandlerSupplier;
3032

@@ -147,6 +149,14 @@ public Map<String, Object> deserializeVariables(String variables) {
147149
}
148150
}
149151

152+
public Map<String,List<String>> deserializeMultipartMap(Part part) {
153+
try {
154+
return getJacksonMapper().readValue(part.getInputStream(), MULTIPART_MAP_TYPE_REFERENCE);
155+
} catch (IOException e) {
156+
throw new RuntimeException(e);
157+
}
158+
}
159+
150160
public static Builder newBuilder() {
151161
return new Builder();
152162
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package graphql.servlet.internal;
2+
3+
import javax.servlet.http.Part;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.regex.Pattern;
7+
8+
public class VariableMapper {
9+
private static final Pattern PERIOD = Pattern.compile("\\.");
10+
11+
private static final Mapper<Map<String, Object>> MAP_MAPPER = new Mapper<Map<String, Object>>() {
12+
@Override
13+
public Object set(Map<String, Object> location, String target, Part value) {
14+
return location.put(target, value);
15+
}
16+
17+
@Override
18+
public Object recurse(Map<String, Object> location, String target) {
19+
return location.get(target);
20+
}
21+
};
22+
private static final Mapper<List<Object>> LIST_MAPPER = new Mapper<List<Object>>() {
23+
@Override
24+
public Object set(List<Object> location, String target, Part value) {
25+
return location.set(Integer.parseInt(target), value);
26+
}
27+
28+
@Override
29+
public Object recurse(List<Object> location, String target) {
30+
return location.get(Integer.parseInt(target));
31+
}
32+
};
33+
34+
public static void mapVariable(String objectPath, Map<String, Object> variables, Part part) {
35+
String[] segments = PERIOD.split(objectPath);
36+
37+
if (segments.length < 2) {
38+
throw new RuntimeException("object-path in map must have at least two segments");
39+
} else if (!"variables".equals(segments[0])) {
40+
throw new RuntimeException("can only map into variables");
41+
}
42+
43+
Object currentLocation = variables;
44+
for (int i = 1; i < segments.length; i++) {
45+
String segmentName = segments[i];
46+
Mapper mapper = determineMapper(currentLocation, objectPath, segmentName);
47+
48+
if (i == segments.length - 1) {
49+
if (null != mapper.set(currentLocation, segmentName, part)) {
50+
throw new RuntimeException("expected null value when mapping " + objectPath);
51+
}
52+
} else {
53+
currentLocation = mapper.recurse(currentLocation, segmentName);
54+
if (null == currentLocation) {
55+
throw new RuntimeException("found null intermediate value when trying to map " + objectPath);
56+
}
57+
}
58+
}
59+
}
60+
61+
private static Mapper<?> determineMapper(Object currentLocation, String objectPath, String segmentName) {
62+
if (currentLocation instanceof Map) {
63+
return MAP_MAPPER;
64+
} else if (currentLocation instanceof List) {
65+
return LIST_MAPPER;
66+
}
67+
68+
throw new RuntimeException("expected a map or list at " + segmentName + " when trying to map " + objectPath);
69+
}
70+
71+
interface Mapper<T> {
72+
Object set(T location, String target, Part value);
73+
74+
Object recurse(T location, String target);
75+
}
76+
}

0 commit comments

Comments
 (0)