com.orange.ngsi2.server.Ngsi2BaseController.java Source code

Java tutorial

Introduction

Here is the source code for com.orange.ngsi2.server.Ngsi2BaseController.java

Source

/*
 * Copyright (C) 2016 Orange
 *
 * 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.orange.ngsi2.server;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.orange.ngsi2.exception.*;
import com.orange.ngsi2.exception.UnsupportedOperationException;
import com.orange.ngsi2.model.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Controller for the NGSI v2 requests
 */
@RequestMapping(value = { "/v2" })
public abstract class Ngsi2BaseController {

    private static Logger logger = LoggerFactory.getLogger(Ngsi2BaseController.class);

    /* Field allowed characters are the ones in the plain ASCII set except the following ones: control characters,
       whitespace, &, ?, / and #.
     */
    //private static Pattern fieldPattern = Pattern.compile("[a-zA-Z0-9_-]*");
    private static Pattern fieldPattern = Pattern
            .compile("[\\x21\\x22\\x24\\x25\\x27-\\x2E\\x30-\\x3E\\x40-\\x7E]*");

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * Endpoint get /v2
     * @return the list of supported operations under /v2 and http status 200 (ok)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/" })
    final public ResponseEntity<Map<String, String>> listResourcesEndpoint() throws Exception {
        return new ResponseEntity<>(listResources(), HttpStatus.OK);
    }

