/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 org.apache.atlas;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.sun.jersey.api.client.GenericType;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import com.sun.jersey.multipart.BodyPart;
import com.sun.jersey.multipart.FormDataBodyPart;
import com.sun.jersey.multipart.FormDataMultiPart;
import com.sun.jersey.multipart.MultiPart;
import com.sun.jersey.multipart.file.StreamDataBodyPart;
import org.apache.atlas.bulkimport.BulkImportResponse;
import org.apache.atlas.model.PList;
import org.apache.atlas.model.SearchFilter;
import org.apache.atlas.model.audit.AtlasAuditEntry;
import org.apache.atlas.model.audit.AuditReductionCriteria;
import org.apache.atlas.model.audit.AuditSearchParameters;
import org.apache.atlas.model.audit.EntityAuditEventV2;
import org.apache.atlas.model.discovery.AtlasQuickSearchResult;
import org.apache.atlas.model.discovery.AtlasSearchResult;
import org.apache.atlas.model.discovery.AtlasSuggestionsResult;
import org.apache.atlas.model.discovery.QuickSearchParameters;
import org.apache.atlas.model.discovery.SearchParameters;
import org.apache.atlas.model.glossary.AtlasGlossary;
import org.apache.atlas.model.glossary.AtlasGlossaryCategory;
import org.apache.atlas.model.glossary.AtlasGlossaryTerm;
import org.apache.atlas.model.glossary.relations.AtlasRelatedCategoryHeader;
import org.apache.atlas.model.glossary.relations.AtlasRelatedTermHeader;
import org.apache.atlas.model.impexp.AsyncImportStatus;
import org.apache.atlas.model.impexp.AtlasAsyncImportRequest;
import org.apache.atlas.model.impexp.AtlasImportRequest;
import org.apache.atlas.model.instance.AtlasClassification;
import org.apache.atlas.model.instance.AtlasClassification.AtlasClassifications;
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntitiesWithExtInfo;
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntityWithExtInfo;
import org.apache.atlas.model.instance.AtlasEntityHeader;
import org.apache.atlas.model.instance.AtlasEntityHeaders;
import org.apache.atlas.model.instance.AtlasRelatedObjectId;
import org.apache.atlas.model.instance.AtlasRelationship;
import org.apache.atlas.model.instance.AtlasRelationship.AtlasRelationshipWithExtInfo;
import org.apache.atlas.model.instance.ClassificationAssociateRequest;
import org.apache.atlas.model.instance.EntityMutationResponse;
import org.apache.atlas.model.lineage.AtlasLineageInfo;
import org.apache.atlas.model.lineage.AtlasLineageInfo.LineageDirection;
import org.apache.atlas.model.lineage.LineageOnDemandConstraints;
import org.apache.atlas.model.profile.AtlasUserSavedSearch;
import org.apache.atlas.model.typedef.AtlasBusinessMetadataDef;
import org.apache.atlas.model.typedef.AtlasClassificationDef;
import org.apache.atlas.model.typedef.AtlasEntityDef;
import org.apache.atlas.model.typedef.AtlasEnumDef;
import org.apache.atlas.model.typedef.AtlasRelationshipDef;
import org.apache.atlas.model.typedef.AtlasStructDef;
import org.apache.atlas.model.typedef.AtlasTypeDefHeader;
import org.apache.atlas.model.typedef.AtlasTypesDef;
import org.apache.atlas.type.AtlasType;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.security.UserGroupInformation;

