protected List<String> serviceNamesNotFoundInZipkin(List<io.zipkin.Span> spans) {
   List<String> serviceNamesFoundInAnnotations =
       spans
           .stream()
           .filter(span -> span.annotations != null)
           .map(span -> span.annotations)
           .flatMap(Collection::stream)
           .filter(span -> span.endpoint != null)
           .map(annotation -> annotation.endpoint)
           .map(endpoint -> endpoint.serviceName)
           .distinct()
           .collect(Collectors.toList());
   List<String> serviceNamesFoundInBinaryAnnotations =
       spans
           .stream()
           .filter(span -> span.binaryAnnotations != null)
           .map(span -> span.binaryAnnotations)
           .flatMap(Collection::stream)
           .filter(span -> span.endpoint != null)
           .map(annotation -> annotation.endpoint)
           .map(endpoint -> endpoint.serviceName)
           .distinct()
           .collect(Collectors.toList());
   List<String> names = new ArrayList<>();
   names.addAll(serviceNamesFoundInAnnotations);
   names.addAll(serviceNamesFoundInBinaryAnnotations);
   return names.contains(getAppName()) ? Collections.emptyList() : names;
 }
 protected List<String> annotationsNotFoundInZipkin(List<io.zipkin.Span> spans) {
   String binaryAnnotationName = getRequiredBinaryAnnotationName();
   Optional<String> names =
       spans
           .stream()
           .filter(span -> span.binaryAnnotations != null)
           .map(span -> span.binaryAnnotations)
           .flatMap(Collection::stream)
           .filter(span -> span.endpoint != null)
           .map(annotation -> annotation.key)
           .filter(binaryAnnotationName::equals)
           .findFirst();
   return names.isPresent()
       ? Collections.emptyList()
       : Collections.singletonList(binaryAnnotationName);
 }
 /**
  * Retrieve the attribute of an entity
  *
  * @param entityId the entity ID
  * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
  *     null or zero-length for empty
  * @param attributeName the attribute name
  * @return
  */
 public ListenableFuture<String> getAttributeValueAsString(
     String entityId, String type, String attributeName) {
   UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
   builder.path("v2/entities/{entityId}/attrs/{attributeName}/value");
   addParam(builder, "type", type);
   HttpHeaders httpHeaders = cloneHttpHeaders();
   httpHeaders.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN));
   return adapt(
       request(
           HttpMethod.GET,
           builder.buildAndExpand(entityId, attributeName).toUriString(),
           httpHeaders,
           null,
           String.class));
 }
 private Ngsi2Client() {
   // set default headers for Content-Type and Accept to application/JSON
   httpHeaders = new HttpHeaders();
   httpHeaders.setContentType(MediaType.APPLICATION_JSON);
   httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
 }
/** NGSIv2 API Client */
public class Ngsi2Client {

  private static final Map<String, ?> noParams = Collections.emptyMap();

  private AsyncRestTemplate asyncRestTemplate;

  private HttpHeaders httpHeaders;

  private String baseURL;

  private Ngsi2Client() {
    // set default headers for Content-Type and Accept to application/JSON
    httpHeaders = new HttpHeaders();
    httpHeaders.setContentType(MediaType.APPLICATION_JSON);
    httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
  }

  /**
   * Default constructor
   *
   * @param asyncRestTemplate AsyncRestTemplate to handle requests
   * @param baseURL base URL for the NGSIv2 service
   */
  public Ngsi2Client(AsyncRestTemplate asyncRestTemplate, String baseURL) {
    this();
    this.asyncRestTemplate = asyncRestTemplate;
    this.baseURL = baseURL;

    // Inject NGSI2 error handler and Java 8 support
    injectNgsi2ErrorHandler();
    injectJava8ObjectMapper();
  }