    /**
     * Endpoint get /v2/entities
     * @param id an optional list of entity IDs separated by comma (cannot be used with idPatterns)
     * @param type an optional list of types of entity separated by comma
     * @param idPattern a optional pattern of entity IDs (cannot be used with ids)
     * @param limit an optional limit (0 for none)
     * @param offset an optional offset (0 for none)
     * @param attrs an optional list of attributes separated by comma to return for all entities
     * @param query an optional Simple Query Language query
     * @param georel an optional Geo query. Possible values: near, coveredBy, intersects, equals, disjoint.
     * @param geometry an optional geometry. Possible values: point, line, polygon, box.
     * @param coords an optional coordinate
     * @param orderBy an option list of attributes to difine the order of entities
     * @param options an optional list of options separated by comma. Possible value for option: count.
     *        Theses keyValues,values and unique options are not supported.
     *        If count is present then the total number of entities is returned in the response as a HTTP header named `X-Total-Count`.
     * @return a list of Entities http status 200 (ok)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/entities" })
    final public ResponseEntity<List<Entity>> listEntitiesEndpoint(@RequestParam Optional<Set<String>> id,
            @RequestParam Optional<Set<String>> type, @RequestParam Optional<String> idPattern,
            @RequestParam Optional<Integer> limit, @RequestParam Optional<Integer> offset,
            @RequestParam Optional<List<String>> attrs, @RequestParam Optional<String> query,
            @RequestParam Optional<String> georel, @RequestParam Optional<String> geometry,
            @RequestParam Optional<String> coords, @RequestParam Optional<List<String>> orderBy,
            @RequestParam Optional<Set<String>> options) throws Exception {

        if (id.isPresent() && idPattern.isPresent()) {
            throw new IncompatibleParameterException("id", "idPattern", "List entities");
        }

        validateSyntax(id.orElse(null), type.orElse(null), attrs.orElse(null));

        Optional<GeoQuery> geoQuery = Optional.empty();
        // If one of them is present, all are mandatory
        if (georel.isPresent() || geometry.isPresent() || coords.isPresent()) {
            if (!(georel.isPresent() && geometry.isPresent() && coords.isPresent())) {
                throw new BadRequestException("Missing one argument of georel, geometry or coords");
            }
            geoQuery = Optional.of(Ngsi2ParsingHelper.parseGeoQuery(georel.get(), geometry.get(), coords.get()));
        }

        boolean count = false;
        if (options.isPresent()) {
            Set<String> optionsSet = options.get();
            //TODO: to support keyValues, values and unique as options
            if (optionsSet.contains("keyValues") || optionsSet.contains("values")
                    || optionsSet.contains("unique")) {
                throw new UnsupportedOptionException("keyValues, values or unique");
            }
            count = optionsSet.contains("count");
        }

        Paginated<Entity> paginatedEntity = listEntities(id.orElse(null), type.orElse(null), idPattern.orElse(null),
                limit.orElse(0), offset.orElse(0), attrs.orElse(new ArrayList<>()), query.orElse(null),
                geoQuery.orElse(null), orderBy.orElse(new ArrayList<>()));
        if (count) {
            return new ResponseEntity<>(paginatedEntity.getItems(), xTotalCountHeader(paginatedEntity.getTotal()),
                    HttpStatus.OK);
        } else {
            return new ResponseEntity<>(paginatedEntity.getItems(), HttpStatus.OK);
        }
    }

    /**
     * Endpoint post /v2/entities
     * @param entity
     * @param options keyValues is not supported.
     * @return http status 201 (created) and location header /v2/entities/{entityId}
     */
    @RequestMapping(method = RequestMethod.POST, value = "/entities", consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity createEntityEndpoint(@RequestBody Entity entity,
            @RequestParam Optional<String> options) {

        validateSyntax(entity);
        //TODO: to support keyValues as options
        if (options.isPresent()) {
            throw new UnsupportedOptionException(options.get());
        }
        createEntity(entity);
        return new ResponseEntity(locationHeader(entity.getId()), HttpStatus.CREATED);
    }

    /**
     * Endpoint get /v2/entities/{entityId}
     * @param entityId the entity ID
     * @param type an optional type of entity
     * @param attrs an optional list of attributes to return for the entity
     * @param options an optional list of options separated by comma.
     *        Theses keyValues,values and unique options are not supported.
     * @return the entity and http status 200 (ok) or 409 (conflict)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/entities/{entityId}" })
    final public ResponseEntity<Entity> retrieveEntityEndpoint(@PathVariable String entityId,
            @RequestParam Optional<String> type, @RequestParam Optional<List<String>> attrs,
            @RequestParam Optional<String> options) throws Exception {

        validateSyntax(entityId, type.orElse(null), attrs.orElse(null));
        //TODO: to support keyValues, values and unique as options
        if (options.isPresent()) {
            throw new UnsupportedOptionException(options.get());
        }
        return new ResponseEntity<>(retrieveEntity(entityId, type.orElse(null), attrs.orElse(new ArrayList<>())),
                HttpStatus.OK);
    }

    /**
     * Endpoint post /v2/entities/{entityId}
     * @param entityId the entity ID
     * @param attributes the attributes to update or to append
     * @param type an optional type of entity
     * @param options an optional list of options separated by comma. Possible value for option: append.
     *        keyValues options is not supported.
     *        If append is present then the operation is an append operation
     * @return http status 201 (created)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.POST, value = {
            "/entities/{entityId}" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity updateOrAppendEntityEndpoint(@PathVariable String entityId,
            @RequestBody HashMap<String, Attribute> attributes, @RequestParam Optional<String> type,
            @RequestParam Optional<Set<String>> options) throws Exception {
        validateSyntax(entityId, type.orElse(null), attributes);

        boolean append = false;
        if (options.isPresent()) {
            //TODO: to support keyValues as options
            if (options.get().contains("keyValues")) {
                throw new UnsupportedOptionException("keyValues");
            }
            append = options.get().contains("append");
        }
        updateOrAppendEntity(entityId, type.orElse(null), attributes, append);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint patch /v2/entities/{entityId}
     * @param entityId the entity ID
     * @param attributes the attributes to update
     * @param type an optional type of entity
     * @param options keyValues is not supported.
     * @return http status 204 (no content)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.PATCH, value = {
            "/entities/{entityId}" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity updateExistingEntityAttributesEndpoint(@PathVariable String entityId,
            @RequestBody HashMap<String, Attribute> attributes, @RequestParam Optional<String> type,
            @RequestParam Optional<String> options) throws Exception {

        validateSyntax(entityId, type.orElse(null), attributes);
        //TODO: to support keyValues as options
        if (options.isPresent()) {
            throw new UnsupportedOptionException(options.get());
        }
        updateExistingEntityAttributes(entityId, type.orElse(null), attributes);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint put /v2/entities/{entityId}
     * @param entityId the entity ID
     * @param attributes the new set of attributes
     * @param type an optional type of entity
     * @param options keyValues is not supported.
     * @return http status 204 (no content)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.PUT, value = {
            "/entities/{entityId}" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity replaceAllEntityAttributesEndpoint(@PathVariable String entityId,
            @RequestBody HashMap<String, Attribute> attributes, @RequestParam Optional<String> type,
            @RequestParam Optional<String> options) throws Exception {

        validateSyntax(entityId, type.orElse(null), attributes);
        //TODO: to support keyValues as options
        if (options.isPresent()) {
            throw new UnsupportedOptionException(options.get());
        }
        replaceAllEntityAttributes(entityId, type.orElse(null), attributes);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint delete /v2/entities/{entityId}
     * @param entityId the entity ID
     * @param type an optional type of entity
     * @return http status 204 (no content)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.DELETE, value = { "/entities/{entityId}" })
    final public ResponseEntity removeEntityEndpoint(@PathVariable String entityId,
            @RequestParam Optional<String> type) throws Exception {

        validateSyntax(entityId);
        type.ifPresent(this::validateSyntax);
        removeEntity(entityId);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint get /v2/entities/{entityId}/attrs/{attrName}
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type of entity
     * @return the attribute and http status 200 (ok) or 409 (conflict)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/entities/{entityId}/attrs/{attrName}" })
    final public ResponseEntity<Attribute> retrieveAttributeByEntityIdEndpoint(@PathVariable String entityId,
            @PathVariable String attrName, @RequestParam Optional<String> type) throws Exception {

        validateSyntax(entityId, type.orElse(null), attrName);
        return new ResponseEntity<>(retrieveAttributeByEntityId(entityId, attrName, type.orElse(null)),
                HttpStatus.OK);
    }

    /**
     * Endpoint put /v2/entities/{entityId}/attrs/{attrName}
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type of entity
     * @return http status 204 (no content) or 409 (conflict)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.PUT, value = {
            "/entities/{entityId}/attrs/{attrName}" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity updateAttributeByEntityIdEndpoint(@PathVariable String entityId,
            @PathVariable String attrName, @RequestParam Optional<String> type, @RequestBody Attribute attribute)
            throws Exception {

        validateSyntax(entityId, type.orElse(null), attrName);
        validateSyntax(attribute);
        updateAttributeByEntityId(entityId, attrName, type.orElse(null), attribute);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint delete /v2/entities/{entityId}/attrs/{attrName}
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type of entity
     * @return http status 204 (no content)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.DELETE, value = { "/entities/{entityId}/attrs/{attrName}" })
    final public ResponseEntity removeAttributeByEntityIdEndpoint(@PathVariable String entityId,
            @PathVariable String attrName, @RequestParam Optional<String> type) throws Exception {

        validateSyntax(entityId, type.orElse(null), attrName);
        removeAttributeByEntityId(entityId, attrName, type.orElse(null));
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint get /v2/entities/{entityId}/attrs/{attrName}/value
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type of entity
     * @return the value and http status 200 (ok) or 409 (conflict)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = {
            "/entities/{entityId}/attrs/{attrName}/value" }, produces = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity<Object> retrieveAttributeValueEndpoint(@PathVariable String entityId,
            @PathVariable String attrName, @RequestParam Optional<String> type) throws Exception {

        validateSyntax(entityId, type.orElse(null), attrName);
        Object value = retrieveAttributeValue(entityId, attrName, type.orElse(null));
        if ((value == null) || (value instanceof String) || (value instanceof Number)
                || (value instanceof Boolean)) {
            throw new NotAcceptableException();
        }
        return new ResponseEntity<>(value, HttpStatus.OK);
    }

    /**
     * Endpoint get /v2/entities/{entityId}/attrs/{attrName}/value
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type of entity
     * @return the value and http status 200 (ok) or 409 (conflict)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = {
            "/entities/{entityId}/attrs/{attrName}/value" }, produces = MediaType.TEXT_PLAIN_VALUE)
    final public ResponseEntity<String> retrievePlainTextAttributeValueEndpoint(@PathVariable String entityId,
            @PathVariable String attrName, @RequestParam Optional<String> type) throws Exception {

        validateSyntax(entityId, type.orElse(null), attrName);
        Object value = retrieveAttributeValue(entityId, attrName, type.orElse(null));
        return new ResponseEntity<>(objectMapper.writeValueAsString(value), HttpStatus.OK);
    }

    /**
     * Endpoint put /v2/entities/{entityId}/attrs/{attrName}/value
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type of entity
     * @return http status 204 (No Content) or 409 (conflict)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.PUT, value = {
            "/entities/{entityId}/attrs/{attrName}/value" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity updateAttributeValueEndpoint(@PathVariable String entityId,
            @PathVariable String attrName, @RequestParam Optional<String> type, @RequestBody Object value)
            throws Exception {

        validateSyntax(entityId, type.orElse(null), attrName);
        updateAttributeValue(entityId, attrName, type.orElse(null), value);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint put /v2/entities/{entityId}/attrs/{attrName}/value
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type of entity
     * @return http status 204 (No Content) or 409 (conflict)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.PUT, value = {
            "/entities/{entityId}/attrs/{attrName}/value" }, consumes = MediaType.TEXT_PLAIN_VALUE)
    final public ResponseEntity updatePlainTextAttributeValueEndpoint(@PathVariable String entityId,
            @PathVariable String attrName, @RequestParam Optional<String> type, @RequestBody String value)
            throws Exception {

        validateSyntax(entityId, type.orElse(null), attrName);
        updateAttributeValue(entityId, attrName, type.orElse(null), Ngsi2ParsingHelper.parseTextValue(value));
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint get /v2/types
     * @param limit an optional limit (0 for none)
     * @param offset an optional offset (0 for none)
     * @param options an optional list of options separated by comma. Possible value for option: count.
     *        values option is not supported.
     *        If count is present then the total number of entities is returned in the response as a HTTP header named `X-Total-Count`.
     * @return the entity type json object and http status 200 (ok)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/types" })
    final public ResponseEntity<List<EntityType>> retrieveEntityTypesEndpoint(@RequestParam Optional<Integer> limit,
            @RequestParam Optional<Integer> offset, @RequestParam Optional<Set<String>> options) throws Exception {

        boolean count = false;
        if (options.isPresent()) {
            //TODO: to support values as options
            if (options.get().contains("values")) {
                throw new UnsupportedOptionException("values");
            }
            count = options.get().contains("count");
        }
        Paginated<EntityType> entityTypes = retrieveEntityTypes(limit.orElse(0), offset.orElse(0), count);
        if (count) {
            return new ResponseEntity<>(entityTypes.getItems(), xTotalCountHeader(entityTypes.getTotal()),
                    HttpStatus.OK);
        }
        return new ResponseEntity<>(entityTypes.getItems(), HttpStatus.OK);
    }

    /**
     * Endpoint get /v2/types/{entityType}
     * @param entityType the type of entity
     * @return the entity type json object and http status 200 (ok)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/types/{entityType}" })
    final public ResponseEntity<EntityType> retrieveEntityTypeEndpoint(@PathVariable String entityType)
            throws Exception {

        validateSyntax(entityType);
        return new ResponseEntity<>(retrieveEntityType(entityType), HttpStatus.OK);
    }

    /**
     * Endpoint get /v2/registrations
     * @return a list of Registrations http status 200 (ok)
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/registrations" })
    final public ResponseEntity<List<Registration>> listRegistrationsEndpoint() {

        return new ResponseEntity<>(listRegistrations(), HttpStatus.OK);
    }

    /**
     * Endpoint post /v2/registrations
     * @param registration a registration to create
     * @return http status 201 (created)
     */
    @RequestMapping(method = RequestMethod.POST, value = "/registrations", consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity createRegistrationEndpoint(@RequestBody Registration registration) {

        validateSyntax(registration);
        createRegistration(registration);
        return new ResponseEntity(HttpStatus.CREATED);
    }

    /**
     * Endpoint get /v2/registrations/{registrationId}
     * @param registrationId the registration ID
     * @return the entity and http status 200 (ok)
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/registrations/{registrationId}" })
    final public ResponseEntity<Registration> retrieveRegistrationEndpoint(@PathVariable String registrationId)
            throws Exception {

        validateSyntax(registrationId);
        return new ResponseEntity<>(retrieveRegistration(registrationId), HttpStatus.OK);
    }

    /**
     * Endpoint patch /v2/registrations/{registrationId}
     * @param registrationId the registration ID
     * @return http status 204 (No Content)
     */
    @RequestMapping(method = RequestMethod.PATCH, value = { "/registrations/{registrationId}" })
    final public ResponseEntity updateRegistrationEndpoint(@PathVariable String registrationId,
            @RequestBody Registration registration) throws Exception {

        validateSyntax(registrationId);
        validateSyntax(registration);
        updateRegistration(registrationId, registration);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint delete /v2/registrations/{registrationId}
     * @param registrationId the registration ID
     * @return http status 204 (no content)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.DELETE, value = { "/registrations/{registrationId}" })
    final public ResponseEntity removeRegistrationEndpoint(@PathVariable String registrationId) throws Exception {

        validateSyntax(registrationId);
        removeRegistration(registrationId);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint get /v2/subscriptions
     * @param limit an optional limit (0 for none)
     * @param offset an optional offset (0 for none)
     * @param options an optional list of options separated by comma. Possible values for option: count.
     *        If count is present then the total number of entities is returned in the response as a HTTP header named `X-Total-Count`.
     * @return a list of Entities http status 200 (ok)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/subscriptions" })
    final public ResponseEntity<List<Subscription>> listSubscriptionsEndpoint(@RequestParam Optional<Integer> limit,
            @RequestParam Optional<Integer> offset, @RequestParam Optional<String> options) throws Exception {

        Paginated<Subscription> paginatedSubscription = listSubscriptions(limit.orElse(0), offset.orElse(0));
        List<Subscription> subscriptionList = paginatedSubscription.getItems();
        if (options.isPresent() && (options.get().contains("count"))) {
            return new ResponseEntity<>(subscriptionList, xTotalCountHeader(paginatedSubscription.getTotal()),
                    HttpStatus.OK);
        } else {
            return new ResponseEntity<>(subscriptionList, HttpStatus.OK);
        }
    }

    /**
     * Endpoint post /v2/subscriptions
     * @param subscription a subscription to create
     * @return http status 201 (created)
     */
    @RequestMapping(method = RequestMethod.POST, value = "/subscriptions", consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity createSubscriptionEndpoint(@RequestBody Subscription subscription) {

        validateSyntax(subscription);
        createSubscription(subscription);
        return new ResponseEntity(HttpStatus.CREATED);
    }

    /**
     * Endpoint get /v2/subscriptions/{subscriptionId}
     * @param subscriptionId the subscription ID
     * @return the subscription and http status 200 (ok)
     */
    @RequestMapping(method = RequestMethod.GET, value = { "/subscriptions/{subscriptionId}" })
    final public ResponseEntity<Subscription> retrieveSubscriptionEndpoint(@PathVariable String subscriptionId)
            throws Exception {

        validateSyntax(subscriptionId);
        return new ResponseEntity<>(retrieveSubscription(subscriptionId), HttpStatus.OK);
    }

    /**
     * Endpoint patch /v2/subscriptions/{subscriptionId}
     * @param subscriptionId the subscription ID
     * @return http status 204 (No Content)
     */
    @RequestMapping(method = RequestMethod.PATCH, value = { "/subscriptions/{subscriptionId}" })
    final public ResponseEntity updateSubscriptionEndpoint(@PathVariable String subscriptionId,
            @RequestBody Subscription subscription) throws Exception {

        validateSyntax(subscriptionId);
        validateSyntax(subscription);
        updateSubscription(subscriptionId, subscription);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Endpoint delete /v2/subscriptions/{subscriptionId}
     * @param subscriptionId the subscription ID
     * @return http status 204 (no content)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.DELETE, value = { "/subscriptions/{subscriptionId}" })
    final public ResponseEntity removeSubscriptionEndpoint(@PathVariable String subscriptionId) throws Exception {

        validateSyntax(subscriptionId);
        removeSubscription(subscriptionId);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /*
     * 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
     * @param options an optional list of options separated by comma. keyValues option is not supported.
     * @return http status 204 (no content)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.POST, value = {
            "/op/update" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity bulkUpdateEndpoint(@RequestBody BulkUpdateRequest bulkUpdateRequest,
            @RequestParam Optional<String> options) throws Exception {

        bulkUpdateRequest.getEntities().forEach(this::validateSyntax);
        //TODO: to support keyValues as options
        if (options.isPresent()) {
            throw new UnsupportedOptionException(options.get());
        }
        bulkUpdate(bulkUpdateRequest);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

    /**
     * Query multiple entities in a single operation
     * @param bulkQueryRequest defines the list of entities, attributes and scopes to match entities
     * @param limit an optional limit
     * @param offset an optional offset
     * @param orderBy an optional list of attributes to order the entities
     * @param options an optional list of options separated by comma. Possible value for option: count.
     *        Theses keyValues,values and unique options are not supported.
     *        If count is present then the total number of entities is returned in the response as a HTTP header named `X-Total-Count`.
     * @return a list of Entities http status 200 (ok)
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.POST, value = {
            "/op/query" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity<List<Entity>> bulkQueryEndpoint(@RequestBody BulkQueryRequest bulkQueryRequest,
            @RequestParam Optional<Integer> limit, @RequestParam Optional<Integer> offset,
            @RequestParam Optional<List<String>> orderBy, @RequestParam Optional<Set<String>> options)
            throws Exception {

        validateSyntax(bulkQueryRequest);
        boolean count = false;
        if (options.isPresent()) {
            Set<String> optionsSet = options.get();
            //TODO: to support keyValues, values and unique as options
            if (optionsSet.contains("keyValues") || optionsSet.contains("values")
                    || optionsSet.contains("unique")) {
                throw new UnsupportedOptionException("keyValues, values or unique");
            }
            count = optionsSet.contains("count");
        }
        Paginated<Entity> paginatedEntity = bulkQuery(bulkQueryRequest, limit.orElse(0), offset.orElse(0),
                orderBy.orElse(new ArrayList<>()), count);
        if (count) {
            return new ResponseEntity<>(paginatedEntity.getItems(), xTotalCountHeader(paginatedEntity.getTotal()),
                    HttpStatus.OK);
        } else {
            return new ResponseEntity<>(paginatedEntity.getItems(), HttpStatus.OK);
        }
    }

    /**
     * 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
     * @throws Exception
     */
    @RequestMapping(method = RequestMethod.POST, value = {
            "/op/register" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity<List<String>> bulkRegisterEndpoint(
            @RequestBody BulkRegisterRequest bulkRegisterRequest) throws Exception {

        bulkRegisterRequest.getRegistrations().forEach(this::validateSyntax);
        return new ResponseEntity<>(bulkRegister(bulkRegisterRequest), HttpStatus.OK);
    }

    /**
     * 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 options an optional list of options separated by comma. Possible value for option: count.
     *        If count is present then the total number of registrations is returned in the response as a HTTP header named `X-Total-Count`.
     * @return a paginated list of registration
     */
    @RequestMapping(method = RequestMethod.POST, value = {
            "/op/discover" }, consumes = MediaType.APPLICATION_JSON_VALUE)
    final public ResponseEntity<List<Registration>> bulkDiscoverEndpoint(
            @RequestBody BulkQueryRequest bulkQueryRequest, @RequestParam Optional<Integer> limit,
            @RequestParam Optional<Integer> offset, @RequestParam Optional<Set<String>> options) {

        validateSyntax(bulkQueryRequest);
        boolean count = false;
        if (options.isPresent()) {
            Set<String> optionsSet = options.get();
            count = optionsSet.contains("count");
        }
        Paginated<Registration> paginatedRegistration = bulkDiscover(bulkQueryRequest, limit.orElse(0),
                offset.orElse(0), count);
        if (count) {
            return new ResponseEntity<>(paginatedRegistration.getItems(),
                    xTotalCountHeader(paginatedRegistration.getTotal()), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(paginatedRegistration.getItems(), HttpStatus.OK);
        }
    }

    /*
     * Exception handling
     */

    @ExceptionHandler({ UnsupportedOperationException.class })
    public ResponseEntity<Object> unsupportedOperation(UnsupportedOperationException exception,
            HttpServletRequest request) {
        logger.error("Unsupported operation: {}", exception.getMessage());
        HttpStatus httpStatus = HttpStatus.NOT_IMPLEMENTED;
        if (request.getHeader("Accept").contains(MediaType.TEXT_PLAIN_VALUE)) {
            return new ResponseEntity<>(exception.getError().toString(), httpStatus);
        }
        return new ResponseEntity<>(exception.getError(), httpStatus);
    }

    @ExceptionHandler({ UnsupportedOptionException.class })
    public ResponseEntity<Object> unsupportedOption(UnsupportedOptionException exception,
            HttpServletRequest request) {
        logger.error("Unsupported option: {}", exception.getMessage());
        HttpStatus httpStatus = HttpStatus.NOT_IMPLEMENTED;
        if (request.getHeader("Accept").contains(MediaType.TEXT_PLAIN_VALUE)) {
            return new ResponseEntity<>(exception.getError().toString(), httpStatus);
        }
        return new ResponseEntity<>(exception.getError(), httpStatus);
    }

    @ExceptionHandler({ BadRequestException.class })
    public ResponseEntity<Object> incompatibleParameter(BadRequestException exception, HttpServletRequest request) {
        logger.error("Bad request: {}", exception.getMessage());
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        if (request.getHeader("Accept").contains(MediaType.TEXT_PLAIN_VALUE)) {
            return new ResponseEntity<>(exception.getError().toString(), httpStatus);
        }
        return new ResponseEntity<>(exception.getError(), httpStatus);
    }

    @ExceptionHandler({ IncompatibleParameterException.class })
    public ResponseEntity<Object> incompatibleParameter(IncompatibleParameterException exception,
            HttpServletRequest request) {
        logger.error("Incompatible parameter: {}", exception.getMessage());
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        if (request.getHeader("Accept").contains(MediaType.TEXT_PLAIN_VALUE)) {
            return new ResponseEntity<>(exception.getError().toString(), httpStatus);
        }
        return new ResponseEntity<>(exception.getError(), httpStatus);
    }

    @ExceptionHandler({ InvalidatedSyntaxException.class })
    public ResponseEntity<Object> invalidSyntax(InvalidatedSyntaxException exception, HttpServletRequest request) {
        logger.error("Invalid syntax: {}", exception.getMessage());
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        if (request.getHeader("Accept").contains(MediaType.TEXT_PLAIN_VALUE)) {
            return new ResponseEntity<>(exception.getError().toString(), httpStatus);
        }
        return new ResponseEntity<>(exception.getError(), httpStatus);
    }

    @ExceptionHandler({ ConflictingEntitiesException.class })
    public ResponseEntity<Object> conflictingEntities(ConflictingEntitiesException exception,
            HttpServletRequest request) {
        logger.error("ConflictingEntities: {}", exception.getMessage());
        HttpStatus httpStatus = HttpStatus.CONFLICT;
        if (request.getHeader("Accept").contains(MediaType.TEXT_PLAIN_VALUE)) {
            return new ResponseEntity<>(exception.getError().toString(), httpStatus);
        }
        return new ResponseEntity<>(exception.getError(), httpStatus);
    }

    @ExceptionHandler({ NotAcceptableException.class })
    public ResponseEntity<Object> notAcceptable(NotAcceptableException exception, HttpServletRequest request) {
        logger.error("Not Acceptable: {}", exception.getMessage());
        HttpStatus httpStatus = HttpStatus.NOT_ACCEPTABLE;
        if (request.getHeader("Accept").contains(MediaType.TEXT_PLAIN_VALUE)) {
            return new ResponseEntity<>(exception.getError().toString(), httpStatus);
        }
        return new ResponseEntity<>(exception.getError(), httpStatus);
    }

    @ExceptionHandler({ IllegalArgumentException.class })
    public ResponseEntity<Object> illegalArgument(IllegalArgumentException exception, HttpServletRequest request) {
        logger.error("Illegal Argument: {}", exception.getMessage());
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        if (request.getHeader("Accept").contains(MediaType.TEXT_PLAIN_VALUE)) {
            return new ResponseEntity<>(exception.getMessage(), httpStatus);
        }
        return new ResponseEntity<>(exception.getMessage(), httpStatus);
    }

    /*
     * Methods overridden by child classes to handle the NGSI v2 requests
     */

    /**
     * Retrieve a list of Entities which match different criteria
     * @param ids an optional list of entity IDs (cannot be used with idPatterns) (null for none)
     * @param types an optional list of types of entity (null for none)
     * @param idPattern a optional pattern of entity IDs (cannot be used with ids) (null for none)
     * @param limit an optional limit (0 for none)
     * @param offset an optional offset (0 for none)
     * @param attrs an optional list of attributes to return for all entities (null or empty for none)
     * @param query an optional Simple Query Language query (null for none)
     * @param geoQuery an optional Geo query (null for none)
     * @param orderBy an option list of attributes to define the order of entities (null or empty for none)
     * @return a paginated of list of Entities
     * @throws Exception
     */
    protected Paginated<Entity> listEntities(Set<String> ids, Set<String> types, String idPattern, int limit,
            int offset, List<String> attrs, String query, GeoQuery geoQuery, List<String> orderBy)
            throws Exception {
        throw new UnsupportedOperationException("List Entities");
    }

    /**
     * Retrieve the list of supported operations under /v2
     * @return the list of supported operations under /v2
     * @throws Exception
     */
    protected Map<String, String> listResources() throws Exception {
        throw new UnsupportedOperationException("Retrieve API Resources");
    }

    /**
     * Create a new entity
     * @param entity the entity to create
     */
    protected void createEntity(Entity entity) {
        throw new UnsupportedOperationException("Create Entity");
    }

    /**
     * Retrieve an Entity by the entity ID
     * @param entityId the entity ID
     * @param type an optional type of entity (null for none)
     * @param attrs an optional list of attributes to return for the entity (null or empty for none)
     * @return the Entity
     * @throws ConflictingEntitiesException
     */
    protected Entity retrieveEntity(String entityId, String type, List<String> attrs)
            throws ConflictingEntitiesException {
        throw new UnsupportedOperationException("Retrieve Entity");
    }

    /**
     * Update existing or append some attributes to an entity
     * @param entityId the entity ID
     * @param type an optional type of entity (null for none)
     * @param attributes the attributes to update or to append
     * @param append boolean true if the operation is an append operation
     */
    protected void updateOrAppendEntity(String entityId, String type, Map<String, Attribute> attributes,
            Boolean append) {
        throw new UnsupportedOperationException("Update Or Append Entity");
    }

    /**
     * Update existing attributes to an entity. The entity attributes are updated with the ones in the attributes.
     * If one or more attributes in the payload doesn't exist in the entity, an error if returned
     * @param entityId the entity ID
     * @param type an optional type of entity (null for none)
     * @param attributes the attributes to update
     */
    protected void updateExistingEntityAttributes(String entityId, String type, Map<String, Attribute> attributes) {
        throw new UnsupportedOperationException("Update Existing Entity Attributes");
    }

    /**
     * Replace all the existing attributes of an entity with a new set of attributes
     * @param entityId the entity ID
     * @param type an optional type of entity (null for none)
     * @param attributes the new set of attributes
     */
    protected void replaceAllEntityAttributes(String entityId, String type, Map<String, Attribute> attributes) {
        throw new UnsupportedOperationException("Replace All Entity Attributes");
    }

    /**
     * Delete an entity
     * @param entityId the entity ID
     */
    protected void removeEntity(String entityId) {
        throw new UnsupportedOperationException("Remove Entity");
    }

    /**
     * Retrieve a list of entity types
     * @param limit an optional limit (0 for none)
     * @param offset an optional offset (0 for none)
     * @param count whether or not to count the total number of entity types
     * @return the list of entity types
     */
    protected Paginated<EntityType> retrieveEntityTypes(int limit, int offset, boolean count) {
        throw new UnsupportedOperationException("Retrieve Entity Types");
    }

    /**
     * Retrieve an Entity Type by the type with the union set of attribute name and attribute type and with the count
     * of entities belonging to that type
     * @param entityType the type of entity
     * @return the EntityType
     */
    protected EntityType retrieveEntityType(String entityType) {
        throw new UnsupportedOperationException("Retrieve Entity Type");
    }

    /**
     * Retrieve an Attribute by the entity ID
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type to avoid ambiguity in the case there are several entities with the same entity id
     *             null for none
     * @return the Attribute
     * @throws ConflictingEntitiesException
     */
    protected Attribute retrieveAttributeByEntityId(String entityId, String attrName, String type)
            throws ConflictingEntitiesException {
        throw new UnsupportedOperationException("Retrieve Attribute by Entity ID");
    }

    /**
     * Update an Attribute by the entity ID
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type to avoid ambiguity in the case there are several entities with the same entity id
     *             null for none
     * @param attribute the new attributes data
     * @throws ConflictingEntitiesException
     */
    protected void updateAttributeByEntityId(String entityId, String attrName, String type, Attribute attribute)
            throws ConflictingEntitiesException {
        throw new UnsupportedOperationException("Update Attribute by Entity ID");
    }

    /**
     * Delete an attribute
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type to avoid ambiguity in the case there are several entities with the same entity id
     *             null for none
     * @throws ConflictingEntitiesException
     */
    protected void removeAttributeByEntityId(String entityId, String attrName, String type)
            throws ConflictingEntitiesException {
        throw new UnsupportedOperationException("Remove Attribute");
    }

    /**
     * Delete an attribute
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type to avoid ambiguity in the case there are several entities with the same entity id
     *             null for none
     * @return value
     */
    protected Object retrieveAttributeValue(String entityId, String attrName, String type) {
        throw new UnsupportedOperationException("Retrieve Attribute Value");
    }

    /**
     * Update an Attribute Value
     * @param entityId the entity ID
     * @param attrName the attribute name
     * @param type an optional type to avoid ambiguity in the case there are several entities with the same entity id.
     *             null for none
     * @param value the new value
     * @throws ConflictingEntitiesException
     */
    protected void updateAttributeValue(String entityId, String attrName, String type, Object value)
            throws ConflictingEntitiesException {
        throw new UnsupportedOperationException("Update Attribute Value");
    }

    /**
     * Retrieve the list of all Registrations presents in the system
     * @return list of Registrations
     */
    protected List<Registration> listRegistrations() {
        throw new UnsupportedOperationException("Retrieve Registrations");
    }

    /**
     * Create a new registration
     * @param registration the registration to create
     */
    protected void createRegistration(Registration registration) {
        throw new UnsupportedOperationException("Create Registration");
    }

    /**
     * Retrieve a Registration by the registration ID
     * @param registrationId the registration ID
     * @return the registration
     */
    protected Registration retrieveRegistration(String registrationId) {
        throw new UnsupportedOperationException("Retrieve Registration");
    }

    /**
     * Update some fields to a registration
     * @param registrationId the registration ID
     * @param registration the some fields of the registration to update
     */
    protected void updateRegistration(String registrationId, Registration registration) {
        throw new UnsupportedOperationException("Update Registration");
    }

    /**
     * Delete a registration
     * @param registrationId the registration ID
     */
    protected void removeRegistration(String registrationId) {
        throw new UnsupportedOperationException("Remove Registration");
    }

    /**
     * Retrieve the list of all Subscriptions present in the system
     * @param limit an optional limit (0 for none)
     * @param offset an optional offset (0 for none)
     * @return a paginated of list of Subscriptions
     * @throws Exception
     */
    protected Paginated<Subscription> listSubscriptions(int limit, int offset) throws Exception {
        throw new UnsupportedOperationException("List Subscriptions");
    }

    /**
     * Create a new subscription
     * @param subscription the subscription to create
     */
    protected void createSubscription(Subscription subscription) {
        throw new UnsupportedOperationException("Create Subscription");
    }

    /**
     * Retrieve a subscription by the subscription ID
     * @param subscriptionId the registration ID
     * @return the registration
     */
    protected Subscription retrieveSubscription(String subscriptionId) {
        throw new UnsupportedOperationException("Retrieve Subscription");
    }

    /**
     * Update some fields to a subscription
     * @param subscriptionId the subscription ID
     * @param subscription the some fields of the subscription to update
     */
    protected void updateSubscription(String subscriptionId, Subscription subscription) {
        throw new UnsupportedOperationException("Update Subscription");
    }

    /**
     * Delete a subscription
     * @param subscriptionId the subscription ID
     */
    protected void removeSubscription(String subscriptionId) {
        throw new UnsupportedOperationException("Remove Subscription");
    }

    /**
     * Update, append or delete multiple entities in a single operation
     * @param bulkUpdateRequest a BulkUpdateRequest with an actionType and a list of entities to update
     */
    protected void bulkUpdate(BulkUpdateRequest bulkUpdateRequest) {
        throw new UnsupportedOperationException("Update");
    }

    /**
     * Query multiple entities in a single operation
     * @param bulkQueryRequest an optional list of entity IDs (cannot be used with idPatterns)
     * @param limit an optional limit (0 for none)
     * @param offset an optional offset (0 for none)
     * @param orderBy an option list of attributes to define the order of entities (empty for none)
     * @param count is true if the count is required
     * @return a paginated of list of Entities
     */
    protected Paginated<Entity> bulkQuery(BulkQueryRequest bulkQueryRequest, int limit, int offset,
            List<String> orderBy, Boolean count) {
        throw new UnsupportedOperationException("Query");
    }

    /**
     * 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
     */
    protected List<String> bulkRegister(BulkRegisterRequest bulkRegisterRequest) {
        throw new UnsupportedOperationException("Register");
    }

    /**
     * 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 is true if the count is required
     * @return a paginated list of registration
     */
    protected Paginated<Registration> bulkDiscover(BulkQueryRequest bulkQueryRequest, int limit, int offset,
            Boolean count) {
        throw new UnsupportedOperationException("Discover");
    }

    /*
     * Private Methods 
     */

    private void validateSyntax(String field) throws InvalidatedSyntaxException {
        if ((field.length() > 256) || (!fieldPattern.matcher(field).matches())) {
            throw new InvalidatedSyntaxException(field);
        }
    }

    private void validateSyntax(Collection<String> strings) {
        if (strings != null) {
            strings.forEach(this::validateSyntax);
        }
    }

    private void validateSyntax(Set<String> ids, Set<String> types, List<String> attrs) {
        validateSyntax(ids);
        validateSyntax(types);
        validateSyntax(attrs);
    }

    private void validateSyntax(String id, String type, List<String> attrs) {
        if (id != null)
            validateSyntax(id);
        if (type != null)
            validateSyntax(type);
        validateSyntax(attrs);
    }

    private void validateSyntax(String id, String type, String attributeName) {
        if (id != null)
            validateSyntax(id);
        if (type != null)
            validateSyntax(type);
        if (attributeName != null)
            validateSyntax(attributeName);
    }

    private void validateSyntax(Entity entity) {
        if (entity.getId() != null) {
            validateSyntax(entity.getId());
        }
        if (entity.getType() != null) {
            validateSyntax(entity.getType());
        }
        if (entity.getAttributes() != null) {
            validateSyntax(entity.getAttributes());
        }
    }

    private void validateSyntax(Attribute attribute) {
        //check attribute type
        if (attribute.getType() != null) {
            attribute.getType().ifPresent(this::validateSyntax);
        }
        Map<String, Metadata> metadatas = attribute.getMetadata();
        if (metadatas != null) {
            //check metadata name
            metadatas.keySet().forEach(this::validateSyntax);
            //check metadata type
            metadatas.values().forEach(metadata -> {
                if (metadata.getType() != null) {
                    validateSyntax(metadata.getType());
                }
            });
        }
    }

    private void validateSyntax(Map<String, Attribute> attributes) {
        //check attribute name
        attributes.keySet().forEach(this::validateSyntax);
        attributes.values().forEach(this::validateSyntax);
    }

    private void validateSyntax(String entityId, String type, Map<String, Attribute> attributes) {
        if (entityId != null) {
            validateSyntax(entityId);
        }
        if (type != null) {
            validateSyntax(type);
        }
        if (attributes != null) {
            validateSyntax(attributes);
        }
    }

    private void validateSyntax(List<SubjectEntity> subjectEntities) {
        subjectEntities.forEach(subjectEntity -> {
            if (subjectEntity.getId() != null) {
                subjectEntity.getId().ifPresent(this::validateSyntax);
            }
            if (subjectEntity.getType() != null) {
                subjectEntity.getType().ifPresent(this::validateSyntax);
            }
        });
    }

    private void validateSyntax(Registration registration) {
        if (registration.getSubject() != null) {
            if (registration.getSubject().getEntities() != null) {
                validateSyntax(registration.getSubject().getEntities());
            }
            if (registration.getSubject().getAttributes() != null) {
                registration.getSubject().getAttributes().forEach(this::validateSyntax);
            }
        }
        Map<String, Metadata> metadatas = registration.getMetadata();
        if (metadatas != null) {
            //check metadata name
            metadatas.keySet().forEach(this::validateSyntax);
            //check metadata type
            metadatas.values().forEach(metadata -> {
                if (metadata.getType() != null) {
                    validateSyntax(metadata.getType());
                }
            });
        }
    }

    private void validateSyntax(Subscription subscription) {
        if (subscription.getSubject() != null) {
            if (subscription.getSubject().getEntities() != null) {
                validateSyntax(subscription.getSubject().getEntities());
            }
            if ((subscription.getSubject().getCondition() != null)
                    && (subscription.getSubject().getCondition().getAttributes() != null)) {
                subscription.getSubject().getCondition().getAttributes().forEach(this::validateSyntax);
            }
        }
        if ((subscription.getNotification() != null) && (subscription.getNotification().getAttributes() != null)) {
            subscription.getNotification().getAttributes().forEach(this::validateSyntax);
        }
    }

    private void validateSyntax(BulkQueryRequest bulkQueryRequest) {
        validateSyntax(bulkQueryRequest.getEntities());
        validateSyntax(bulkQueryRequest.getAttributes());
        bulkQueryRequest.getScopes().forEach(scope -> {
            if (scope.getType() != null) {
                validateSyntax(scope.getType());
            }
        });
    }

    private HttpHeaders locationHeader(String entityId) {
        HttpHeaders headers = new HttpHeaders();
        headers.put("Location", Collections.singletonList("/v2/entities/" + entityId));
        return headers;
    }

    private HttpHeaders xTotalCountHeader(int countNumber) {
        HttpHeaders headers = new HttpHeaders();
        headers.put("X-Total-Count", Collections.singletonList(Integer.toString(countNumber)));
        return headers;
    }
}