1   package eu.fbk.knowledgestore.server.http.jaxrs;
2   
3   import com.google.common.base.MoreObjects;
4   import com.google.common.base.Preconditions;
5   import com.google.common.base.Strings;
6   import com.google.common.collect.ImmutableList;
7   import com.google.common.collect.ImmutableMap;
8   import com.google.common.collect.ImmutableSet;
9   import com.google.common.collect.Lists;
10  import com.google.common.net.HttpHeaders;
11  import eu.fbk.knowledgestore.KnowledgeStore;
12  import eu.fbk.knowledgestore.OperationException;
13  import eu.fbk.knowledgestore.Outcome;
14  import eu.fbk.knowledgestore.data.*;
15  import eu.fbk.knowledgestore.internal.Logging;
16  import eu.fbk.knowledgestore.internal.Util;
17  import eu.fbk.knowledgestore.internal.jaxrs.Protocol;
18  import eu.fbk.knowledgestore.internal.jaxrs.Serializer;
19  import eu.fbk.knowledgestore.server.http.CustomConfig;
20  import eu.fbk.knowledgestore.server.http.UIConfig;
21  import org.eclipse.jetty.server.Server;
22  import org.glassfish.jersey.message.DeflateEncoder;
23  import org.glassfish.jersey.message.GZipEncoder;
24  import org.glassfish.jersey.message.internal.HttpDateFormat;
25  import org.glassfish.jersey.server.ResourceConfig;
26  import org.glassfish.jersey.server.ServerProperties;
27  import org.glassfish.jersey.server.mvc.mustache.MustacheMvcFeature;
28  import org.openrdf.model.URI;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  import org.slf4j.MDC;
32  
33  import javax.annotation.Nullable;
34  import javax.servlet.ServletContext;
35  import javax.ws.rs.Produces;
36  import javax.ws.rs.WebApplicationException;
37  import javax.ws.rs.container.*;
38  import javax.ws.rs.core.*;
39  import javax.ws.rs.core.Response.ResponseBuilder;
40  import javax.ws.rs.core.Response.Status;
41  import javax.ws.rs.ext.*;
42  import java.io.IOException;
43  import java.lang.annotation.Annotation;
44  import java.lang.reflect.Type;
45  import java.security.Principal;
46  import java.text.SimpleDateFormat;
47  import java.util.*;
48  import java.util.concurrent.Future;
49  import java.util.concurrent.TimeUnit;
50  
51  public final class Application extends javax.ws.rs.core.Application {
52  
53      public static final String STORE_ATTRIBUTE = "store";
54  
55      public static final String TRACING_ATTRIBUTE = "tracing";
56  
57      public static final String RESOURCE_ATTRIBUTE = "resource";
58  
59      public static final String UI_ATTRIBUTE = "ui";
60  
61      public static final String CUSTOM_ATTRIBUTE = "custom";
62  
63      public static final int DEFAULT_TIMEOUT = 600000; // 600 sec; TODO: make this customizable
64  
65      public static final int GRACE_PERIOD = 5000; // 5 sec extra beyond timeout
66  
67      private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);
68  
69      private static final String SERVER = String.format("KnowledgeStore/%s Jetty/%s",
70              Util.getVersion("eu.fbk.knowledgestore", "ks-core", "devel"), Server.getVersion());
71  
72      private static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
73  
74      private static ThreadLocal<URI> INVOCATION_ID = new ThreadLocal<URI>();
75  
76      private static ThreadLocal<URI> OBJECT_ID = new ThreadLocal<URI>();
77  
78      private static ThreadLocal<List<MediaType>> ACCEPT = new ThreadLocal<List<MediaType>>(); // \m/
79  
80      private static ThreadLocal<Future<?>> TIMEOUT_FUTURE = new ThreadLocal<Future<?>>();
81  
82      private static long invocationCounter = 0;
83  
84      private final UIConfig uiConfig;
85  
86      private final KnowledgeStore store;
87  
88      private final Set<Class<?>> classes;
89  
90      private final Set<Object> singletons;
91  
92      private final Map<String, Object> properties;
93      private final Map<String, CustomConfig> customConfigs;
94  
95      private int pendingModifications;
96  
97      private Date lastModified;
98  
99      @SuppressWarnings("unchecked")
100     public Application(@Context final ServletContext context) {
101         this((UIConfig) context.getAttribute(UI_ATTRIBUTE), //
102                 (KnowledgeStore) context.getAttribute(STORE_ATTRIBUTE), //
103                 (Boolean) context.getAttribute(TRACING_ATTRIBUTE), //
104                 (Iterable<? extends Class<?>>) context.getAttribute(RESOURCE_ATTRIBUTE),
105                 (Iterable<CustomConfig>) context.getAttribute(CUSTOM_ATTRIBUTE));
106     }
107 
108     public Application(final UIConfig uiConfig, final KnowledgeStore store,
109             final Boolean enableTracing, final Iterable<? extends Class<?>> resourceClasses,
110                        final Iterable<CustomConfig> configs) {
111 
112         // keep track of KS and UI config
113         this.store = Preconditions.checkNotNull(store);
114         this.uiConfig = Preconditions.checkNotNull(uiConfig);
115         customConfigs = new HashMap<>();
116         if (configs != null) {
117 			for (CustomConfig config : configs) {
118 				customConfigs.put(config.getName(), config);
119 			}
120 		}
121 
122         // define JAX-RS classes
123         final ImmutableSet.Builder<Class<?>> classes = ImmutableSet.builder();
124         classes.add(DeflateEncoder.class);
125         classes.add(GZipEncoder.class);
126         for (final Class<?> resourceClass : resourceClasses) {
127             classes.add(resourceClass);
128         }
129         classes.add(Converter.class);
130         classes.add(Filter.class);
131         classes.add(Mapper.class);
132         classes.add(Serializer.class);
133         classes.add(MustacheMvcFeature.class);
134         this.classes = classes.build();
135 
136         // define singletons
137         this.singletons = ImmutableSet.of();
138 
139         // define JAX-RS properties
140         final ImmutableMap.Builder<String, Object> properties = ImmutableMap.builder();
141         properties.put(ServerProperties.APPLICATION_NAME, "KnowledgeStore");
142         if (Boolean.TRUE.equals(enableTracing)) {
143             properties.put(ServerProperties.TRACING, "ALL");
144             properties.put(ServerProperties.TRACING_THRESHOLD, "TRACE");
145 
146             // note: in a particular instance we observed 1GB ram being used by Jersey monitoring
147             // code after 1h uptime and only three requests received by the server (!). Therefore,
148             // enable these settings only if strictly necessary
149             properties.put(ServerProperties.MONITORING_STATISTICS_ENABLED, true);
150             properties.put(ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED, true);
151         }
152         properties.put(ServerProperties.WADL_FEATURE_DISABLE, false);
153         properties.put(ServerProperties.JSON_PROCESSING_FEATURE_DISABLE, true); // JSONLD used
154         properties.put(ServerProperties.METAINF_SERVICES_LOOKUP_DISABLE, true); // not used
155         properties.put(ServerProperties.MOXY_JSON_FEATURE_DISABLE, true); // not used
156         properties.put(ServerProperties.OUTBOUND_CONTENT_LENGTH_BUFFER, 8192); // default value
157         properties.put(MustacheMvcFeature.CACHE_TEMPLATES, true);
158         properties.put(MustacheMvcFeature.TEMPLATE_BASE_PATH,
159                 "/eu/fbk/knowledgestore/server/http/jaxrs/");
160         this.properties = properties.build();
161 
162         // Initialize globally last modified variables
163         this.pendingModifications = 0;
164         this.lastModified = new Date();
165     }
166 
167     public Map<String, CustomConfig> getCustomConfigs() {
168         return customConfigs;
169     }
170 
171     public UIConfig getUIConfig() {
172         return this.uiConfig;
173     }
174 
175     public KnowledgeStore getStore() {
176         return this.store;
177     }
178 
179     @Override
180     public Set<Class<?>> getClasses() {
181         return this.classes;
182     }
183 
184     @Override
185     public Set<Object> getSingletons() {
186         return this.singletons;
187     }
188 
189     @Override
190     public Map<String, Object> getProperties() {
191         return this.properties;
192     }
193 
194     public synchronized Date getLastModified() {
195         return this.pendingModifications == 0 ? this.lastModified : new Date();
196     }
197 
198     synchronized void beginModification() {
199         ++this.pendingModifications;
200     }
201 
202     synchronized void endModification() {
203         --this.pendingModifications;
204         if (this.pendingModifications == 0) {
205             this.lastModified = new Date();
206         }
207     }
208 
209     static Application unwrap(final javax.ws.rs.core.Application application) {
210         if (application instanceof Application) {
211             return (Application) application;
212         } else if (application instanceof ResourceConfig) {
213             return (Application) ((ResourceConfig) application).getApplication();
214         }
215         Preconditions.checkNotNull(application, "Null application");
216         throw new IllegalArgumentException("Invalid application class "
217                 + application.getClass().getName());
218     }
219 
220     @Provider
221     static final class Converter implements ParamConverterProvider {
222 
223         private static final ParamConverter<URI> URI_CONVERTER = new ParamConverter<URI>() {
224 
225             @Override
226             public URI fromString(final String string) {
227                 try {
228                     return (URI) Data.parseValue(string, Data.getNamespaceMap());
229                 } catch (final ParseException ex) {
230                     throw new WebApplicationException(ex.getMessage(), Status.BAD_REQUEST);
231                 }
232             }
233 
234             @Override
235             public String toString(final URI uri) {
236                 return Data.toString(uri, null); // no QNames for max compatibility
237             }
238 
239         };
240 
241         private static final ParamConverter<XPath> XPATH_CONVERTER = new ParamConverter<XPath>() {
242 
243             @Override
244             public XPath fromString(final String string) {
245                 try {
246                     return XPath.parse(Data.getNamespaceMap(), string);
247                 } catch (final ParseException ex) {
248                     throw new WebApplicationException(ex.getMessage(), Status.BAD_REQUEST);
249                 }
250             }
251 
252             @Override
253             public String toString(final XPath xpath) {
254                 return xpath.toString();
255             }
256 
257         };
258 
259         private static final ParamConverter<Criteria> CRITERIA_CONVERTER = new ParamConverter<Criteria>() {
260 
261             @Override
262             public Criteria fromString(final String string) {
263                 try {
264                     return Criteria.parse(string, Data.getNamespaceMap());
265                 } catch (final ParseException ex) {
266                     throw new WebApplicationException(ex.getMessage(), Status.BAD_REQUEST);
267                 }
268             }
269 
270             @Override
271             public String toString(final Criteria criteria) {
272                 return criteria.toString();
273             }
274 
275         };
276 
277         @SuppressWarnings("unchecked")
278         @Override
279         public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType,
280                 final Annotation[] annotations) {
281 
282             if (rawType.equals(URI.class)) {
283                 return (ParamConverter<T>) URI_CONVERTER;
284             } else if (rawType.equals(XPath.class)) {
285                 return (ParamConverter<T>) XPATH_CONVERTER;
286             } else if (rawType.equals(Criteria.class)) {
287                 return (ParamConverter<T>) CRITERIA_CONVERTER;
288             }
289             return null;
290         }
291 
292     }
293 
294     @Provider
295     @PreMatching
296     static final class Filter implements ContainerRequestFilter, ContainerResponseFilter,
297             WriterInterceptor {
298 
299         private static final String PROPERTY_TIMESTAMP = "timestamp";
300 
301         @Override
302         public void filter(final ContainerRequestContext request) throws IOException {
303 
304             // Keep timestamp
305             final long timestamp = System.currentTimeMillis();
306             request.setProperty(PROPERTY_TIMESTAMP, timestamp);
307 
308             // Extract Accept types either from headers or query parameters
309             List<MediaType> acceptTypes = request.getAcceptableMediaTypes();
310             String accept = request.getUriInfo().getQueryParameters()
311                     .getFirst(Protocol.PARAMETER_ACCEPT);
312             if (accept == null) {
313                 accept = MoreObjects.firstNonNull(request.getHeaderString(HttpHeaders.ACCEPT),
314                         "*/*");
315             } else {
316                 request.getHeaders().putSingle(HttpHeaders.ACCEPT, accept);
317                 acceptTypes = Lists.newArrayList();
318                 for (final String type : accept.split(",")) {
319                     acceptTypes.add(MediaType.valueOf(type.trim()));
320                 }
321             }
322 
323             // Extract timeout parameter
324             long timeout = DEFAULT_TIMEOUT;
325             try {
326                 final Thread thread = Thread.currentThread();
327                 final String timeoutString = Strings.nullToEmpty(
328                         request.getUriInfo().getQueryParameters()
329                                 .getFirst(Protocol.PARAMETER_TIMEOUT)).trim();
330                 final long theTimeout = "".equals(timeoutString) ? DEFAULT_TIMEOUT : Long
331                         .parseLong(timeoutString) * 1000;
332                 timeout = theTimeout;
333                 TIMEOUT_FUTURE.set(Data.getExecutor().schedule(new Runnable() {
334 
335                     @Override
336                     public void run() {
337                         synchronized (Filter.this) {
338                             LOGGER.info("Http: Request timed out after {} ms", theTimeout);
339                             thread.interrupt(); // Let's hope this will enforce the timeout
340                         }
341                     }
342 
343                 }, timeout + GRACE_PERIOD, TimeUnit.MILLISECONDS));
344             } catch (final Throwable ex) {
345                 // Ignore invalid timeout
346             }
347 
348             // Extract information from the request
349             final URI invocationID = extractInvocationID(request);
350             final URI objectID = extractObjectID(request);
351             final String username = extractUsername(request);
352             final boolean chunkedInput = extractChunkedInput(request);
353             final boolean cachingEnabled = extractCachingEnabled(request);
354 
355             // Store relevant attribute as request properties (not visible to REST resources)
356             INVOCATION_ID.set(invocationID);
357             OBJECT_ID.set(objectID);
358             ACCEPT.set(acceptTypes); // required by mapper
359 
360             // Update the MDC context with the invocation ID, so to bind log messages to it.
361             // Invocation ID will be removed from MDC when request processing is complete
362             MDC.put(Logging.MDC_CONTEXT, invocationID.stringValue());
363 
364             // Configure REST resources for this request
365             Resource.begin(invocationID, objectID, username, chunkedInput, cachingEnabled, timeout);
366 
367             // Store invocation ID and record type as request headers to be used by serializers
368             request.getHeaders().putSingle(Protocol.HEADER_INVOCATION, invocationID.stringValue());
369 
370             // Log the request
371             if (LOGGER.isDebugEnabled()) {
372                 final String etag = request.getHeaders().getFirst(HttpHeaders.IF_NONE_MATCH);
373                 final String lastModified = reformatDate(request.getHeaders().getFirst(
374                         HttpHeaders.IF_MODIFIED_SINCE));
375                 final StringBuilder builder = new StringBuilder("Http: ");
376                 builder.append(request.getMethod());
377                 builder.append(' ').append(request.getUriInfo().getRequestUri());
378                 builder.append(' ').append(accept);
379                 final String type = request.getHeaderString(HttpHeaders.CONTENT_TYPE);
380                 if (type != null) {
381                     builder.append(' ').append(type);
382                 }
383                 final String encoding = request.getHeaderString(HttpHeaders.CONTENT_ENCODING);
384                 if (encoding != null) {
385                     builder.append(' ').append(encoding);
386                 }
387                 if (etag != null) {
388                     builder.append(' ').append(etag);
389                 }
390                 if (lastModified != null) {
391                     builder.append('/').append(lastModified);
392                 }
393                 final Principal user = request.getSecurityContext().getUserPrincipal();
394                 if (user != null) {
395                     builder.append(' ').append(user.getName());
396                 }
397                 LOGGER.debug(builder.toString());
398             }
399         }
400 
401         @Override
402         public void filter(final ContainerRequestContext request,
403                 final ContainerResponseContext response) throws IOException {
404 
405             try {
406                 // Retrieve relevant attributes of the request
407                 final URI invocationID = INVOCATION_ID.get();
408 
409                 // Set response headers
410                 response.getHeaders().putSingle("Server", SERVER);
411                 response.getHeaders().add(Protocol.HEADER_INVOCATION, invocationID.stringValue());
412 
413                 // Log the response
414                 if (LOGGER.isDebugEnabled()) {
415                     final long elapsed = System.currentTimeMillis()
416                             - (Long) request.getProperty(PROPERTY_TIMESTAMP);
417                     final StringBuilder builder = new StringBuilder();
418                     builder.append("Http: status ");
419                     builder.append(response.getStatus());
420                     if (response.hasEntity()) {
421                         final String etag = response.getHeaderString(HttpHeaders.ETAG);
422                         if (etag != null) {
423                             builder.append(", ").append(etag);
424                         } else {
425                             builder.append(", ").append(response.getMediaType());
426                         }
427                         try {
428                             final Date lastModified = response.getLastModified();
429                             if (lastModified != null) {
430                                 synchronized (DATE_FORMAT) {
431                                     builder.append(", ").append(DATE_FORMAT.format(lastModified));
432                                 }
433                             }
434                         } catch (final Throwable ex) {
435                             // ignore parsing errors
436                         }
437                     }
438                     builder.append(", ").append(elapsed).append(" ms");
439                     LOGGER.debug(builder.toString());
440                 }
441 
442             } finally {
443                 // Restore MDC and Resource thread-level data if processing ends here (no entity)
444                 if (response.getEntity() == null) {
445                     complete();
446                 }
447             }
448         }
449 
450         @Override
451         public void aroundWriteTo(final WriterInterceptorContext context) throws IOException,
452                 WebApplicationException {
453 
454             try {
455                 // Emit the response body
456                 context.proceed();
457 
458             } finally {
459                 // Restore MDC and Resource thread-level data
460                 complete();
461             }
462         }
463 
464         private void complete() {
465             Resource.end();
466             final Future<?> future = TIMEOUT_FUTURE.get();
467             if (future != null) {
468                 TIMEOUT_FUTURE.set(null);
469                 future.cancel(false);
470                 // synchronization force waiting for the timeout runnable to complete
471                 synchronized (Filter.this) {
472                     Thread.interrupted(); // clear interrupted status
473                 }
474             }
475             MDC.remove(Logging.MDC_CONTEXT);
476         }
477 
478         private static URI extractInvocationID(final ContainerRequestContext request) {
479             final String id = request.getHeaderString(Protocol.HEADER_INVOCATION);
480             if (id != null) {
481                 try {
482                     return Data.getValueFactory().createURI(id);
483                 } catch (final Throwable ex) {
484                     // not valid: ignore
485                 }
486             }
487             final long ts = System.currentTimeMillis();
488             long counterSnapshot;
489             synchronized (Application.class) {
490                 ++invocationCounter;
491                 if (invocationCounter < ts) {
492                     invocationCounter = ts;
493                 }
494                 counterSnapshot = invocationCounter;
495             }
496             return Data.getValueFactory().createURI("req:" + Long.toString(counterSnapshot, 32));
497         }
498 
499         private static URI extractObjectID(final ContainerRequestContext request) {
500             final List<String> ids = request.getUriInfo().getQueryParameters().get("id");
501             if (ids != null && ids.size() == 1) {
502                 try {
503                     return (URI) Data.parseValue(ids.get(0), Data.getNamespaceMap());
504                 } catch (final Throwable ex) {
505                     // ignore
506                 }
507             }
508             return null;
509         }
510 
511         private static String extractUsername(final ContainerRequestContext request) {
512             final SecurityContext context = request.getSecurityContext();
513             if (context != null && context.getUserPrincipal() != null) {
514                 final Principal principal = context.getUserPrincipal();
515                 if (principal != null) {
516                     return principal.getName();
517                 }
518             }
519             return null;
520         }
521 
522         private static boolean extractChunkedInput(final ContainerRequestContext request) {
523             final List<String> values = request.getHeaders().get(Protocol.HEADER_CHUNKED);
524             return values != null && values.size() == 1 && "true".equalsIgnoreCase(values.get(0));
525         }
526 
527         private static boolean extractCachingEnabled(final ContainerRequestContext request) {
528             final List<String> values = request.getHeaders().get("Cache-Control");
529             if (values != null && values.size() == 1) {
530                 try {
531                     final CacheControl cacheControl = CacheControl.valueOf(values.get(0));
532                     return !cacheControl.isNoCache() && !cacheControl.isNoStore();
533                 } catch (final Throwable ex) {
534                     // ignore
535                 }
536             }
537             return true; // default
538         }
539 
540         @Nullable
541         private static String reformatDate(@Nullable final String httpDate) {
542             if (httpDate != null) {
543                 try {
544                     final Date date = HttpDateFormat.readDate(httpDate);
545                     synchronized (DATE_FORMAT) {
546                         return DATE_FORMAT.format(date);
547                     }
548                 } catch (final Throwable ex) {
549                     // ignore
550                 }
551             }
552             return null;
553         }
554 
555     }
556 
557     @Provider
558     @Produces(Protocol.MIME_TYPES_RDF)
559     static final class Mapper implements ExceptionMapper<Throwable> {
560 
561         private static final List<MediaType> RDF_TYPES;
562 
563         static {
564             final ImmutableList.Builder<MediaType> builder = ImmutableList.builder();
565             for (final String token : Protocol.MIME_TYPES_RDF.split(",")) {
566                 builder.add(MediaType.valueOf(token.trim()));
567             }
568             RDF_TYPES = builder.build();
569         }
570 
571         private static MediaType selectType() {
572             for (final MediaType acceptableType : ACCEPT.get()) {
573                 for (final MediaType supportedType : RDF_TYPES) {
574                     if (acceptableType.isCompatible(supportedType)) {
575                         return supportedType;
576                     }
577                 }
578             }
579             return RDF_TYPES.get(0);
580         }
581 
582         @Override
583         public Response toResponse(final Throwable throwable) {
584 
585             // Try to unwrap the exception
586             final Throwable ex = throwable instanceof RuntimeException
587                     && throwable.getCause() instanceof OperationException ? throwable.getCause()
588                     : throwable;
589 
590             // Retrieve relevant attributes of the request
591             final URI invocationID = INVOCATION_ID.get();
592             final URI objectID = OBJECT_ID.get();
593 
594             // Determine HTTP status and Outcome from the exception
595             int httpStatus;
596             MultivaluedMap<String, Object> headers = null;
597             Outcome outcome = null;
598 
599             if (ex instanceof OperationException) {
600                 outcome = ((OperationException) ex).getOutcome();
601                 httpStatus = outcome.getStatus().getHTTPStatus();
602 
603             } else if (ex instanceof WebApplicationException) {
604                 Outcome.Status status = null;
605                 final Response exResponse = ((WebApplicationException) ex).getResponse();
606                 headers = exResponse.getHeaders();
607                 httpStatus = exResponse.getStatus();
608                 if (httpStatus >= 400 && httpStatus != Status.PRECONDITION_FAILED.getStatusCode()) {
609                     status = Outcome.Status.valueOf(httpStatus);
610                     outcome = Outcome.create(status, invocationID, objectID, exResponse
611                             .hasEntity() ? exResponse.getEntity().toString() : ex.getMessage());
612                 }
613 
614             } else {
615                 httpStatus = Status.INTERNAL_SERVER_ERROR.getStatusCode();
616                 outcome = Outcome.create(Outcome.Status.ERROR_UNEXPECTED, invocationID, objectID,
617                         ex.getMessage() + " [" + ex.getClass().getSimpleName() + "]");
618             }
619 
620             // Log the exception in case of server error
621             if (httpStatus >= 500) {
622                 LOGGER.error("Http: reporting server error " + httpStatus, ex);
623             } else if (httpStatus >= 400) {
624                 LOGGER.debug("Http: reporting client error: " + httpStatus + " - "
625                         + ex.getMessage() + " (" + ex.getClass().getSimpleName() + ")");
626             }
627 
628             // Build and return the response.
629             final ResponseBuilder builder = Response.status(httpStatus);
630             if (outcome != null && httpStatus >= 400
631                     && httpStatus != Status.PRECONDITION_FAILED.getStatusCode()) {
632                 final CacheControl cacheControl = new CacheControl();
633                 cacheControl.setNoStore(true);
634                 builder.entity(
635                         new GenericEntity<Stream<Outcome>>(Stream.create(outcome),
636                                 Protocol.STREAM_OF_OUTCOMES.getType())).cacheControl(cacheControl)
637                         .type(selectType());
638             }
639             if (headers != null) {
640                 for (final Map.Entry<String, List<Object>> entry : headers.entrySet()) {
641                     final String name = entry.getKey();
642                     for (final Object value : entry.getValue()) {
643                         builder.header(name, value);
644                     }
645                 }
646             }
647             return builder.build();
648         }
649     }
650 
651 }