  /** @return the list of supported operations under /v2 */
  public ListenableFuture<Map<String, String>> getV2() {
    ListenableFuture<ResponseEntity<JsonNode>> responseFuture =
        request(HttpMethod.GET, baseURL + "v2", null, JsonNode.class);
    return new ListenableFutureAdapter<Map<String, String>, ResponseEntity<JsonNode>>(
        responseFuture) {
      @Override
      protected Map<String, String> adapt(ResponseEntity<JsonNode> result)
          throws ExecutionException {
        Map<String, String> services = new HashMap<>();
        result
            .getBody()
            .fields()
            .forEachRemaining(entry -> services.put(entry.getKey(), entry.getValue().textValue()));
        return services;
      }
    };
  }

  /*
   * Entities requests
   */

  /**
   * Retrieve a list of Entities (simplified)
   *
   * @param ids an optional list of entity IDs (cannot be used with idPatterns)
   * @param idPattern an optional pattern of entity IDs (cannot be used with ids)
   * @param types an optional list of types of entity
   * @param attrs an optional list of attributes to return for all entities
   * @param offset an optional offset (0 for none)
   * @param limit an optional limit (0 for none)
   * @param count true to return the total number of matching entities
   * @return a pagined list of Entities
   */
  public ListenableFuture<Paginated<Entity>> getEntities(
      Collection<String> ids,
      String idPattern,
      Collection<String> types,
      Collection<String> attrs,
      int offset,
      int limit,
      boolean count) {

    return getEntities(ids, idPattern, types, attrs, null, null, null, offset, limit, count);
  }

  /**
   * Retrieve a list of Entities
   *
   * @param ids an optional list of entity IDs (cannot be used with idPatterns)
   * @param idPattern an optional pattern of entity IDs (cannot be used with ids)
   * @param types an optional list of types of entity
   * @param attrs an optional list of attributes to return for all entities
   * @param query an optional Simple Query Language query
   * @param geoQuery an optional Geo query
   * @param orderBy an option list of attributes to difine the order of entities
   * @param offset an optional offset (0 for none)
   * @param limit an optional limit (0 for none)
   * @param count true to return the total number of matching entities
   * @return a pagined list of Entities
   */
  public ListenableFuture<Paginated<Entity>> getEntities(
      Collection<String> ids,
      String idPattern,
      Collection<String> types,
      Collection<String> attrs,
      String query,
      GeoQuery geoQuery,
      Collection<String> orderBy,
      int offset,
      int limit,
      boolean count) {

    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities");
    addParam(builder, "id", ids);
    addParam(builder, "idPattern", idPattern);
    addParam(builder, "type", types);
    addParam(builder, "attrs", attrs);
    addParam(builder, "query", query);
    addGeoQueryParams(builder, geoQuery);
    addParam(builder, "orderBy", orderBy);
    addPaginationParams(builder, offset, limit);
    if (count) {
      addParam(builder, "options", "count");
    }

    return adaptPaginated(
        request(HttpMethod.GET, builder.toUriString(), null, Entity[].class), offset, limit);
  }

  /**
   * Create a new entity
   *
   * @param entity the Entity to add
   * @return the listener to notify of completion
   */
  public ListenableFuture<Void> addEntity(Entity entity) {
    return adapt(
        request(
            HttpMethod.POST,
            UriComponentsBuilder.fromHttpUrl(baseURL).path("v2/entities").toUriString(),
            entity,
            Void.class));
  }