import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class AtlasClientV2 extends AtlasBaseClient {
    // Type APIs
    public static final  String TYPES_API            = BASE_URI + "v2/types/";
    // Entity APIs
    public static final  String ENTITY_API           = BASE_URI + "v2/entity/";
    private static final String PREFIX_ATTR          = "attr:";
    private static final String PREFIX_ATTR_         = "attr_";
    private static final String TYPEDEFS_API         = TYPES_API + "typedefs/";
    private static final String TYPEDEF_BY_NAME      = TYPES_API + "typedef/name/";
    private static final String TYPEDEF_BY_GUID      = TYPES_API + "typedef/guid/";
    private static final String GET_BY_NAME_TEMPLATE = TYPES_API + "%s/name/%s";
    private static final String GET_BY_GUID_TEMPLATE = TYPES_API + "%s/guid/%s";
    private static final String ENTITY_BULK_API      = ENTITY_API + "bulk/";

    //Admin Entity
    private static final String ADMIN_API        = BASE_URI + "admin/";
    private static final String ENTITY_PURGE_API = ADMIN_API + "purge/";
    private static final String ATLAS_AUDIT_API  = ADMIN_API + "audits/";

    // Lineage APIs
    private static final String LINEAGE_URI = BASE_URI + "v2/lineage/";

    // Discovery APIs
    private static final String DISCOVERY_URI        = BASE_URI + "v2/search";
    private static final String DSL_SEARCH_URI       = DISCOVERY_URI + "/dsl";
    private static final String FULL_TEXT_SEARCH_URI = DISCOVERY_URI + "/fulltext";
    private static final String BASIC_SEARCH_URI     = DISCOVERY_URI + "/basic";
    private static final String FACETED_SEARCH_URI   = BASIC_SEARCH_URI;
    private static final String SAVED_SEARCH_URI     = DISCOVERY_URI + "/saved";
    private static final String QUICK_SEARCH_URI     = DISCOVERY_URI + "/quick";

    // Relationships APIs
    private static final String RELATIONSHIPS_URI        = BASE_URI + "v2/relationship/";
    private static final String BULK_HEADERS             = "bulk/headers";
    private static final String BULK_SET_CLASSIFICATIONS = "bulk/setClassifications";
    private static final String RELATIONSHIP_URI         = DISCOVERY_URI + "/relationship";

    //Glossary APIs
    private static final String GLOSSARY_URI        = BASE_URI + "v2/glossary";
    private static final String GLOSSARY_TERM       = GLOSSARY_URI + "/term";
    private static final String GLOSSARY_TERMS      = GLOSSARY_URI + "/terms";
    private static final String GLOSSARY_CATEGORY   = GLOSSARY_URI + "/category";
    private static final String GLOSSARY_CATEGORIES = GLOSSARY_URI + "/categories";

    //Notification APIs
    private static final String NOTIFICATION_URI = BASE_URI + "v2/notification";

    //IndexRecovery APIs
    private static final String INDEX_RECOVERY_URI = BASE_URI + "v2/indexrecovery";

    // Async Import APIs
    private static final String ASYNC_IMPORT_URI              = BASE_URI + "admin/async/import";
    private static final String ASYNC_IMPORT_STATUS_URI       = BASE_URI + "admin/async/import/status";

    private static final String IMPORT_REQUEST_PARAMTER = "request";
    private static final String IMPORT_DATA_PARAMETER   = "data";

    public AtlasClientV2(String[] baseUrl, String[] basicAuthUserNamePassword) {
        super(baseUrl, basicAuthUserNamePassword);
    }

    public AtlasClientV2(String... baseUrls) throws AtlasException {
        super(baseUrls);
    }

    public AtlasClientV2(UserGroupInformation ugi, String doAsUser, String... baseUrls) {
        super(ugi, doAsUser, baseUrls);
    }

    /**
     * Constructor for AtlasClient with cookie params as header
     * @param baseUrl
     * @param cookieName
     * @param value
     * @param path
     * @param domain
     */
    public AtlasClientV2(String[] baseUrl, String cookieName, String value, String path, String domain) {
        super(baseUrl, new Cookie(cookieName, value, path, domain));
    }

    /**
     * Constructor for AtlasClient with cookie as header
     * @param baseUrl
     * @param cookie
     */
    public AtlasClientV2(String[] baseUrl, Cookie cookie) {
        super(baseUrl, cookie);
    }

    public AtlasClientV2(Configuration configuration, String[] baseUrl, String[] basicAuthUserNamePassword) {
        super(configuration, baseUrl, basicAuthUserNamePassword);
    }

    @VisibleForTesting
    public AtlasClientV2(WebResource service, Configuration configuration) {
        super(service, configuration);
    }

    /**
     * Bulk retrieval API for retrieving all type definitions in Atlas
     *
     * @return A composite wrapper object with lists of all type definitions
     */
    public AtlasTypesDef getAllTypeDefs(SearchFilter searchFilter) throws AtlasServiceException {
        return callAPI(API_V2.GET_ALL_TYPE_DEFS, AtlasTypesDef.class, searchFilter.getParams());
    }

    public List<AtlasTypeDefHeader> getAllTypeDefHeaders(SearchFilter searchFilter) throws AtlasServiceException {
        return callAPI(API_V2.GET_ALL_TYPE_DEF_HEADERS, List.class, searchFilter.getParams());
    }

    public boolean typeWithGuidExists(String guid) {
        try {
            callAPI(API_V2.GET_TYPEDEF_BY_GUID, String.class, null, guid);
        } catch (AtlasServiceException e) {
            return false;
        }

        return true;
    }

    public boolean typeWithNameExists(String name) {
        try {
            callAPI(API_V2.GET_TYPEDEF_BY_NAME, String.class, null, name);
        } catch (AtlasServiceException e) {
            return false;
        }

        return true;
    }

    public AtlasEnumDef getEnumDefByName(String name) throws AtlasServiceException {
        return getTypeDefByName(name, AtlasEnumDef.class);
    }

    public AtlasEnumDef getEnumDefByGuid(String guid) throws AtlasServiceException {
        return getTypeDefByGuid(guid, AtlasEnumDef.class);
    }

    public AtlasStructDef getStructDefByName(String name) throws AtlasServiceException {
        return getTypeDefByName(name, AtlasStructDef.class);
    }

    public AtlasStructDef getStructDefByGuid(String guid) throws AtlasServiceException {
        return getTypeDefByGuid(guid, AtlasStructDef.class);
    }

    public AtlasClassificationDef getClassificationDefByName(String name) throws AtlasServiceException {
        return getTypeDefByName(name, AtlasClassificationDef.class);
    }

    public AtlasClassificationDef getClassificationDefByGuid(String guid) throws AtlasServiceException {
        return getTypeDefByGuid(guid, AtlasClassificationDef.class);
    }

    public AtlasEntityDef getEntityDefByName(String name) throws AtlasServiceException {
        return getTypeDefByName(name, AtlasEntityDef.class);
    }

    public AtlasEntityDef getEntityDefByGuid(String guid) throws AtlasServiceException {
        return getTypeDefByGuid(guid, AtlasEntityDef.class);
    }

    public AtlasRelationshipDef getRelationshipDefByName(String name) throws AtlasServiceException {
        return getTypeDefByName(name, AtlasRelationshipDef.class);
    }

    public AtlasRelationshipDef getRelationshipDefByGuid(String guid) throws AtlasServiceException {
        return getTypeDefByGuid(guid, AtlasRelationshipDef.class);
    }

    public AtlasBusinessMetadataDef getBusinessMetadataDefByName(String name) throws AtlasServiceException {
        return getTypeDefByName(name, AtlasBusinessMetadataDef.class);
    }

    public AtlasBusinessMetadataDef getBusinessMetadataDefGuid(String guid) throws AtlasServiceException {
        return getTypeDefByGuid(guid, AtlasBusinessMetadataDef.class);
    }

    @Deprecated
    public AtlasEnumDef createEnumDef(AtlasEnumDef enumDef) throws AtlasServiceException {
        AtlasTypesDef typesDef = new AtlasTypesDef();

        typesDef.getEnumDefs().add(enumDef);

        AtlasTypesDef created = createAtlasTypeDefs(typesDef);

        return created.getEnumDefs().get(0);
    }

    @Deprecated
    public AtlasStructDef createStructDef(AtlasStructDef structDef) throws AtlasServiceException {
        AtlasTypesDef typesDef = new AtlasTypesDef();

        typesDef.getStructDefs().add(structDef);

        AtlasTypesDef created = createAtlasTypeDefs(typesDef);

        return created.getStructDefs().get(0);
    }

    @Deprecated
    public AtlasEntityDef createEntityDef(AtlasEntityDef entityDef) throws AtlasServiceException {
        AtlasTypesDef typesDef = new AtlasTypesDef();

        typesDef.getEntityDefs().add(entityDef);

        AtlasTypesDef created = createAtlasTypeDefs(typesDef);

        return created.getEntityDefs().get(0);
    }

    @Deprecated
    public AtlasClassificationDef createClassificationDef(AtlasClassificationDef classificationDef) throws AtlasServiceException {
        AtlasTypesDef typesDef = new AtlasTypesDef();

        typesDef.getClassificationDefs().add(classificationDef);

        AtlasTypesDef created = createAtlasTypeDefs(typesDef);

        return created.getClassificationDefs().get(0);
    }

    /**
     * Bulk create APIs for all atlas type definitions, only new definitions will be created.
     * Any changes to the existing definitions will be discarded
     *
     * @param typesDef A composite wrapper object with corresponding lists of the type definition
     * @return A composite wrapper object with lists of type definitions that were successfully
     * created
     */
    public AtlasTypesDef createAtlasTypeDefs(AtlasTypesDef typesDef) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_TYPE_DEFS, AtlasTypesDef.class, AtlasType.toJson(typesDef));
    }

    /**
     * Bulk update API for all types, changes detected in the type definitions would be persisted
     *
     * @param typesDef A composite object that captures all type definition changes
     * @return A composite object with lists of type definitions that were updated
     */
    public AtlasTypesDef updateAtlasTypeDefs(AtlasTypesDef typesDef) throws AtlasServiceException {
        return callAPI(API_V2.UPDATE_TYPE_DEFS, AtlasTypesDef.class, AtlasType.toJson(typesDef));
    }

    /**
     * Bulk delete API for all types
     *
     * @param typesDef A composite object that captures all types to be deleted
     */
    public void deleteAtlasTypeDefs(AtlasTypesDef typesDef) throws AtlasServiceException {
        callAPI(API_V2.DELETE_TYPE_DEFS, (Class<?>) null, AtlasType.toJson(typesDef));
    }

    public void deleteTypeByName(String typeName) throws AtlasServiceException {
        callAPI(API_V2.DELETE_TYPE_DEF_BY_NAME, (Class) null, null, typeName);
    }

    // Entity APIs
    public AtlasEntityWithExtInfo getEntityByGuid(String guid) throws AtlasServiceException {
        return getEntityByGuid(guid, false, false);
    }

    public AtlasEntityWithExtInfo getEntityByGuid(String guid, boolean minExtInfo, boolean ignoreRelationships) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("minExtInfo", String.valueOf(minExtInfo));
        queryParams.add("ignoreRelationships", String.valueOf(ignoreRelationships));

        return callAPI(API_V2.GET_ENTITY_BY_GUID, AtlasEntityWithExtInfo.class, queryParams, guid);
    }

    public AtlasEntityWithExtInfo getEntityByAttribute(String typeName, Map<String, String> uniqAttributes) throws AtlasServiceException {
        return getEntityByAttribute(typeName, uniqAttributes, false, false);
    }

    public AtlasEntityWithExtInfo getEntityByAttribute(String typeName, Map<String, String> uniqAttributes, boolean minExtInfo, boolean ignoreRelationship) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        queryParams.add("minExtInfo", String.valueOf(minExtInfo));
        queryParams.add("ignoreRelationships", String.valueOf(ignoreRelationship));

        return callAPI(API_V2.GET_ENTITY_BY_UNIQUE_ATTRIBUTE, AtlasEntityWithExtInfo.class, queryParams, typeName);
    }

    public AtlasEntitiesWithExtInfo getEntitiesByGuids(List<String> guids) throws AtlasServiceException {
        return getEntitiesByGuids(guids, false, false);
    }

    public AtlasEntitiesWithExtInfo getEntitiesByGuids(List<String> guids, boolean minExtInfo, boolean ignoreRelationships) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.put("guid", guids);
        queryParams.add("minExtInfo", String.valueOf(minExtInfo));
        queryParams.add("ignoreRelationships", String.valueOf(ignoreRelationships));

        return callAPI(API_V2.GET_ENTITIES_BY_GUIDS, AtlasEntitiesWithExtInfo.class, queryParams);
    }

    public AtlasEntitiesWithExtInfo getEntitiesByAttribute(String typeName, List<Map<String, String>> uniqAttributesList) throws AtlasServiceException {
        return getEntitiesByAttribute(typeName, uniqAttributesList, false, false);
    }

    public AtlasEntitiesWithExtInfo getEntitiesByAttribute(String typeName, List<Map<String, String>> uniqAttributesList, boolean minExtInfo, boolean ignoreRelationship) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributesList, null);

        queryParams.add("minExtInfo", String.valueOf(minExtInfo));
        queryParams.add("ignoreRelationships", String.valueOf(ignoreRelationship));

        return callAPI(API_V2.GET_ENTITIES_BY_UNIQUE_ATTRIBUTE, AtlasEntitiesWithExtInfo.class, queryParams, typeName);
    }

    public AtlasEntityHeader getEntityHeaderByGuid(String entityGuid) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.GET_ENTITY_HEADER_BY_GUID, entityGuid), AtlasEntityHeader.class, null);
    }

    public AtlasEntityHeader getEntityHeaderByAttribute(String typeName, Map<String, String> uniqAttributes) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        return callAPI(formatPathParameters(API_V2.GET_ENTITY_HEADER_BY_UNIQUE_ATTRIBUTE, typeName), AtlasEntityHeader.class, queryParams);
    }

    public List<EntityAuditEventV2> getAuditEvents(String guid, String startKey, EntityAuditEventV2.EntityAuditActionV2 auditAction, short count) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("startKey", startKey);

        if (auditAction != null) {
            queryParams.add("auditAction", auditAction.toString());
        }

        queryParams.add("count", String.valueOf(count));

        return callAPI(formatPathParameters(API_V2.GET_AUDIT_EVENTS, guid), List.class, queryParams);
    }

    public EntityMutationResponse createEntity(AtlasEntityWithExtInfo entity) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_ENTITY, EntityMutationResponse.class, entity);
    }

    public EntityMutationResponse createEntities(AtlasEntitiesWithExtInfo atlasEntities) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_ENTITIES, EntityMutationResponse.class, atlasEntities);
    }

    public EntityMutationResponse updateEntity(AtlasEntityWithExtInfo entity) throws AtlasServiceException {
        return callAPI(API_V2.UPDATE_ENTITY, EntityMutationResponse.class, entity);
    }

    public EntityMutationResponse updateEntities(AtlasEntitiesWithExtInfo atlasEntities) throws AtlasServiceException {
        return callAPI(API_V2.UPDATE_ENTITIES, EntityMutationResponse.class, atlasEntities);
    }

    public EntityMutationResponse updateEntityByAttribute(String typeName, Map<String, String> uniqAttributes, AtlasEntityWithExtInfo entityInfo) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        return callAPI(API_V2.UPDATE_ENTITY_BY_ATTRIBUTE, EntityMutationResponse.class, entityInfo, queryParams, typeName);
    }

    public EntityMutationResponse partialUpdateEntityByGuid(String entityGuid, Object attrValue, String attrName) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("name", attrName);

        return callAPI(formatPathParameters(API_V2.PARTIAL_UPDATE_ENTITY_BY_GUID, entityGuid), EntityMutationResponse.class, attrValue, queryParams);
    }

    public EntityMutationResponse deleteEntityByGuid(String guid) throws AtlasServiceException {
        return callAPI(API_V2.DELETE_ENTITY_BY_GUID, EntityMutationResponse.class, null, guid);
    }

    public EntityMutationResponse deleteEntityByAttribute(String typeName, Map<String, String> uniqAttributes) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        return callAPI(API_V2.DELETE_ENTITY_BY_ATTRIBUTE, EntityMutationResponse.class, queryParams, typeName);
    }

    public EntityMutationResponse deleteEntitiesByGuids(List<String> guids) throws AtlasServiceException {
        return callAPI(API_V2.DELETE_ENTITIES_BY_GUIDS, EntityMutationResponse.class, "guid", guids);
    }

    public EntityMutationResponse purgeEntitiesByGuids(Set<String> guids) throws AtlasServiceException {
        return callAPI(API_V2.PURGE_ENTITIES_BY_GUIDS, EntityMutationResponse.class, guids);
    }

    // Entity-classification APIs
    public AtlasClassifications getClassifications(String guid) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.GET_CLASSIFICATIONS, guid), AtlasClassifications.class, null);
    }

    public AtlasClassifications getEntityClassifications(String entityGuid, String classificationName) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.GET_FROM_CLASSIFICATION, entityGuid, classificationName), AtlasClassifications.class, null);
    }

    public void addClassification(ClassificationAssociateRequest request) throws AtlasServiceException {
        callAPI(API_V2.ADD_CLASSIFICATION, (Class<?>) null, request);
    }

    public void addClassifications(String guid, List<AtlasClassification> classifications) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.ADD_CLASSIFICATIONS, guid), (Class<?>) null, classifications, (String[]) null);
    }

    public void addClassifications(String typeName, Map<String, String> uniqAttributes, List<AtlasClassification> classifications) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        callAPI(formatPathParameters(API_V2.ADD_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE, typeName), (Class<?>) null, classifications, queryParams);
    }

    public void updateClassifications(String guid, List<AtlasClassification> classifications) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.UPDATE_CLASSIFICATIONS, guid), (Class<?>) null, classifications);
    }

    public void updateClassifications(String typeName, Map<String, String> uniqAttributes, List<AtlasClassification> classifications) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        callAPI(formatPathParameters(API_V2.UPDATE_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE, typeName), (Class<?>) null, classifications, queryParams);
    }

    public String setClassifications(AtlasEntityHeaders entityHeaders) throws AtlasServiceException {
        return callAPI(API_V2.UPDATE_BULK_SET_CLASSIFICATIONS, String.class, entityHeaders);
    }

    public void deleteClassification(String guid, String classificationName) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_CLASSIFICATION, guid, classificationName), null, null);
    }

    public void deleteClassifications(String guid, List<AtlasClassification> classifications) throws AtlasServiceException {
        for (AtlasClassification c : classifications) {
            callAPI(formatPathParameters(API_V2.DELETE_CLASSIFICATION, guid, c.getTypeName()), null, null);
        }
    }

    public void removeClassification(String entityGuid, String classificationName, String associatedEntityGuid) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_CLASSIFICATION, entityGuid, classificationName), (Class<?>) null, associatedEntityGuid);
    }

    public void removeClassification(String typeName, Map<String, String> uniqAttributes, String classificationName) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        callAPI(formatPathParameters(API_V2.DELETE_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE, typeName, classificationName), (Class<?>) null, queryParams);
    }

    public AtlasEntityHeaders getEntityHeaders(long tagUpdateStartTime) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("tagUpdateStartTime", Long.toString(tagUpdateStartTime));

        return callAPI(API_V2.GET_BULK_HEADERS, AtlasEntityHeaders.class, queryParams);
    }

    // Business attributes APIs
    public void addOrUpdateBusinessAttributes(String entityGuid, boolean isOverwrite, Map<String, Map<String, Object>> businessAttributes) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("isOverwrite", String.valueOf(isOverwrite));

        callAPI(formatPathParameters(API_V2.ADD_BUSINESS_ATTRIBUTE, entityGuid), (Class<?>) null, businessAttributes, queryParams);
    }

    public void addOrUpdateBusinessAttributes(String entityGuid, String bmName, Map<String, Map<String, Object>> businessAttributes) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.ADD_BUSINESS_ATTRIBUTE_BY_NAME, entityGuid, bmName), (Class<?>) null, businessAttributes);
    }

    public void removeBusinessAttributes(String entityGuid, Map<String, Map<String, Object>> businessAttributes) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_BUSINESS_ATTRIBUTE, entityGuid), (Class<?>) null, businessAttributes);
    }

    public void removeBusinessAttributes(String entityGuid, String bmName, Map<String, Map<String, Object>> businessAttributes) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_BUSINESS_ATTRIBUTE_BY_NAME, entityGuid, bmName), (Class<?>) null, businessAttributes);
    }

    public String getTemplateForBulkUpdateBusinessAttributes() throws AtlasServiceException {
        InputStream inputStream = (InputStream) callAPI(API_V2.GET_BUSINESS_METADATA_TEMPLATE, Object.class, null);

        return readStreamContents(inputStream);
    }

    public BulkImportResponse bulkUpdateBusinessAttributes(String fileName) throws AtlasServiceException {
        MultiPart multipartEntity = getMultiPartData(fileName);

        return callAPI(API_V2.IMPORT_BUSINESS_METADATA, BulkImportResponse.class, multipartEntity);
    }

    // Labels APIs
    public void addLabels(String entityGuid, Set<String> labels) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.ADD_LABELS, entityGuid), (Class<?>) null, labels, (String[]) null);
    }

    public void addLabels(String typeName, Map<String, String> uniqAttributes, Set<String> labels) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        callAPI(formatPathParameters(API_V2.ADD_LABELS_BY_UNIQUE_ATTRIBUTE, typeName), (Class<?>) null, labels, queryParams);
    }

    public void removeLabels(String entityGuid, Set<String> labels) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_LABELS, entityGuid), (Class<?>) null, labels);
    }

    public void removeLabels(String typeName, Map<String, String> uniqAttributes, Set<String> labels) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        callAPI(formatPathParameters(API_V2.DELETE_LABELS_BY_UNIQUE_ATTRIBUTE, typeName), (Class<?>) null, labels, queryParams);
    }

    public void setLabels(String entityGuid, Set<String> labels) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.SET_LABELS, entityGuid), (Class<?>) null, labels, (String[]) null);
    }

    public void setLabels(String typeName, Map<String, String> uniqAttributes, Set<String> labels) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(uniqAttributes);

        callAPI(formatPathParameters(API_V2.SET_LABELS_BY_UNIQUE_ATTRIBUTE, typeName), (Class<?>) null, labels, queryParams);
    }

    /* Lineage APIs  */
    public AtlasLineageInfo getLineageInfo(String guid, LineageDirection direction, int depth) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("direction", direction.toString());
        queryParams.add("depth", String.valueOf(depth));

        return callAPI(API_V2.LINEAGE_INFO, AtlasLineageInfo.class, queryParams, guid);
    }

    public AtlasLineageInfo getLineageInfo(String typeName, Map<String, String> attributes, LineageDirection direction, int depth) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = attributesToQueryParams(attributes);

        queryParams.add("direction", direction.toString());
        queryParams.add("depth", String.valueOf(depth));

        return callAPI(API_V2.GET_LINEAGE_BY_ATTRIBUTES, AtlasLineageInfo.class, queryParams, typeName);
    }

    public AtlasLineageInfo getLineageInfoOnDemand(String guid, Map<String, LineageOnDemandConstraints> lineageConstraintsByGuid) throws AtlasServiceException {
        return callAPI(API_V2.LINEAGE_INFO_ON_DEMAND, AtlasLineageInfo.class, lineageConstraintsByGuid, guid);
    }

    /* Discovery APIs */
    public AtlasSearchResult dslSearch(String query) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add(QUERY, query);

        return callAPI(API_V2.DSL_SEARCH, AtlasSearchResult.class, queryParams);
    }

    public AtlasSearchResult dslSearchWithParams(String query, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add(QUERY, query);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));

        return callAPI(API_V2.DSL_SEARCH, AtlasSearchResult.class, queryParams);
    }

    public AtlasSearchResult fullTextSearch(String query) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add(QUERY, query);

        return callAPI(API_V2.FULL_TEXT_SEARCH, AtlasSearchResult.class, queryParams);
    }

    public AtlasSearchResult fullTextSearchWithParams(String query, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add(QUERY, query);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));

        return callAPI(API_V2.FULL_TEXT_SEARCH, AtlasSearchResult.class, queryParams);
    }

    public AtlasSearchResult basicSearch(String typeName, String classification, String query, boolean excludeDeletedEntities, int limit, int offset) throws AtlasServiceException {
        return this.basicSearch(typeName, null, classification, query, excludeDeletedEntities, limit, offset);
    }

    public AtlasSearchResult basicSearch(String typeName, SearchParameters.FilterCriteria entityFilters, String classification, String query, boolean excludeDeletedEntities, int limit, int offset) throws AtlasServiceException {
        SearchParameters parameters = new SearchParameters();

        parameters.setTypeName(typeName);
        parameters.setClassification(classification);
        parameters.setQuery(query);
        parameters.setExcludeDeletedEntities(excludeDeletedEntities);
        parameters.setLimit(limit);
        parameters.setOffset(offset);

        if (entityFilters != null) {
            parameters.setEntityFilters(entityFilters);
        }

        return callAPI(API_V2.BASIC_SEARCH, AtlasSearchResult.class, parameters);
    }

    public AtlasSearchResult facetedSearch(SearchParameters searchParameters) throws AtlasServiceException {
        return callAPI(API_V2.FACETED_SEARCH, AtlasSearchResult.class, searchParameters);
    }

    public AtlasSearchResult attributeSearch(String typeName, String attrName, String attrValuePrefix, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("attrName", attrName);
        queryParams.add("attrValuePrefix", attrValuePrefix);
        queryParams.add("typeName", typeName);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));

        return callAPI(API_V2.ATTRIBUTE_SEARCH, AtlasSearchResult.class, queryParams);
    }

    public AtlasSearchResult relationshipSearch(String guid, String relation, String sortByAttribute, SortOrder sortOrder, boolean excludeDeletedEntities, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("guid", guid);
        queryParams.add("relation", relation);
        queryParams.add("sortBy", sortByAttribute);

        if (sortOrder != null) {
            queryParams.add("sortOrder", String.valueOf(sortOrder));
        }

        queryParams.add("excludeDeletedEntities", String.valueOf(excludeDeletedEntities));
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));

        return callAPI(API_V2.RELATIONSHIP_SEARCH, AtlasSearchResult.class, queryParams);
    }

    public AtlasQuickSearchResult quickSearch(String query, String typeName, boolean excludeDeletedEntities, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("query", query);
        queryParams.add("typeName", typeName);
        queryParams.add("excludeDeletedEntities", String.valueOf(excludeDeletedEntities));
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));

        return callAPI(API_V2.QUICK_SEARCH_WITH_GET, AtlasQuickSearchResult.class, queryParams);
    }

    public AtlasQuickSearchResult quickSearch(QuickSearchParameters quickSearchParameters) throws AtlasServiceException {
        return callAPI(API_V2.QUICK_SEARCH_WITH_POST, AtlasQuickSearchResult.class, quickSearchParameters);
    }

    // fieldName should be the parameter on which indexing is enabled such as "qualifiedName"
    public AtlasSuggestionsResult getSuggestions(String prefixString, String fieldName) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        if (StringUtils.isNotEmpty(prefixString)) {
            queryParams.add("prefixString", prefixString);
        }

        if (StringUtils.isNotEmpty(fieldName)) {
            queryParams.add("fieldName", fieldName);
        }

        return callAPI(API_V2.GET_SUGGESTIONS, AtlasSuggestionsResult.class, queryParams);
    }

    public List<AtlasUserSavedSearch> getSavedSearches(String userName) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("user", userName);

        return callAPI(API_V2.GET_SAVED_SEARCHES, List.class, queryParams);
    }

    public AtlasUserSavedSearch getSavedSearch(String userName, String searchName) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("user", userName);

        return callAPI(formatPathParameters(API_V2.GET_SAVED_SEARCH, searchName), AtlasUserSavedSearch.class, queryParams);
    }

    public AtlasUserSavedSearch addSavedSearch(AtlasUserSavedSearch savedSearch) throws AtlasServiceException {
        return callAPI(API_V2.ADD_SAVED_SEARCH, AtlasUserSavedSearch.class, savedSearch);
    }

    public AtlasUserSavedSearch updateSavedSearch(AtlasUserSavedSearch savedSearch) throws AtlasServiceException {
        return callAPI(API_V2.UPDATE_SAVED_SEARCH, AtlasUserSavedSearch.class, savedSearch);
    }

    public void deleteSavedSearch(String guid) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_SAVED_SEARCH, guid), (Class<?>) null, null);
    }

    public AtlasSearchResult executeSavedSearch(String userName, String searchName) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("user", userName);

        return callAPI(formatPathParameters(API_V2.EXECUTE_SAVED_SEARCH_BY_NAME, searchName), AtlasSearchResult.class, queryParams);
    }

    public AtlasSearchResult executeSavedSearch(String searchGuid) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.EXECUTE_SAVED_SEARCH_BY_GUID, searchGuid), AtlasSearchResult.class, null);
    }

    // Relationship APIs
    public AtlasRelationshipWithExtInfo getRelationshipByGuid(String guid) throws AtlasServiceException {
        return callAPI(API_V2.GET_RELATIONSHIP_BY_GUID, AtlasRelationshipWithExtInfo.class, null, guid);
    }

    public AtlasRelationshipWithExtInfo getRelationshipByGuid(String guid, boolean extendedInfo) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("extendedInfo", String.valueOf(extendedInfo));

        return callAPI(API_V2.GET_RELATIONSHIP_BY_GUID, AtlasRelationshipWithExtInfo.class, queryParams, guid);
    }

    public AtlasRelationship createRelationship(AtlasRelationship relationship) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_RELATIONSHIP, AtlasRelationship.class, relationship);
    }

    public AtlasRelationship updateRelationship(AtlasRelationship relationship) throws AtlasServiceException {
        return callAPI(API_V2.UPDATE_RELATIONSHIP, AtlasRelationship.class, relationship);
    }

    public void deleteRelationshipByGuid(String guid) throws AtlasServiceException {
        callAPI(API_V2.DELETE_RELATIONSHIP_BY_GUID, (Class) null, null, guid);
    }

    // Admin APIs
    public List<AtlasAuditEntry> getAtlasAuditByOperation(AuditSearchParameters auditSearchParameters) throws AtlasServiceException {
        ArrayNode response = callAPI(API_V2.GET_ATLAS_AUDITS, ArrayNode.class, auditSearchParameters);

        return extractResults(response, new ExtractOperation<AtlasAuditEntry, ObjectNode>() {
            @Override
            AtlasAuditEntry extractElement(ObjectNode element) {
                return AtlasType.fromV1Json(element.toString(), AtlasAuditEntry.class);
            }
        });
    }

    public void ageoutAtlasAudits(AuditReductionCriteria auditReductionCriteria, boolean useAuditConfig) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("useAuditConfig", String.valueOf(useAuditConfig));

        callAPI(API_V2.AGEOUT_ATLAS_AUDITS, List.class, auditReductionCriteria, queryParams);
    }

    // Glossary APIs
    public List<AtlasGlossary> getAllGlossaries(String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("sort", sortByAttribute);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));

        return callAPI(API_V2.GET_ALL_GLOSSARIES, List.class, queryParams);
    }

    public AtlasGlossary getGlossaryByGuid(String glossaryGuid) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.GET_GLOSSARY_BY_GUID, glossaryGuid), AtlasGlossary.class, null);
    }

    public AtlasGlossary.AtlasGlossaryExtInfo getGlossaryExtInfo(String glossaryGuid) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.GET_DETAILED_GLOSSARY, glossaryGuid), AtlasGlossary.AtlasGlossaryExtInfo.class, null);
    }

    public AtlasGlossaryTerm getGlossaryTerm(String termGuid) throws AtlasServiceException {
        return callAPI(API_V2.GET_GLOSSARY_TERM, AtlasGlossaryTerm.class, null, termGuid);
    }

    public List<AtlasGlossaryTerm> getGlossaryTerms(String glossaryGuid, String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("glossaryGuid", glossaryGuid);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));
        queryParams.add("sort", sortByAttribute);

        return callAPI(formatPathParameters(API_V2.GET_GLOSSARY_TERMS, glossaryGuid), List.class, queryParams);
    }

    public List<AtlasRelatedTermHeader> getGlossaryTermHeaders(String glossaryGuid, String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("glossaryGuid", glossaryGuid);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));
        queryParams.add("sort", sortByAttribute);

        return callAPI(formatPathParameters(API_V2.GET_GLOSSARY_TERMS_HEADERS, glossaryGuid), List.class, queryParams);
    }

    public AtlasGlossaryCategory getGlossaryCategory(String categoryGuid) throws AtlasServiceException {
        return callAPI(API_V2.GET_GLOSSARY_CATEGORY, AtlasGlossaryCategory.class, null, categoryGuid);
    }

    public List<AtlasGlossaryCategory> getGlossaryCategories(String glossaryGuid, String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("glossaryGuid", glossaryGuid);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));
        queryParams.add("sort", sortByAttribute);

        return callAPI(formatPathParameters(API_V2.GET_GLOSSARY_CATEGORIES, glossaryGuid), List.class, queryParams);
    }

    public List<AtlasRelatedCategoryHeader> getGlossaryCategoryHeaders(String glossaryGuid, String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("glossaryGuid", glossaryGuid);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));
        queryParams.add("sort", sortByAttribute);

        return callAPI(formatPathParameters(API_V2.GET_GLOSSARY_CATEGORIES_HEADERS, glossaryGuid), List.class, queryParams);
    }

    public List<AtlasRelatedTermHeader> getCategoryTerms(String categoryGuid, String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("categoryGuid", categoryGuid);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));
        queryParams.add("sort", sortByAttribute);

        return callAPI(formatPathParameters(API_V2.GET_CATEGORY_TERMS, categoryGuid), List.class, queryParams);
    }

    public Map<AtlasGlossaryTerm.Relation, Set<AtlasRelatedTermHeader>> getRelatedTerms(String termGuid, String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("termGuid", termGuid);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));
        queryParams.add("sort", sortByAttribute);

        return callAPI(formatPathParameters(API_V2.GET_RELATED_TERMS, termGuid), Map.class, queryParams);
    }

    public Map<String, List<AtlasRelatedCategoryHeader>> getRelatedCategories(String categoryGuid, String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));
        queryParams.add("sort", sortByAttribute);

        return callAPI(formatPathParameters(API_V2.GET_RELATED_CATEGORIES, categoryGuid), Map.class, queryParams);
    }

    public AtlasGlossary createGlossary(AtlasGlossary glossary) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_GLOSSARY, AtlasGlossary.class, glossary);
    }

    public AtlasGlossaryTerm createGlossaryTerm(AtlasGlossaryTerm glossaryTerm) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_GLOSSARY_TERM, AtlasGlossaryTerm.class, glossaryTerm);
    }

    public List<AtlasGlossaryTerm> createGlossaryTerms(List<AtlasGlossaryTerm> glossaryTerms) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_GLOSSARY_TERMS, List.class, glossaryTerms);
    }

    public AtlasGlossaryCategory createGlossaryCategory(AtlasGlossaryCategory glossaryCategory) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_GLOSSARY_CATEGORY, AtlasGlossaryCategory.class, glossaryCategory);
    }

    public List<AtlasGlossaryCategory> createGlossaryCategories(List<AtlasGlossaryCategory> glossaryCategories) throws AtlasServiceException {
        return callAPI(API_V2.CREATE_GLOSSARY_CATEGORIES, List.class, glossaryCategories);
    }

    public AtlasGlossary updateGlossaryByGuid(String glossaryGuid, AtlasGlossary updatedGlossary) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.UPDATE_GLOSSARY_BY_GUID, glossaryGuid), AtlasGlossary.class, updatedGlossary);
    }

    public AtlasGlossary partialUpdateGlossaryByGuid(String glossaryGuid, Map<String, String> attributes) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.UPDATE_PARTIAL_GLOSSARY, glossaryGuid), AtlasGlossary.class, attributes);
    }

    public AtlasGlossaryTerm updateGlossaryTermByGuid(String termGuid, AtlasGlossaryTerm glossaryTerm) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.UPDATE_GLOSSARY_TERM, termGuid), AtlasGlossaryTerm.class, glossaryTerm);
    }

    public AtlasGlossaryTerm partialUpdateTermByGuid(String termGuid, Map<String, String> attributes) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.UPDATE_PARTIAL_TERM, termGuid), AtlasGlossaryTerm.class, attributes);
    }

    public AtlasGlossaryCategory updateGlossaryCategoryByGuid(String categoryGuid, AtlasGlossaryCategory glossaryCategory) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.UPDATE_CATEGORY_BY_GUID, categoryGuid), AtlasGlossaryCategory.class, glossaryCategory);
    }

    public AtlasGlossaryCategory partialUpdateCategoryByGuid(String categoryGuid, Map<String, String> attributes) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.UPDATE_PARTIAL_CATEGORY, categoryGuid), AtlasGlossaryCategory.class, attributes);
    }

    public void deleteGlossaryByGuid(String glossaryGuid) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_GLOSSARY_BY_GUID, glossaryGuid), (Class<?>) null, null);
    }

    public void deleteGlossaryTermByGuid(String termGuid) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_TERM_BY_GUID, termGuid), (Class<?>) null, null);
    }

    public void deleteGlossaryCategoryByGuid(String categoryGuid) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DELETE_CATEGORY_BY_GUID, categoryGuid), (Class<?>) null, null);
    }

    public List<AtlasRelatedObjectId> getEntitiesAssignedWithTerm(String termGuid, String sortByAttribute, int limit, int offset) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("termGuid", termGuid);
        queryParams.add(LIMIT, String.valueOf(limit));
        queryParams.add(OFFSET, String.valueOf(offset));
        queryParams.add("sort", sortByAttribute);

        return callAPI(formatPathParameters(API_V2.GET_ENTITIES_ASSIGNED_WITH_TERM, termGuid), List.class, queryParams);
    }

    public void assignTermToEntities(String termGuid, List<AtlasRelatedObjectId> relatedObjectIds) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.ASSIGN_TERM_TO_ENTITIES, termGuid), (Class<?>) null, relatedObjectIds);
    }

    public void disassociateTermFromEntities(String termGuid, List<AtlasRelatedObjectId> relatedObjectIds) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.DISASSOCIATE_TERM_FROM_ENTITIES, termGuid), (Class<?>) null, relatedObjectIds);
    }

    public String getGlossaryImportTemplate() throws AtlasServiceException {
        InputStream inputStream = (InputStream) callAPI(API_V2.GET_IMPORT_GLOSSARY_TEMPLATE, Object.class, null);

        return readStreamContents(inputStream);
    }

    public BulkImportResponse importGlossary(String fileName) throws AtlasServiceException {
        MultiPart multipartEntity = getMultiPartData(fileName);

        return callAPI(API_V2.IMPORT_GLOSSARY, BulkImportResponse.class, multipartEntity);
    }

    public void postNotificationToTopic(String topic, List<String> messages) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.POST_NOTIFICATIONS_TO_TOPIC, topic), (Class<?>) null, messages);
    }

    public Map<String, String> getIndexRecoveryData() throws AtlasServiceException {
        return callAPI(API_V2.GET_INDEX_RECOVERY_DATA, Map.class, null);
    }

    public void startIndexRecovery(Instant startTime) throws AtlasServiceException {
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();

        queryParams.add("startTime", startTime != null ? String.valueOf(startTime) : null);

        callAPI(API_V2.START_INDEX_RECOVERY, (Class<?>) null, queryParams);
    }

    @VisibleForTesting
    public API formatPathWithParameter(API api, String... params) {
        return formatPathParameters(api, params);
    }

    public AtlasAsyncImportRequest importAsync(AtlasImportRequest request, InputStream stream) throws AtlasServiceException {
        return performAsyncImport(getImportRequestBodyPart(request), new StreamDataBodyPart(IMPORT_DATA_PARAMETER, stream));
    }

    /**
     * Retrieves a list of asynchronous import statuses.
     * If offset or limit is null, defaults to offset = 0 and limit = 50.
     *
     * @param offset Starting index for the result set
     * @param limit  Maximum number of results to return
     * @return A paginated list of asynchronous import statuses
     * @throws AtlasServiceException if the request fails
     */
    public PList<AsyncImportStatus> getAsyncImportStatus(Integer offset, Integer limit) throws AtlasServiceException {
        int actualOffset = (offset != null) ? offset : 0;
        int actualLimit = (limit != null) ? limit : 50;

        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();
        queryParams.add("offset", String.valueOf(actualOffset));
        queryParams.add("limit", String.valueOf(actualLimit));

        return callAPI(API_V2.ASYNC_IMPORT_STATUS, new GenericType<PList<AsyncImportStatus>>() {}, queryParams);
    }

    public AtlasAsyncImportRequest getAsyncImportStatusById(String importId) throws AtlasServiceException {
        return callAPI(formatPathParameters(API_V2.ASYNC_IMPORT_STATUS_BY_ID, importId), AtlasAsyncImportRequest.class, null);
    }

    public void abortAsyncImport(String importId) throws AtlasServiceException {
        callAPI(formatPathParameters(API_V2.ABORT_ASYNC_IMPORT_BY_ID, importId), null, null);
    }

    @Override
    protected API formatPathParameters(API api, String... params) {
        return new API(String.format(api.getPath(), params), api.getMethod(), api.getExpectedStatus());
    }

    protected <T> String getPathForType(Class<T> typeDefClass) {
        if (AtlasEnumDef.class.isAssignableFrom(typeDefClass)) {
            return "enumdef";
        } else if (AtlasEntityDef.class.isAssignableFrom(typeDefClass)) {
            return "entitydef";
        } else if (AtlasClassificationDef.class.isAssignableFrom(typeDefClass)) {
            return "classificationdef";
        } else if (AtlasRelationshipDef.class.isAssignableFrom(typeDefClass)) {
            return "relationshipdef";
        } else if (AtlasBusinessMetadataDef.class.isAssignableFrom(typeDefClass)) {
            return "businessmetadatadef";
        } else if (AtlasStructDef.class.isAssignableFrom(typeDefClass)) {
            return "structdef";
        }

        // Code should never reach this point
        return "";
    }

    private MultiPart getMultiPartData(String fileName) throws AtlasServiceException {
        try {
            File                             file        = new File(fileName);
            InputStream                      inputStream = new FileInputStream(file);
            final FormDataContentDisposition fd          = FormDataContentDisposition.name("file").fileName(file.getName()).build();

            return new FormDataMultiPart().bodyPart(new StreamDataBodyPart("file", inputStream)).bodyPart(new FormDataBodyPart(fd, "file"));
        } catch (FileNotFoundException e) {
            throw new AtlasServiceException(e);
        }
    }

    private String readStreamContents(InputStream inputStream) throws AtlasServiceException {
        try {
            //Converting InputStream to String
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            StringBuffer   sb     = new StringBuffer();
            String         template;

            while ((template = reader.readLine()) != null) {
                sb.append(template);
            }

            return sb.toString();
        } catch (Exception e) {
            throw new AtlasServiceException(e);
        }
    }

    private <T, U> List<T> extractResults(ArrayNode jsonResponse, ExtractOperation<T, U> extractInterafce) {
        ArrayList<T> ret = new ArrayList<>();

        for (int index = 0; index < jsonResponse.size(); index++) {
            Object element = jsonResponse.get(index);

            ret.add(extractInterafce.extractElement((U) element));
        }

        return ret;
    }

    private MultivaluedMap<String, String> attributesToQueryParams(Map<String, String> attributes) {
        return attributesToQueryParams(attributes, null);
    }

    private MultivaluedMap<String, String> attributesToQueryParams(Map<String, String> attributes, MultivaluedMap<String, String> queryParams) {
        if (queryParams == null) {
            queryParams = new MultivaluedMapImpl();
        }

        if (MapUtils.isNotEmpty(attributes)) {
            for (Map.Entry<String, String> e : attributes.entrySet()) {
                queryParams.putSingle(PREFIX_ATTR + e.getKey(), e.getValue());
            }
        }

        return queryParams;
    }

    private MultivaluedMap<String, String> attributesToQueryParams(List<Map<String, String>> attributesList, MultivaluedMap<String, String> queryParams) {
        if (queryParams == null) {
            queryParams = new MultivaluedMapImpl();
        }

        for (int i = 0; i < attributesList.size(); i++) {
            Map<String, String> attributes = attributesList.get(i);

            for (Map.Entry<String, String> entry : attributes.entrySet()) {
                queryParams.putSingle(PREFIX_ATTR_ + i + ":" + entry.getKey(), entry.getValue());
            }
        }

        return queryParams;
    }

    private <T> T getTypeDefByName(String name, Class<T> typeDefClass) throws AtlasServiceException {
        String pathForType = getPathForType(typeDefClass);
        API    api         = new API(String.format(GET_BY_NAME_TEMPLATE, pathForType, name), HttpMethod.GET, Response.Status.OK);

        return callAPI(api, typeDefClass, null);
    }

    private <T> T getTypeDefByGuid(String guid, Class<T> typeDefClass) throws AtlasServiceException {
        String pathForType = getPathForType(typeDefClass);
        API    api         = new API(String.format(GET_BY_GUID_TEMPLATE, pathForType, guid), HttpMethod.GET, Response.Status.OK);

        return callAPI(api, typeDefClass, null);
    }

    private FormDataBodyPart getImportRequestBodyPart(AtlasImportRequest request) {
        return new FormDataBodyPart(IMPORT_REQUEST_PARAMTER, AtlasType.toJson(request), MediaType.APPLICATION_JSON_TYPE);
    }

    private AtlasAsyncImportRequest performAsyncImport(BodyPart requestPart, BodyPart filePart) throws AtlasServiceException {
        try (FormDataMultiPart formDataMultiPart = new FormDataMultiPart()) {
            MultiPart multipartEntity = formDataMultiPart.bodyPart(requestPart).bodyPart(filePart);

            return callAPI(API_V2.ASYNC_IMPORT, AtlasAsyncImportRequest.class, multipartEntity);
        } catch (IOException e) {
            throw new AtlasServiceException(e);
        }
    }

    public static class API_V2 extends API {
        // TypeDef APIs
        public static final API_V2 GET_TYPEDEF_BY_NAME      = new API_V2(TYPEDEF_BY_NAME, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_TYPEDEF_BY_GUID      = new API_V2(TYPEDEF_BY_GUID, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_ALL_TYPE_DEFS        = new API_V2(TYPEDEFS_API, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_ALL_TYPE_DEF_HEADERS = new API_V2(TYPEDEFS_API + "headers", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 CREATE_TYPE_DEFS         = new API_V2(TYPEDEFS_API, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 UPDATE_TYPE_DEFS         = new API_V2(TYPEDEFS_API, HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 DELETE_TYPE_DEFS         = new API_V2(TYPEDEFS_API, HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 DELETE_TYPE_DEF_BY_NAME  = new API_V2(TYPEDEF_BY_NAME, HttpMethod.DELETE, Response.Status.NO_CONTENT);

        // Entity APIs
        public static final API_V2 GET_ENTITY_BY_GUID                    = new API_V2(ENTITY_API + "guid/", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_ENTITY_BY_UNIQUE_ATTRIBUTE        = new API_V2(ENTITY_API + "uniqueAttribute/type/", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_ENTITIES_BY_GUIDS                 = new API_V2(ENTITY_BULK_API, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_ENTITIES_BY_UNIQUE_ATTRIBUTE      = new API_V2(ENTITY_BULK_API + "uniqueAttribute/type/", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_ENTITY_HEADER_BY_GUID             = new API_V2(ENTITY_API + "guid/%s/header", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_ENTITY_HEADER_BY_UNIQUE_ATTRIBUTE = new API_V2(ENTITY_API + "uniqueAttribute/type/%s/header", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_AUDIT_EVENTS                      = new API_V2(ENTITY_API + "%s/audit", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 CREATE_ENTITY                         = new API_V2(ENTITY_API, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 CREATE_ENTITIES                       = new API_V2(ENTITY_BULK_API, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 UPDATE_ENTITY                         = new API_V2(ENTITY_API, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 UPDATE_ENTITY_BY_ATTRIBUTE            = new API_V2(ENTITY_API + "uniqueAttribute/type/", HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 UPDATE_ENTITIES                       = new API_V2(ENTITY_BULK_API, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 PARTIAL_UPDATE_ENTITY_BY_GUID         = new API_V2(ENTITY_API + "guid/%s", HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 DELETE_ENTITY_BY_GUID                 = new API_V2(ENTITY_API + "guid/", HttpMethod.DELETE, Response.Status.OK);
        public static final API_V2 DELETE_ENTITY_BY_ATTRIBUTE            = new API_V2(ENTITY_API + "uniqueAttribute/type/", HttpMethod.DELETE, Response.Status.OK);
        public static final API_V2 DELETE_ENTITIES_BY_GUIDS              = new API_V2(ENTITY_BULK_API, HttpMethod.DELETE, Response.Status.OK);
        public static final API_V2 PURGE_ENTITIES_BY_GUIDS               = new API_V2(ENTITY_PURGE_API, HttpMethod.PUT, Response.Status.OK);

        // Entity-classification APIs
        public static final API_V2 GET_CLASSIFICATIONS                         = new API_V2(ENTITY_API + "guid/%s/classifications", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_FROM_CLASSIFICATION                     = new API_V2(ENTITY_API + "guid/%s/classification/%s", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 ADD_CLASSIFICATIONS                         = new API_V2(ENTITY_API + "guid/%s/classifications", HttpMethod.POST, Response.Status.NO_CONTENT);
        public static final API_V2 ADD_CLASSIFICATION                          = new API_V2(ENTITY_BULK_API + "/classification", HttpMethod.POST, Response.Status.NO_CONTENT);
        public static final API_V2 ADD_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE    = new API_V2(ENTITY_API + "uniqueAttribute/type/%s/classifications", HttpMethod.POST, Response.Status.NO_CONTENT);
        public static final API_V2 UPDATE_CLASSIFICATIONS                      = new API_V2(ENTITY_API + "guid/%s/classifications", HttpMethod.PUT, Response.Status.NO_CONTENT);
        public static final API_V2 UPDATE_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE = new API_V2(ENTITY_API + "uniqueAttribute/type/%s/classifications", HttpMethod.PUT, Response.Status.NO_CONTENT);
        public static final API_V2 UPDATE_BULK_SET_CLASSIFICATIONS             = new API_V2(ENTITY_API + AtlasClientV2.BULK_SET_CLASSIFICATIONS, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 DELETE_CLASSIFICATION                       = new API_V2(ENTITY_API + "guid/%s/classification/%s", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 DELETE_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE = new API_V2(ENTITY_API + "uniqueAttribute/type/%s/classification/%s", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 GET_BULK_HEADERS                            = new API_V2(ENTITY_API + BULK_HEADERS, HttpMethod.GET, Response.Status.OK);

        // business-attributes APIs
        public static final API_V2 ADD_BUSINESS_ATTRIBUTE            = new API_V2(ENTITY_API + "guid/%s/businessmetadata", HttpMethod.POST, Response.Status.NO_CONTENT);
        public static final API_V2 ADD_BUSINESS_ATTRIBUTE_BY_NAME    = new API_V2(ENTITY_API + "guid/%s/businessmetadata/%s", HttpMethod.POST, Response.Status.NO_CONTENT);
        public static final API_V2 DELETE_BUSINESS_ATTRIBUTE         = new API_V2(ENTITY_API + "guid/%s/businessmetadata", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 DELETE_BUSINESS_ATTRIBUTE_BY_NAME = new API_V2(ENTITY_API + "guid/%s/businessmetadata/%s", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 GET_BUSINESS_METADATA_TEMPLATE    = new API_V2(ENTITY_API + "businessmetadata/import/template", HttpMethod.GET, Response.Status.OK, MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM);
        public static final API_V2 IMPORT_BUSINESS_METADATA          = new API_V2(ENTITY_API + "businessmetadata/import", HttpMethod.POST, Response.Status.OK, MediaType.MULTIPART_FORM_DATA, MediaType.APPLICATION_JSON);

        public static final API_V2 POST_NOTIFICATIONS_TO_TOPIC = new API_V2(NOTIFICATION_URI + "/topic/%s", HttpMethod.POST, Response.Status.NO_CONTENT);

        public static final API_V2 GET_INDEX_RECOVERY_DATA = new API_V2(INDEX_RECOVERY_URI, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 START_INDEX_RECOVERY    = new API_V2(INDEX_RECOVERY_URI + "/start", HttpMethod.POST, Response.Status.NO_CONTENT);

        // labels APIs
        public static final API_V2 ADD_LABELS                        = new API_V2(ENTITY_API + "guid/%s/labels", HttpMethod.PUT, Response.Status.NO_CONTENT);
        public static final API_V2 ADD_LABELS_BY_UNIQUE_ATTRIBUTE    = new API_V2(ENTITY_API + "uniqueAttribute/type/%s/labels", HttpMethod.PUT, Response.Status.NO_CONTENT);
        public static final API_V2 SET_LABELS                        = new API_V2(ENTITY_API + "guid/%s/labels", HttpMethod.POST, Response.Status.NO_CONTENT);
        public static final API_V2 SET_LABELS_BY_UNIQUE_ATTRIBUTE    = new API_V2(ENTITY_API + "uniqueAttribute/type/%s/labels", HttpMethod.POST, Response.Status.NO_CONTENT);
        public static final API_V2 DELETE_LABELS                     = new API_V2(ENTITY_API + "guid/%s/labels", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 DELETE_LABELS_BY_UNIQUE_ATTRIBUTE = new API_V2(ENTITY_API + "uniqueAttribute/type/%s/labels", HttpMethod.DELETE, Response.Status.NO_CONTENT);

        // Lineage APIs
        public static final API_V2 LINEAGE_INFO              = new API_V2(LINEAGE_URI, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_LINEAGE_BY_ATTRIBUTES = new API_V2(LINEAGE_URI + "uniqueAttribute/type/", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 LINEAGE_INFO_ON_DEMAND    = new API_V2(LINEAGE_URI, HttpMethod.POST, Response.Status.OK);

        // Discovery APIs
        public static final API_V2 DSL_SEARCH                   = new API_V2(DSL_SEARCH_URI, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 FULL_TEXT_SEARCH             = new API_V2(FULL_TEXT_SEARCH_URI, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 BASIC_SEARCH                 = new API_V2(BASIC_SEARCH_URI, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 FACETED_SEARCH               = new API_V2(FACETED_SEARCH_URI, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 ATTRIBUTE_SEARCH             = new API_V2(DISCOVERY_URI + "/attribute", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 RELATIONSHIP_SEARCH          = new API_V2(DISCOVERY_URI + "/relationship", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 QUICK_SEARCH_WITH_GET        = new API_V2(QUICK_SEARCH_URI, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 QUICK_SEARCH_WITH_POST       = new API_V2(QUICK_SEARCH_URI, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 GET_SUGGESTIONS              = new API_V2(DISCOVERY_URI + "/suggestions", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_SAVED_SEARCHES           = new API_V2(SAVED_SEARCH_URI, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_SAVED_SEARCH             = new API_V2(SAVED_SEARCH_URI + "/%s", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 ADD_SAVED_SEARCH             = new API_V2(SAVED_SEARCH_URI, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 UPDATE_SAVED_SEARCH          = new API_V2(SAVED_SEARCH_URI, HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 DELETE_SAVED_SEARCH          = new API_V2(SAVED_SEARCH_URI + "/%s", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 EXECUTE_SAVED_SEARCH_BY_NAME = new API_V2(SAVED_SEARCH_URI + "/execute/%s", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 EXECUTE_SAVED_SEARCH_BY_GUID = new API_V2(SAVED_SEARCH_URI + "/execute/guid/%s", HttpMethod.GET, Response.Status.OK);

        // Relationship APIs
        public static final API_V2 GET_RELATIONSHIP_BY_GUID    = new API_V2(RELATIONSHIPS_URI + "guid/", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 CREATE_RELATIONSHIP         = new API_V2(RELATIONSHIPS_URI, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 UPDATE_RELATIONSHIP         = new API_V2(RELATIONSHIPS_URI, HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 DELETE_RELATIONSHIP_BY_GUID = new API_V2(RELATIONSHIPS_URI + "guid/", HttpMethod.DELETE, Response.Status.NO_CONTENT);

        // Admin APIs
        public static final API_V2 GET_ATLAS_AUDITS    = new API_V2(ATLAS_AUDIT_API, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 AGEOUT_ATLAS_AUDITS = new API_V2(ATLAS_AUDIT_API + "ageout/", HttpMethod.POST, Response.Status.OK);

        // Async Import APIs
        public static final API_V2 ASYNC_IMPORT              = new API_V2(ASYNC_IMPORT_URI, HttpMethod.POST, Response.Status.OK, MediaType.MULTIPART_FORM_DATA, MediaType.APPLICATION_JSON);
        public static final API_V2 ASYNC_IMPORT_STATUS   = new API_V2(ASYNC_IMPORT_STATUS_URI, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 ASYNC_IMPORT_STATUS_BY_ID = new API_V2(ASYNC_IMPORT_STATUS_URI + "/%s", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 ABORT_ASYNC_IMPORT_BY_ID = new API_V2(ASYNC_IMPORT_URI + "/%s", HttpMethod.DELETE, Response.Status.NO_CONTENT);

        // Glossary APIs
        public static final API_V2 GET_ALL_GLOSSARIES              = new API_V2(GLOSSARY_URI, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_GLOSSARY_BY_GUID            = new API_V2(GLOSSARY_URI + "/%s", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_DETAILED_GLOSSARY           = new API_V2(GLOSSARY_URI + "/%s/detailed", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_GLOSSARY_TERM               = new API_V2(GLOSSARY_TERM, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_GLOSSARY_TERMS              = new API_V2(GLOSSARY_URI + "/%s/terms", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_GLOSSARY_TERMS_HEADERS      = new API_V2(GLOSSARY_URI + "/%s/terms/headers", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_GLOSSARY_CATEGORY           = new API_V2(GLOSSARY_CATEGORY, HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_GLOSSARY_CATEGORIES         = new API_V2(GLOSSARY_URI + "/%s/categories", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_GLOSSARY_CATEGORIES_HEADERS = new API_V2(GLOSSARY_URI + "/%s/categories/headers", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_CATEGORY_TERMS              = new API_V2(GLOSSARY_CATEGORY + "/%s/terms", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_RELATED_TERMS               = new API_V2(GLOSSARY_TERMS + "/%s/related", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 GET_RELATED_CATEGORIES          = new API_V2(GLOSSARY_CATEGORY + "/%s/related", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 CREATE_GLOSSARY                 = new API_V2(GLOSSARY_URI, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 CREATE_GLOSSARY_TERM            = new API_V2(GLOSSARY_TERM, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 CREATE_GLOSSARY_TERMS           = new API_V2(GLOSSARY_TERMS, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 CREATE_GLOSSARY_CATEGORY        = new API_V2(GLOSSARY_CATEGORY, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 CREATE_GLOSSARY_CATEGORIES      = new API_V2(GLOSSARY_CATEGORIES, HttpMethod.POST, Response.Status.OK);
        public static final API_V2 UPDATE_GLOSSARY_BY_GUID         = new API_V2(GLOSSARY_URI + "/%s", HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 UPDATE_PARTIAL_GLOSSARY         = new API_V2(GLOSSARY_URI + "/%s/partial", HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 UPDATE_GLOSSARY_TERM            = new API_V2(GLOSSARY_TERM + "/%s", HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 UPDATE_PARTIAL_TERM             = new API_V2(GLOSSARY_TERM + "/%s/partial", HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 UPDATE_CATEGORY_BY_GUID         = new API_V2(GLOSSARY_CATEGORY + "/%s", HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 UPDATE_PARTIAL_CATEGORY         = new API_V2(GLOSSARY_CATEGORY + "/%s/partial", HttpMethod.PUT, Response.Status.OK);
        public static final API_V2 DELETE_GLOSSARY_BY_GUID         = new API_V2(GLOSSARY_URI + "/%s", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 DELETE_TERM_BY_GUID             = new API_V2(GLOSSARY_TERM + "/%s", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 DELETE_CATEGORY_BY_GUID         = new API_V2(GLOSSARY_CATEGORY + "/%s", HttpMethod.DELETE, Response.Status.NO_CONTENT);
        public static final API_V2 GET_ENTITIES_ASSIGNED_WITH_TERM = new API_V2(GLOSSARY_TERMS + "/%s/assignedEntities", HttpMethod.GET, Response.Status.OK);
        public static final API_V2 ASSIGN_TERM_TO_ENTITIES         = new API_V2(GLOSSARY_TERMS + "/%s/assignedEntities", HttpMethod.POST, Response.Status.NO_CONTENT);
        public static final API_V2 DISASSOCIATE_TERM_FROM_ENTITIES = new API_V2(GLOSSARY_TERMS + "/%s/assignedEntities", HttpMethod.PUT, Response.Status.NO_CONTENT);
        public static final API_V2 GET_IMPORT_GLOSSARY_TEMPLATE    = new API_V2(GLOSSARY_URI + "/import/template", HttpMethod.GET, Response.Status.OK, MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM);
        public static final API_V2 IMPORT_GLOSSARY                 = new API_V2(GLOSSARY_URI + "/import", HttpMethod.POST, Response.Status.OK, MediaType.MULTIPART_FORM_DATA, MediaType.APPLICATION_JSON);

        private API_V2(String path, String method, Response.Status status) {
            super(path, method, status);
        }

        private API_V2(String path, String method, Response.Status status, String consumes, String produces) {
            super(path, method, status, consumes, produces);
        }
    }

    private static class ExtractOperation<T, U> {
        T extractElement(U element) {
            return (T) element;
        }
    }
}