  /**
   * Get an entity
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @param attrs the list of attributes to retreive for this entity, null or empty means all
   *     attributes
   * @return the entity
   */
  public ListenableFuture<Entity> getEntity(
      String entityId, String type, Collection<String> attrs) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}");
    addParam(builder, "type", type);
    addParam(builder, "attrs", attrs);
    return adapt(
        request(
            HttpMethod.GET, builder.buildAndExpand(entityId).toUriString(), null, Entity.class));
  }

  /**
   * Update existing or append some attributes to an entity
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @param attributes the attributes to update or to append
   * @param append if true, will only allow to append new attributes
   * @return the listener to notify of completion
   */
  public ListenableFuture<Void> updateEntity(
      String entityId, String type, Map<String, Attribute> attributes, boolean append) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}");
    addParam(builder, "type", type);
    if (append) {
      addParam(builder, "options", "append");
    }
    return adapt(
        request(
            HttpMethod.POST,
            builder.buildAndExpand(entityId).toUriString(),
            attributes,
            Void.class));
  }

  /**
   * Replace all the existing attributes of an entity with a new set of attributes
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @param attributes the new set of attributes
   * @return the listener to notify of completion
   */
  public ListenableFuture<Void> replaceEntity(
      String entityId, String type, Map<String, Attribute> attributes) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}");
    addParam(builder, "type", type);
    return adapt(
        request(
            HttpMethod.PUT,
            builder.buildAndExpand(entityId).toUriString(),
            attributes,
            Void.class));
  }

  /**
   * Delete an entity
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @return the listener to notify of completion
   */
  public ListenableFuture<Void> deleteEntity(String entityId, String type) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}");
    addParam(builder, "type", type);
    return adapt(
        request(
            HttpMethod.DELETE, builder.buildAndExpand(entityId).toUriString(), null, Void.class));
  }

  /*
   * Attributes requests
   */

  /**
   * Retrieve the attribute of an entity
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @param attributeName the attribute name
   * @return
   */
  public ListenableFuture<Attribute> getAttribute(
      String entityId, String type, String attributeName) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}/attrs/{attributeName}");
    addParam(builder, "type", type);
    return adapt(
        request(
            HttpMethod.GET,
            builder.buildAndExpand(entityId, attributeName).toUriString(),
            null,
            Attribute.class));
  }

  /**
   * Update the attribute of an entity
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @param attributeName the attribute name
   * @return
   */
  public ListenableFuture<Void> updateAttribute(
      String entityId, String type, String attributeName, Attribute attribute) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}/attrs/{attributeName}");
    addParam(builder, "type", type);
    return adapt(
        request(
            HttpMethod.PUT,
            builder.buildAndExpand(entityId, attributeName).toUriString(),
            attribute,
            Void.class));
  }

  /**
   * Delete the attribute of an entity
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @param attributeName the attribute name
   * @return
   */
  public ListenableFuture<Attribute> deleteAttribute(
      String entityId, String type, String attributeName) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}/attrs/{attributeName}");
    addParam(builder, "type", type);
    return adapt(
        request(
            HttpMethod.DELETE,
            builder.buildAndExpand(entityId, attributeName).toUriString(),
            null,
            Attribute.class));
  }

  /*
   * Attribute values requests
   */

  /**
   * Retrieve the attribute of an entity
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @param attributeName the attribute name
   * @return
   */
  public ListenableFuture<Object> getAttributeValue(
      String entityId, String type, String attributeName) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}/attrs/{attributeName}/value");
    addParam(builder, "type", type);
    return adapt(
        request(
            HttpMethod.GET,
            builder.buildAndExpand(entityId, attributeName).toUriString(),
            null,
            Object.class));
  }

  /**
   * Retrieve the attribute of an entity
   *
   * @param entityId the entity ID
   * @param type optional entity type to avoid ambiguity when multiple entities have the same ID,
   *     null or zero-length for empty
   * @param attributeName the attribute name
   * @return
   */
  public ListenableFuture<String> getAttributeValueAsString(
      String entityId, String type, String attributeName) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/entities/{entityId}/attrs/{attributeName}/value");
    addParam(builder, "type", type);
    HttpHeaders httpHeaders = cloneHttpHeaders();
    httpHeaders.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN));
    return adapt(
        request(
            HttpMethod.GET,
            builder.buildAndExpand(entityId, attributeName).toUriString(),
            httpHeaders,
            null,
            String.class));
  }

  /*
   * Entity Type requests
   */

  /**
   * Retrieve a list of entity types
   *
   * @param offset an optional offset (0 for none)
   * @param limit an optional limit (0 for none)
   * @param count true to return the total number of matching entities
   * @return a pagined list of entity types
   */
  public ListenableFuture<Paginated<EntityType>> getEntityTypes(
      int offset, int limit, boolean count) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/types");
    addPaginationParams(builder, offset, limit);
    if (count) {
      addParam(builder, "options", "count");
    }
    return adaptPaginated(
        request(HttpMethod.GET, builder.toUriString(), null, EntityType[].class), offset, limit);
  }

  /**
   * Retrieve an entity type
   *
   * @param entityType the entityType to retrieve
   * @return an entity type
   */
  public ListenableFuture<EntityType> getEntityType(String entityType) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/types/{entityType}");
    return adapt(
        request(
            HttpMethod.GET,
            builder.buildAndExpand(entityType).toUriString(),
            null,
            EntityType.class));
  }

  /*
   * Registrations requests
   */

  /**
   * Retrieve the list of all Registrations
   *
   * @return a list of registrations
   */
  public ListenableFuture<List<Registration>> getRegistrations() {

    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/registrations");

    ListenableFuture<ResponseEntity<Registration[]>> e =
        request(HttpMethod.GET, builder.toUriString(), null, Registration[].class);
    return new ListenableFutureAdapter<List<Registration>, ResponseEntity<Registration[]>>(e) {
      @Override
      protected List<Registration> adapt(ResponseEntity<Registration[]> result)
          throws ExecutionException {
        return new ArrayList<>(Arrays.asList(result.getBody()));
      }
    };
  }

  /**
   * Create a new registration
   *
   * @param registration the Registration to add
   * @return the listener to notify of completion
   */
  public ListenableFuture<Void> addRegistration(Registration registration) {
    return adapt(
        request(
            HttpMethod.POST,
            UriComponentsBuilder.fromHttpUrl(baseURL).path("v2/registrations").toUriString(),
            registration,
            Void.class));
  }

  /**
   * Retrieve the registration by registration ID
   *
   * @param registrationId the registration ID
   * @return registration
   */
  public ListenableFuture<Registration> getRegistration(String registrationId) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/registrations/{registrationId}");
    return adapt(
        request(
            HttpMethod.GET,
            builder.buildAndExpand(registrationId).toUriString(),
            null,
            Registration.class));
  }

  /**
   * Update the registration by registration ID
   *
   * @param registrationId the registration ID
   * @return
   */
  public ListenableFuture<Void> updateRegistration(
      String registrationId, Registration registration) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/registrations/{registrationId}");
    return adapt(
        request(
            HttpMethod.PATCH,
            builder.buildAndExpand(registrationId).toUriString(),
            registration,
            Void.class));
  }

  /**
   * Delete the registration by registration ID
   *
   * @param registrationId the registration ID
   * @return
   */
  public ListenableFuture<Void> deleteRegistration(String registrationId) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/registrations/{registrationId}");
    return adapt(
        request(
            HttpMethod.DELETE,
            builder.buildAndExpand(registrationId).toUriString(),
            null,
            Void.class));
  }

  /*
   * Subscriptions requests
   */

  /**
   * Retrieve the list of all Subscriptions present in the system
   *
   * @param offset an optional offset (0 for none)
   * @param limit an optional limit (0 for none)
   * @param count true to return the total number of matching entities
   * @return a pagined list of Subscriptions
   */
  public ListenableFuture<Paginated<Subscription>> getSubscriptions(
      int offset, int limit, boolean count) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/subscriptions");
    addPaginationParams(builder, offset, limit);
    if (count) {
      addParam(builder, "options", "count");
    }

    return adaptPaginated(
        request(HttpMethod.GET, builder.toUriString(), null, Subscription[].class), offset, limit);
  }

  /**
   * Create a new subscription
   *
   * @param subscription the Subscription to add
   * @return subscription Id
   */
  public ListenableFuture<String> addSubscription(Subscription subscription) {
    ListenableFuture<ResponseEntity<Void>> s =
        request(
            HttpMethod.POST,
            UriComponentsBuilder.fromHttpUrl(baseURL).path("v2/subscriptions").toUriString(),
            subscription,
            Void.class);
    return new ListenableFutureAdapter<String, ResponseEntity<Void>>(s) {
      @Override
      protected String adapt(ResponseEntity<Void> result) throws ExecutionException {
        return extractId(result);
      }
    };
  }

  /**
   * Get a Subscription by subscription ID
   *
   * @param subscriptionId the subscription ID
   * @return the subscription
   */
  public ListenableFuture<Subscription> getSubscription(String subscriptionId) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/subscriptions/{subscriptionId}");
    return adapt(
        request(
            HttpMethod.GET,
            builder.buildAndExpand(subscriptionId).toUriString(),
            null,
            Subscription.class));
  }

  /**
   * Update the subscription by subscription ID
   *
   * @param subscriptionId the subscription ID
   * @return
   */
  public ListenableFuture<Void> updateSubscription(
      String subscriptionId, Subscription subscription) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/subscriptions/{subscriptionId}");
    return adapt(
        request(
            HttpMethod.PATCH,
            builder.buildAndExpand(subscriptionId).toUriString(),
            subscription,
            Void.class));
  }

  /**
   * Delete the subscription by subscription ID
   *
   * @param subscriptionId the subscription ID
   * @return
   */
  public ListenableFuture<Void> deleteSubscription(String subscriptionId) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/subscriptions/{subscriptionId}");
    return adapt(
        request(
            HttpMethod.DELETE,
            builder.buildAndExpand(subscriptionId).toUriString(),
            null,
            Void.class));
  }

  /*
   * POJ RPC "bulk" Operations
   */

  /**
   * Update, append or delete multiple entities in a single operation
   *
   * @param bulkUpdateRequest a BulkUpdateRequest with an actionType and a list of entities to
   *     update
   * @return Nothing on success
   */
  public ListenableFuture<Void> bulkUpdate(BulkUpdateRequest bulkUpdateRequest) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/op/update");
    return adapt(request(HttpMethod.POST, builder.toUriString(), bulkUpdateRequest, Void.class));
  }

  /**
   * Query multiple entities in a single operation
   *
   * @param bulkQueryRequest defines the list of entities, attributes and scopes to match entities
   * @param orderBy an optional list of attributes to order the entities (null or empty for none)
   * @param offset an optional offset (0 for none)
   * @param limit an optional limit (0 for none)
   * @param count true to return the total number of matching entities
   * @return a paginated list of entities
   */
  public ListenableFuture<Paginated<Entity>> bulkQuery(
      BulkQueryRequest bulkQueryRequest,
      Collection<String> orderBy,
      int offset,
      int limit,
      boolean count) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/op/query");
    addPaginationParams(builder, offset, limit);
    addParam(builder, "orderBy", orderBy);
    if (count) {
      addParam(builder, "options", "count");
    }
    return adaptPaginated(
        request(HttpMethod.POST, builder.toUriString(), bulkQueryRequest, Entity[].class),
        offset,
        limit);
  }

  /**
   * Create, update or delete registrations to multiple entities in a single operation
   *
   * @param bulkRegisterRequest defines the list of entities to register
   * @return a list of registration ids
   */
  public ListenableFuture<String[]> bulkRegister(BulkRegisterRequest bulkRegisterRequest) {
    return adapt(
        request(
            HttpMethod.POST,
            UriComponentsBuilder.fromHttpUrl(baseURL).path("v2/op/register").toUriString(),
            bulkRegisterRequest,
            String[].class));
  }

  /**
   * Discover registration matching entities and their attributes
   *
   * @param bulkQueryRequest defines the list of entities, attributes and scopes to match
   *     registrations
   * @param offset an optional offset (0 for none)
   * @param limit an optional limit (0 for none)
   * @param count true to return the total number of matching entities
   * @return a paginated list of registration
   */
  public ListenableFuture<Paginated<Registration>> bulkDiscover(
      BulkQueryRequest bulkQueryRequest, int offset, int limit, boolean count) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseURL);
    builder.path("v2/op/discover");
    addPaginationParams(builder, offset, limit);
    if (count) {
      addParam(builder, "options", "count");
    }
    return adaptPaginated(
        request(HttpMethod.POST, builder.toUriString(), bulkQueryRequest, Registration[].class),
        offset,
        limit);
  }

  /**
   * Default headers
   *
   * @return the default headers
   */
  public HttpHeaders getHttpHeaders() {
    return httpHeaders;
  }

  /** Make an HTTP request with default headers */
  protected <T, U> ListenableFuture<ResponseEntity<T>> request(
      HttpMethod method, String uri, U body, Class<T> responseType) {
    return request(method, uri, getHttpHeaders(), body, responseType);
  }

  /** Make an HTTP request with custom headers */
  protected <T, U> ListenableFuture<ResponseEntity<T>> request(
      HttpMethod method, String uri, HttpHeaders httpHeaders, U body, Class<T> responseType) {
    HttpEntity<U> requestEntity = new HttpEntity<>(body, httpHeaders);
    return asyncRestTemplate.exchange(uri, method, requestEntity, responseType);
  }

  private <T> ListenableFuture<T> adapt(
      ListenableFuture<ResponseEntity<T>> responseEntityListenableFuture) {
    return new ListenableFutureAdapter<T, ResponseEntity<T>>(responseEntityListenableFuture) {
      @Override
      protected T adapt(ResponseEntity<T> result) throws ExecutionException {
        return result.getBody();
      }
    };
  }

  private <T> ListenableFuture<Paginated<T>> adaptPaginated(
      ListenableFuture<ResponseEntity<T[]>> responseEntityListenableFuture, int offset, int limit) {
    return new ListenableFutureAdapter<Paginated<T>, ResponseEntity<T[]>>(
        responseEntityListenableFuture) {
      @Override
      protected Paginated<T> adapt(ResponseEntity<T[]> result) throws ExecutionException {
        return new Paginated<>(
            Arrays.asList(result.getBody()), offset, limit, extractTotalCount(result));
      }
    };
  }

  private void addPaginationParams(UriComponentsBuilder builder, int offset, int limit) {
    if (offset > 0) {
      builder.queryParam("offset", offset);
    }
    if (limit > 0) {
      builder.queryParam("limit", limit);
    }
  }

  private void addParam(UriComponentsBuilder builder, String key, String value) {
    if (!nullOrEmpty(value)) {
      builder.queryParam(key, value);
    }
  }

  private void addParam(
      UriComponentsBuilder builder, String key, Collection<? extends CharSequence> value) {
    if (!nullOrEmpty(value)) {
      builder.queryParam(key, String.join(",", value));
    }
  }

  private void addGeoQueryParams(UriComponentsBuilder builder, GeoQuery geoQuery) {
    if (geoQuery != null) {
      StringBuilder georel = new StringBuilder(geoQuery.getRelation().name());
      if (geoQuery.getRelation() == GeoQuery.Relation.near) {
        georel.append(';').append(geoQuery.getModifier());
        georel.append(':').append(geoQuery.getDistance());
      }
      builder.queryParam("georel", georel.toString());
      builder.queryParam("geometry", geoQuery.getGeometry());
      builder.queryParam(
          "coords",
          geoQuery
              .getCoordinates()
              .stream()
              .map(Coordinate::toString)
              .collect(Collectors.joining(";")));
    }
  }

  private boolean nullOrEmpty(Collection i) {
    return i == null || i.isEmpty();
  }

  private boolean nullOrEmpty(String i) {
    return i == null || i.isEmpty();
  }

  private int extractTotalCount(ResponseEntity responseEntity) {
    String total = responseEntity.getHeaders().getFirst("X-Total-Count");
    try {
      return Integer.parseInt(total);
    } catch (NumberFormatException e) {
      return 0;
    }
  }

  private String extractId(ResponseEntity responseEntity) {
    String location = responseEntity.getHeaders().getFirst("Location");
    String paths[] = location.split("/");
    if (paths != null && paths.length > 0) {
      return paths[paths.length - 1];
    }
    return "";
  }

  /** @return return a clone HttpHeader from default HttpHeader */
  private HttpHeaders cloneHttpHeaders() {
    HttpHeaders httpHeaders = getHttpHeaders();
    HttpHeaders clone = new HttpHeaders();
    for (String entry : httpHeaders.keySet()) {
      clone.put(entry, httpHeaders.get(entry));
    }
    return clone;
  }

  /** Inject the Ngsi2ResponseErrorHandler */
  protected void injectNgsi2ErrorHandler() {
    MappingJackson2HttpMessageConverter converter = getMappingJackson2HttpMessageConverter();
    if (converter != null) {
      this.asyncRestTemplate.setErrorHandler(
          new Ngsi2ResponseErrorHandler(converter.getObjectMapper()));
    }
  }

  /** Inject an ObjectMapper supporting Java8 and JavaTime module by default */
  protected void injectJava8ObjectMapper() {
    MappingJackson2HttpMessageConverter converter = getMappingJackson2HttpMessageConverter();
    if (converter != null) {
      converter
          .getObjectMapper()
          .registerModule(new Jdk8Module())
          .registerModule(new JavaTimeModule())
          .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
  }

  private MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter() {
    for (HttpMessageConverter httpMessageConverter : asyncRestTemplate.getMessageConverters()) {
      if (httpMessageConverter instanceof MappingJackson2HttpMessageConverter) {
        return (MappingJackson2HttpMessageConverter) httpMessageConverter;
      }
    }
    return null;
  }
}
@RestController
@RequestMapping(
    value = ReConfConstants.CRUD_ROOT,
    produces = ReConfConstants.MT_APPLICATION_JSON,
    consumes = {
      ReConfConstants.MT_TEXT_PLAIN,
      ReConfConstants.MT_ALL,
      ReConfConstants.MT_APPLICATION_JSON
    })
public class UpsertUserService {

  @Autowired JdbcUserDetailsManager userDetailsManager;
  private static final List<String> mustBeRoot =
      Collections.singletonList("must be root to perform this action");
  private static final List<String> cannotChangeRootPassword =
      Collections.singletonList("cannot change root password");

  @RequestMapping(value = "/user", method = RequestMethod.PUT)
  @Transactional
  public ResponseEntity<Client> doIt(@RequestBody Client client, Authentication authentication) {

    List<String> errors = DomainValidator.checkForErrors(client);
    if (!errors.isEmpty()) {
      return new ResponseEntity<Client>(new Client(client, errors), HttpStatus.BAD_REQUEST);
    }
    HttpStatus status = null;

    List<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(new SimpleGrantedAuthority("USER"));

    if (ApplicationSecurity.isRoot(authentication)) {
      if (ApplicationSecurity.isRoot(client.getUsername())) {
        return new ResponseEntity<Client>(
            new Client(client, cannotChangeRootPassword), HttpStatus.BAD_REQUEST);
      }
      status = upsert(client, authorities);

    } else if (StringUtils.equals(client.getUsername(), authentication.getName())) {
      if (!userDetailsManager.userExists(client.getUsername())) {
        return new ResponseEntity<Client>(new Client(client, mustBeRoot), HttpStatus.BAD_REQUEST);
      }
      User user = new User(client.getUsername(), client.getPassword(), authorities);
      userDetailsManager.updateUser(user);
      status = HttpStatus.OK;

    } else {
      return new ResponseEntity<Client>(HttpStatus.FORBIDDEN);
    }

    return new ResponseEntity<Client>(new Client(client), status);
  }

  private HttpStatus upsert(Client client, List<GrantedAuthority> authorities) {
    HttpStatus status;

    User user = new User(client.getUsername(), client.getPassword(), authorities);
    if (userDetailsManager.userExists(client.getUsername())) {
      userDetailsManager.updateUser(user);
      status = HttpStatus.OK;
    } else {
      userDetailsManager.createUser(user);
      status = HttpStatus.CREATED;
    }
    return status;
  }
}