/*-
 * ~~~~~~licensing~~~~~~
 * atlasmap-entaxy-services
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
/*
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * 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 ru.entaxy.esb.atlasmap.rest;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Paths;
import java.util.ArrayList;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

import org.osgi.framework.BundleContext;
import org.osgi.framework.wiring.BundleWiring;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;

import io.atlasmap.api.AtlasContext;
import io.atlasmap.api.AtlasContextFactory;
import io.atlasmap.api.AtlasException;
import io.atlasmap.api.AtlasMappingBuilder;
import io.atlasmap.api.AtlasPreviewContext;
import io.atlasmap.api.AtlasSession;
import io.atlasmap.core.ADMArchiveHandler;
import io.atlasmap.core.AtlasUtil;
import io.atlasmap.core.DefaultAtlasContextFactory;
import io.atlasmap.core.DefaultAtlasFieldActionService;
import io.atlasmap.json.inspect.JsonInspectionService;
import io.atlasmap.json.v2.JsonDocument;
import io.atlasmap.json.v2.JsonInspectionRequest;
import io.atlasmap.json.v2.JsonInspectionResponse;
import io.atlasmap.service.AtlasLibraryLoader;
import io.atlasmap.service.AtlasLibraryLoader.AtlasLibraryLoaderListener;
import io.atlasmap.v2.ActionDetails;
import io.atlasmap.v2.AtlasMapping;
import io.atlasmap.v2.Audits;
import io.atlasmap.v2.Json;
import io.atlasmap.v2.Mapping;
import io.atlasmap.v2.MappingFileType;
import io.atlasmap.v2.ProcessMappingRequest;
import io.atlasmap.v2.ProcessMappingResponse;
import io.atlasmap.v2.StringMap;
import io.atlasmap.v2.StringMapEntry;
import io.atlasmap.v2.Validations;
import io.atlasmap.xml.inspect.XmlInspectionService;
import io.atlasmap.xml.v2.XmlDocument;
import io.atlasmap.xml.v2.XmlInspectionRequest;
import io.atlasmap.xml.v2.XmlInspectionResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import ru.entaxy.esb.resources.EntaxyResource;
import ru.entaxy.esb.resources.EntaxyResourceService;

@Path("/")
public class AtlasAdminJaxrsApplication {

    static final String MAPPING_NAME_PREFIX = "UI.";
    static final String ATLASMAP_ADM_PATH = "atlasmap.adm.path";
    static final String ATLASMAP_WORKSPACE = "atlasmap.workspace";
    private static final Logger LOG = LoggerFactory.getLogger(AtlasAdminJaxrsApplication.class);

    private final DefaultAtlasContextFactory atlasContextFactory = DefaultAtlasContextFactory.getInstance();
    private final AtlasPreviewContext previewContext;

    private String baseFolder = "";
    private String mappingFolder = "";
    private String libFolder = "";
    private AtlasLibraryLoader libraryLoader;

    private BundleContext bundleContext;
    private EntaxyResourceService entaxyResourceService;

    public AtlasAdminJaxrsApplication(BundleContext bundleContext) throws AtlasException {
        this.bundleContext = bundleContext;

        String atlasmapWorkspace = System.getProperty(ATLASMAP_WORKSPACE);
        LOG.debug("AtlasMap backend Working directory: {}", atlasmapWorkspace);
        if (atlasmapWorkspace != null && atlasmapWorkspace.length() > 0) {
            baseFolder = atlasmapWorkspace;
        }
        else {
            baseFolder = "target";
        }

        mappingFolder = baseFolder + File.separator + "mappings";
        libFolder = baseFolder + File.separator + "lib";

        this.libraryLoader = new AtlasLibraryLoader(libFolder);

        setClassLoader();

        // Add atlas-core in case it runs on modular class loader
        this.libraryLoader.addAlternativeLoader(DefaultAtlasFieldActionService.class.getClassLoader());
        this.libraryLoader.addListener(new AtlasLibraryLoaderListener() {
            @Override
            public void onUpdate(AtlasLibraryLoader loader) {
                synchronized (atlasContextFactory) {
                    ((DefaultAtlasContextFactory)atlasContextFactory).destroy();
                    ((DefaultAtlasContextFactory)atlasContextFactory).init(libraryLoader);
                }
            }
        });

        String atlasmapAdmPath = System.getProperty(ATLASMAP_ADM_PATH);
        if (atlasmapAdmPath != null && atlasmapAdmPath.length() > 0) {
            LOG.debug("Loading initial ADM file: {}", atlasmapAdmPath);
            this.libraryLoader.clearLibraries();
            ADMArchiveHandler admHandler = new ADMArchiveHandler(this.libraryLoader);
            java.nio.file.Path mappingDirPath = Paths.get(getMappingSubDirectory(0));
            admHandler.setPersistDirectory(mappingDirPath);
            admHandler.setIgnoreLibrary(false);
            admHandler.setLibraryDirectory(Paths.get(libFolder));
            admHandler.load(Paths.get(atlasmapAdmPath));
            this.libraryLoader.reload();
            admHandler.persist();
        }

        synchronized (atlasContextFactory) {
            ((DefaultAtlasContextFactory)atlasContextFactory).destroy();
            ((DefaultAtlasContextFactory)atlasContextFactory).init(libraryLoader);
        }
        this.previewContext = atlasContextFactory.createPreviewContext();
    }
    
    public void setEntaxyResourceService(EntaxyResourceService entaxyResourceService) {
        this.entaxyResourceService = entaxyResourceService;
    }
    
//JSON Service
	
    @GET
    @Path("/json/simple")
    @Produces(MediaType.TEXT_PLAIN)
    @Operation(summary ="Simple", description = "Simple hello service")
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = String.class)), description = "Return a response"))
    public String jsonSimpleHelloWorld(@Parameter(description = "From") @QueryParam("from") String from) {
        return "Got it! " + from;
    }

    @POST
    @Path("/json/inspect")
    @Consumes({ MediaType.APPLICATION_JSON })
    @Produces({ MediaType.APPLICATION_JSON })
    @Operation(summary ="Inspect JSON", description = "Inspect a JSON schema or instance and return a Document object")
    @RequestBody(description = "JsonInspectionRequest object",  content = @Content(schema = @Schema(implementation = JsonInspectionRequest.class)))
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = JsonInspectionResponse.class)), description = "Return a Document object represented by JsonDocument"))
    public Response jsonInspect(InputStream requestIn) {
        JsonInspectionRequest request = fromJson(requestIn, JsonInspectionRequest.class);
        long startTime = System.currentTimeMillis();

        JsonInspectionResponse response = new JsonInspectionResponse();
        JsonDocument d = null;

        try {

            if (request.getType() == null || request.getJsonData() == null) {
                response.setErrorMessage(
                        "Json data and Instance or Schema inspection type must be specified in request");
            } else {
                JsonInspectionService s = new JsonInspectionService();

                String jsonData = cleanJsonData(request.getJsonData());
                if (!validJsonData(jsonData)) {
                    response.setErrorMessage("Invalid json payload specified");
                } else {
                    switch (request.getType()) {
                    case INSTANCE:
                        d = s.inspectJsonDocument(jsonData);
                        break;
                    case SCHEMA:
                        d = s.inspectJsonSchema(jsonData);
                        break;
                    default:
                        response.setErrorMessage("Unsupported inspection type: " + request.getType());
                        break;
                    }
                }
            }
        } catch (Exception e) {
            LOG.error("Error inspecting json: " + e.getMessage(), e);
            response.setErrorMessage(e.getMessage());
        } finally {
            response.setExecutionTime(System.currentTimeMillis() - startTime);
        }

        response.setJsonDocument(d);
        return Response.ok().entity(toJson(response)).build();
    }
    
    //End of JSON Service
    
    //XML service
    
    @GET
    @Path("/xml/simple")
    @Produces(MediaType.TEXT_PLAIN)
    @Operation(summary = "Simple", description = "Simple hello service")
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = String.class)), description = "Return a response"))
    public String xmlSimpleHelloWorld(@QueryParam("from") String from) {
        return "Got it! " + from;
    }


    @POST
    @Consumes({ MediaType.APPLICATION_JSON })
    @Produces({ MediaType.APPLICATION_JSON })
    @Path("/xml/inspect")
    @Operation(summary = "Inspect XML", description = "Inspect a XML schema or instance and return a Document object")
    @RequestBody(description = "XmlInspectionRequest object", content = @Content(schema = @Schema(implementation = XmlInspectionRequest.class)))
    @ApiResponses(@ApiResponse(responseCode = "200",  content = @Content(schema = @Schema(implementation = XmlInspectionResponse.class)), description = "Return a Document object represented by XmlDocument"))
    public Response xmlInspect(InputStream request) {
        return xmlInspect(fromJson(request, XmlInspectionRequest.class));
    }

    public Response xmlInspect(XmlInspectionRequest request) {
        long startTime = System.currentTimeMillis();

        XmlInspectionResponse response = new XmlInspectionResponse();
        XmlDocument d = null;

        try {

            if (request.getType() == null) {
                response.setErrorMessage("Instance or Schema type must be specified in request");
            } else {
                XmlInspectionService s = new XmlInspectionService();

                switch (request.getType()) {
                case INSTANCE:
                    d = s.inspectXmlDocument(request.getXmlData());
                    break;
                case SCHEMA:
                    d = s.inspectSchema(request.getXmlData());
                    break;
                default:
                    response.setErrorMessage("Unsupported inspection type: " + request.getType());
                    break;
                }
            }
        } catch (Exception e) {
            LOG.error("Error inspecting xml: " + e.getMessage(), e);
            response.setErrorMessage(e.getMessage());
        } finally {
            response.setExecutionTime(System.currentTimeMillis() - startTime);
        }

        response.setXmlDocument(d);
        return Response.ok().entity(toJson(response)).build();
    }
    
    //End of XML service

    protected boolean validJsonData(String jsonData) {
        if (jsonData == null || jsonData.isEmpty()) {
            return false;
        }

        return jsonData.startsWith("{") || jsonData.startsWith("[");
    }

    protected String cleanJsonData(String jsonData) {
        return jsonData.trim();
    }
	
    @GET
    @Path("/fieldActions")
    @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_OCTET_STREAM})
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM})
    @Operation(summary = "List FieldActions", description = "Retrieves a list of available field action")
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ActionDetails.class)) , description = "Return a list of field action detail"))
    public Response listFieldActions(@Context UriInfo uriInfo) {
        ActionDetails details = new ActionDetails();

        if (atlasContextFactory == null || atlasContextFactory.getFieldActionService() == null) {
            return Response.ok().entity(toJson(details)).build();
        }

        details.getActionDetail().addAll(atlasContextFactory.getFieldActionService().listActionDetails());
        byte[] serialized = toJson(details);
        if (LOG.isDebugEnabled()) {
            LOG.debug(new String(serialized));
        }
        return Response.ok().entity(serialized).build();
    }

    @Deprecated
    @GET
    @Path("/mappings")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "List Mappings", description = "Retrieves a list of mapping file name saved with specified mappingDefinitionId")
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = StringMap.class)) , description = "Return a list of a pair of mapping file name and content"))
    public Response listMappingsOld(@Context UriInfo uriInfo, @QueryParam("filter") final String filter)
    {
        return listMappings(uriInfo, filter, 0);
    }

    @GET
    @Path("/mappings/{mappingDefinitionId}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "List Mappings", description = "Retrieves a list of mapping file name saved with specified mappingDefinitionId")
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = StringMap.class)) , description = "Return a list of a pair of mapping file name and content"))
    public Response listMappings(@Context UriInfo uriInfo, @QueryParam("filter") final String filter,
                                 @Parameter(description = "Mapping Definition ID") @PathParam("mappingDefinitionId") Integer mappingDefinitionId) {
        StringMap sMap = new StringMap();
        LOG.debug("listMappings with filter '{}'", filter);

        ADMArchiveHandler handler = loadExplodedMappingDirectory(mappingDefinitionId);
        AtlasMapping map = handler.getMappingDefinition();
        if (map == null) {
            return Response.ok().entity(toJson(sMap)).build();
        }
        StringMapEntry mapEntry = new StringMapEntry();
        mapEntry.setName(map.getName());
        UriBuilder builder = uriInfo.getBaseUriBuilder().path("v2").path("atlas").path("mapping")
            .path(map.getName());
        mapEntry.setValue(builder.build().toString());
        sMap.getStringMapEntry().add(mapEntry);

        byte[] serialized = toJson(sMap);
        if (LOG.isDebugEnabled()) {
            LOG.debug(new String(serialized));
        }
        return Response.ok().entity(serialized).build();
    }

    @Deprecated
    @DELETE
    @Path("/mapping")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Remove Mapping", description = "Remove a mapping file saved on the server")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Specified mapping file was removed successfully"),
        @ApiResponse(responseCode = "204", description = "Mapping file was not found")})
    public Response removeMappingRequestOld() {
        return removeMappingRequest(0);
    }

    @DELETE
    @Path("/mapping/{mappingDefinitionId}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Remove Mapping", description = "Remove a mapping file saved on the server")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Specified mapping file was removed successfully"),
        @ApiResponse(responseCode = "204", description = "Mapping file was not found")})
    public Response removeMappingRequest(@Parameter(description = "Mapping ID") @PathParam("mappingDefinitionId") Integer mappingDefinitionId) {

        java.nio.file.Path mappingDirPath = Paths.get(getMappingSubDirectory(mappingDefinitionId));
        File mappingDirFile = mappingDirPath.toFile();

        if (mappingDirFile == null || !mappingDirFile.exists()) {
            return Response.noContent().build();
        }

        if (!mappingDirFile.isDirectory()) {
            LOG.warn("Removing invalid file '{}' in a persistent directory", mappingDirFile.getAbsolutePath());
        } else {
            AtlasUtil.deleteDirectory(mappingDirFile);
        }

        return Response.ok().build();
    }

    @Deprecated
    @DELETE
    @Path("/mapping/RESET")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Remove Mapping by ID", description = "Remove mapping file and catalogs related to specified ID")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Mapping file and Catalogs were removed successfully"),
        @ApiResponse(responseCode = "204", description = "Unable to remove mapping file and Catalogs for the specified ID")})
    public Response resetMappingByIdOld()
    {
        return resetMappingById(0);
    }

    @DELETE
    @Path("/mapping/RESET/{mappingDefinitionId}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Remove Mapping by ID", description = "Remove mapping file and catalogs related to specified ID")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Mapping file and Catalogs were removed successfully"),
        @ApiResponse(responseCode = "204", description = "Unable to remove mapping file and Catalogs for the specified ID")})
    public Response resetMappingById(@Parameter(description = "Mapping ID") @PathParam("mappingDefinitionId") Integer mappingDefinitionId) {
        LOG.debug("resetMappingById {} ", mappingDefinitionId);

        java.nio.file.Path mappingFolderPath = Paths.get(getMappingSubDirectory(mappingDefinitionId));
        File mappingFolderFile = mappingFolderPath.toFile();

        if (mappingFolderFile == null || !mappingFolderFile.exists()) {
            return Response.ok().build();
        }

        if (!mappingFolderFile.isDirectory()) {
            LOG.warn("{} is not a directory - removing anyway", mappingFolderFile.getAbsolutePath());
        }
        AtlasUtil.deleteDirectory(mappingFolderFile);
        return Response.ok().build();
    }

    @DELETE
    @Path("/mapping/RESET/ALL")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary =  "Remove All Mappings", description = "Remove all mapping files and catalogs saved on the server")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "All mapping files were removed successfully"),
        @ApiResponse(responseCode = "204", description = "Unable to remove all mapping files")})
    public Response resetAllMappings() {
        LOG.debug("resetAllMappings");

        java.nio.file.Path mappingFolderPath = Paths.get(mappingFolder);
        File mappingFolderPathFile = mappingFolderPath.toFile();

        if (mappingFolderPathFile == null || !mappingFolderPathFile.exists()) {
            return Response.ok().build();
        }

        AtlasUtil.deleteDirectoryContents(mappingFolderPathFile);
        return Response.ok().build();
    }

    @DELETE
    @Path("/mapping/resetLibs")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Remove All User-Defined JAR libraries", description = "Remove all user-defined JAR files saved on the server")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "All user-defined JAR files were removed successfully"),
        @ApiResponse(responseCode = "204", description = "Unable to remove all user-defined JAR files")})
    public Response resetUserLibs() {
        LOG.debug("resetUserLibs");
        this.libraryLoader.clearLibraries();
        return Response.ok().build();
    }

    @Deprecated
    @GET
    @Path("/mapping/{mappingFormat}")
    @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.APPLICATION_OCTET_STREAM})
    @Operation(summary = "Get Mapping", description = "Retrieve a mapping file saved on the server")
    @ApiResponses({
        @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation =  AtlasMapping.class)), description = "Return a mapping file content"),
        @ApiResponse(responseCode = "204", description = "Mapping file was not found"),
        @ApiResponse(responseCode = "500", description = "Mapping file access error")})
    public Response getMappingRequestOld(
      @Parameter(description = "Mapping Format") @PathParam("mappingFormat") MappingFileType mappingFormat)
    {
        return getMappingRequest(mappingFormat, 0);
    }

    @GET
    @Path("/mapping/ntxregistry/{registryLocation}")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    @Operation(summary = "Get Mapping", description = "Retrieve a mapping file saved on the server")
    @ApiResponses({
            @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = AtlasMapping.class)), description = "Return a mapping file content"),
            @ApiResponse(responseCode = "204", description = "Mapping file was not found"),
            @ApiResponse(responseCode = "500", description = "Mapping file access error")})
    public Response getMappingNTXRequest(
            @Parameter(description = "Entaxy Registry Location") @PathParam("registryLocation") String registryLocation
    ) {
        LOG.debug("Getting mapping file from Entaxy registry '{}'", registryLocation);

        if (registryLocation != null || !registryLocation.isEmpty()) {

            EntaxyResource entaxyResource = entaxyResourceService.getResource(registryLocation);
            InputStream resourceInputStream = entaxyResource.getInputStream();

            try {
                return Response.ok().entity(readFromStream(resourceInputStream)).build();
            } catch (Exception e) {
                LOG.error("Error getting ADM archive file.\n" + e.getMessage(), e);
                throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
            }
        } else {
            throw new WebApplicationException("Invalid registry location.");
        }
    }

    @GET
    @Path("/mapping/ntxregistry/content/{registryLocation}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Get Resourse Content", description = "Retrieve a resource content saved on the server")
    @ApiResponses({
            @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = AtlasMapping.class)), description = "Return a mapping file content"),
            @ApiResponse(responseCode = "204", description = "Mapping file was not found"),
            @ApiResponse(responseCode = "500", description = "Mapping file access error")})
    public Response getResourceContentRequest(
            @Parameter(description = "Entaxy Registry Location") @PathParam("registryLocation") String registryLocation
    ) {
        LOG.debug("Getting resource content from Entaxy registry '{}'", registryLocation);

        if (registryLocation != null || !registryLocation.isEmpty()) {

            EntaxyResource entaxyResource = entaxyResourceService.getResource(registryLocation);

            return Response.ok().entity(entaxyResource.getAsString()).build();
        } else {
            throw new WebApplicationException("Invalid registry location.");
        }
    }

    @GET
    @Path("/mapping_ntx/{mappingFormat}/{mappingDefinitionId}/{registryLocation}")
    @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.APPLICATION_OCTET_STREAM})
    @Operation(summary = "Get Mapping", description = "Retrieve a mapping file saved on the server")
    @ApiResponses({
        @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = AtlasMapping.class)), description = "Return a mapping file content"),
        @ApiResponse(responseCode = "204", description = "Mapping file was not found"),
        @ApiResponse(responseCode = "500", description = "Mapping file access error")})
    public Response getMappingNTXRequest(
      @Parameter(description = "Mapping Format") @PathParam("mappingFormat") MappingFileType mappingFormat,
      @Parameter(description = "Mapping ID") @PathParam("mappingDefinitionId") Integer mappingDefinitionId,
      @Parameter(description = "Entaxy Registry Location") @PathParam("registryLocation") String registryLocation) {
    	LOG.debug("getMappingRequest: {} '{}'", mappingFormat, mappingDefinitionId);
        ADMArchiveHandler admHandler = loadExplodedMappingDirectory(mappingDefinitionId);

        switch (mappingFormat) {
        case JSON:
            byte[] serialized = null;
            try {
                serialized = admHandler.getMappingDefinitionBytes();
            } catch (Exception e) {
                LOG.error("Error retrieving mapping definition file for ID:" + mappingDefinitionId, e);
                throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
            }
            if (LOG.isDebugEnabled() && serialized != null) {
                LOG.debug(new String(serialized));
            }
            if (serialized == null) {
                LOG.debug("Mapping definition not found for ID:{}", mappingDefinitionId);
                return Response.noContent().build();
            }
            return Response.ok().entity(serialized).build();
        case GZ:
            try {
                if (admHandler.getGzippedADMDigestBytes() == null) {
                    LOG.debug("ADM Digest file not found for ID:{}", mappingDefinitionId);
                    return Response.noContent().build();
                }
                return Response.ok().entity(admHandler.getGzippedADMDigestBytes()).build();
            } catch (Exception e) {
                LOG.error("Error getting compressed ADM digest file.\n" + e.getMessage(), e);
                throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
            }
        case ZIP:
        	if (registryLocation == null || registryLocation.isEmpty()) {
        		try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                    admHandler.setIgnoreLibrary(false);
                    admHandler.setLibraryDirectory(Paths.get(this.libFolder));
                    admHandler.export(out);
                    return Response.ok().entity(out.toByteArray()).build();
                } catch (Exception e) {
                    LOG.error("Error getting ADM archive file.\n" + e.getMessage(), e);
                    throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
                }
        	} else {
        		//TODO Save to Entaxy registry
//        		try {
//            		EntaxyResource entaxyResource = entaxyResourceService.getResource(registryLocation);
//            		entaxyResource.save(mapping);
//            		builder.path(entaxyResource.getName());
//            	} catch (Exception e) {
//            		LOG.error("Error while uploading file to Entaxy registry: " + e.getMessage(), e);
//            		throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
//                }
        	}
        default:
            throw new WebApplicationException("Unrecognized mapping format: " + mappingFormat, Status.INTERNAL_SERVER_ERROR);
        }
    }

    @GET
    @Path("/mapping/{mappingFormat}/{mappingDefinitionId}")
    @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.APPLICATION_OCTET_STREAM})
    @Operation(summary = "Get Mapping", description = "Retrieve a mapping file saved on the server")
    @ApiResponses({
        @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = AtlasMapping.class)), description = "Return a mapping file content"),
        @ApiResponse(responseCode = "204", description = "Mapping file was not found"),
        @ApiResponse(responseCode = "500", description = "Mapping file access error")})
    public Response getMappingRequest(
      @Parameter(description = "Mapping Format") @PathParam("mappingFormat") MappingFileType mappingFormat,
      @Parameter(description = "Mapping ID") @PathParam("mappingDefinitionId") Integer mappingDefinitionId) {
        return getMappingNTXRequest(mappingFormat, mappingDefinitionId, null);
    }
    
    @PUT
    @Path("/mapping/ntxregistry/{registryLocation}")
    @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.APPLICATION_OCTET_STREAM})
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Save Mapping To Entaxy", description = "Save a mapping file on the Entaxy registry")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Succeeded"),
        @ApiResponse(responseCode = "500", description = "Mapping file save error")})
    public Response saveMappingToNTX(
      @Parameter(description = "Entaxy Registry Location") @PathParam("registryLocation") String registryLocation,
      @RequestBody(description = "Mapping file content") InputStream content) {
    	LOG.debug("Saving mapping file to Entaxy registry '{}'", registryLocation);
    	try {
    		InputStream targetStream = new ByteArrayInputStream(readFromStream(content));
        	EntaxyResource entaxyResource = entaxyResourceService.getResource(registryLocation);
        	entaxyResource.save(targetStream);
        	return Response.ok().build();
    	} catch (Exception e) {
    		LOG.error("Error while uploading file to Entaxy registry: " + e.getMessage(), e);
    		throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
        }
    }
    
    @Deprecated
    @PUT
    @Path("/mapping/{mappingFormat}")
    @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.APPLICATION_OCTET_STREAM})
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Create Mapping", description = "Save a mapping file on the server")
    @RequestBody(description = "Mapping file content", content = @Content(schema = @Schema(implementation = AtlasMapping.class)))
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Succeeded"),
        @ApiResponse(responseCode = "500", description = "Mapping file save error")})
    public Response createMappingRequestOld(InputStream mapping,
    @Parameter(description = "Mapping Format") @PathParam("mappingFormat") MappingFileType mappingFormat,
    @Context UriInfo uriInfo) {
        return createMappingRequest(mapping, mappingFormat, 0, uriInfo);
    }
    
    @PUT
    @Path("/mapping/{mappingFormat}/{mappingDefinitionId}")
    @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.APPLICATION_OCTET_STREAM})
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Create Mapping", description = "Save a mapping file on the server")
    @RequestBody(description = "Mapping file content", content = @Content(schema = @Schema(implementation = AtlasMapping.class)))
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Succeeded"),
        @ApiResponse(responseCode = "500", description = "Mapping file save error")})
    public Response createMappingRequest(InputStream mapping,
      @Parameter(description = "Mapping Format") @PathParam("mappingFormat") MappingFileType mappingFormat,
      @Parameter(description = "Mapping ID") @PathParam("mappingDefinitionId") Integer mappingDefinitionId,
      @Context UriInfo uriInfo) {
    	LOG.debug("createMappingRequest (save) with format '{}'", mappingFormat);
        UriBuilder builder = uriInfo.getAbsolutePathBuilder();
        ADMArchiveHandler admHandler = loadExplodedMappingDirectory(mappingDefinitionId);

        switch (mappingFormat) {
        case JSON:
            try {
                admHandler.setMappingDefinitionBytes(mapping);
                admHandler.persist();
                if (admHandler.getMappingDefinition() != null) {
                    builder.path(admHandler.getMappingDefinition().getName());
                }
            } catch (AtlasException e) {
                LOG.error("Error saving Mapping Definition file.\n" + e.getMessage(), e);
                throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
            }
            return Response.ok().location(builder.build()).build();
        case GZ:
            LOG.debug("  saveGzippedADMDigestRequest '{}' - ID: {}", admHandler.getGzippedADMDigestFileName(), mappingDefinitionId);
        	try {
                admHandler.setGzippedADMDigest(mapping);
                admHandler.persist();
            } catch (AtlasException e) {
                LOG.error("Error saving gzipped ADM digest file.\n" + e.getMessage(), e);
                throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
            }
            builder.path(admHandler.getGzippedADMDigestFileName());
            return Response.ok().location(builder.build()).build();
        case ZIP:
            LOG.debug("  importADMArchiveRequest - ID:'{}'", mappingDefinitionId);
            try {
                admHandler.setIgnoreLibrary(false);
                admHandler.setLibraryDirectory(Paths.get(libFolder));
                admHandler.load(mapping);
                this.libraryLoader.reload();
                admHandler.persist();
                LOG.debug("  importADMArchiveRequest complete - ID:'{}'", mappingDefinitionId);
            } catch (Exception e) {
                LOG.error("Error importing ADM archive.\n" + e.getMessage(), e);
                throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
            }
            builder.path("atlasmap-" + mappingDefinitionId + ".adm");
            return Response.ok().location(builder.build()).build();
        case XML:
            throw new WebApplicationException("XML mapping format is no longer supported. Please use JSON format instead.");
        default:
            throw new WebApplicationException("Unrecognized mapping format: " + mappingFormat, Status.INTERNAL_SERVER_ERROR);
        }
    }
    
    @Deprecated
    @POST
    @Path("/mapping")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Update Mapping", description = "Update existing mapping file on the server")
    @RequestBody(description = "Mapping file content", content = @Content(schema = @Schema(implementation = AtlasMapping.class)))
    @ApiResponses(@ApiResponse(responseCode = "200", description = "Succeeded"))
    public Response updateMappingRequestOld(
            InputStream mapping,
            @Context UriInfo uriInfo)
    {
        return updateMappingRequest(mapping, 0, uriInfo);
    }

    @POST
    @Path("/mapping/{mappingDefinitionId}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Update Mapping", description = "Update existing mapping file on the server")
    @RequestBody(description = "Mapping file content", content = @Content(schema = @Schema(implementation = AtlasMapping.class)))
    @ApiResponses(@ApiResponse(responseCode = "200", description = "Succeeded"))
    public Response updateMappingRequest(
            InputStream mapping,
            @Parameter(description = "Mapping Definition ID") @PathParam("mappingDefinitionId") Integer mappingDefinitionId,
            @Context UriInfo uriInfo) {
        ADMArchiveHandler handler = loadExplodedMappingDirectory(mappingDefinitionId);
        UriBuilder builder = uriInfo.getAbsolutePathBuilder();
        try {
            handler.setMappingDefinitionBytes(mapping);
            handler.persist();
            builder.path(handler.getMappingDefinition().getName());
        } catch (AtlasException e) {
            LOG.error("Error saving Mapping Definition file.\n" + e.getMessage(), e);
            throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
        }

        return Response.ok().location(builder.build()).build();
    }

    @Deprecated
    @PUT
    @Path("/mapping/validate")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Validate Mapping", description = "Validate mapping file")
    @RequestBody(description = "Mapping file content", content = @Content(schema = @Schema(implementation = AtlasMapping.class)))
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Validations.class)), description = "Return a validation result"))
    public Response validateMappingRequestOld(InputStream mapping,
                                           @Context UriInfo uriInfo)
    {
        return validateMappingRequest(mapping, 0, uriInfo);
    }

    @PUT
    @Path("/mapping/validate/{mappingDefinitionId}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Validate Mapping", description = "Validate mapping file")
    @RequestBody(description = "Mapping file content", content = @Content(schema = @Schema(implementation = AtlasMapping.class)))
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation =  Validations.class)), description = "Return a validation result"))
    public Response validateMappingRequest(InputStream mapping,
                                           @Parameter(description = "Mapping ID") @PathParam("mappingDefinitionId") Integer mappingDefinitionId,
                                           @Context UriInfo uriInfo) {
        try {
            AtlasMapping atlasMapping = fromJson(mapping, AtlasMapping.class);
            LOG.debug("Validate mappings: {}", atlasMapping.getName());
            return validateMapping(mappingDefinitionId, atlasMapping, uriInfo);
        } catch (AtlasException | IOException e) {
            throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
        }
    }

    @PUT
    @Path("/mapping/process")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Process Mapping", description = "Process Mapping by feeding input data")
    @RequestBody(description = "Mapping file content", content = @Content(schema = @Schema(implementation = AtlasMapping.class)))
    @ApiResponses({
        @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ProcessMappingResponse.class)), description = "Return a mapping result"),
        @ApiResponse(responseCode = "204", description = "Skipped empty mapping execution") })
    public Response processMappingRequest(InputStream request, @Context UriInfo uriInfo) {
        ProcessMappingRequest pmr = fromJson(request, ProcessMappingRequest.class);
        if (pmr.getAtlasMapping() != null) {
            throw new WebApplicationException("Whole mapping execution is not yet supported");
        }
        Mapping mapping = pmr.getMapping();
        if (mapping == null) {
            return Response.noContent().build();
        }
        Audits audits = null;
        try {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Preview request: {}", new String(toJson(mapping)));
            }
            audits = previewContext.processPreview(mapping);
        } catch (AtlasException e) {
            throw new WebApplicationException("Unable to process mapping preview", e);
        }
        ProcessMappingResponse response = new ProcessMappingResponse();
        response.setMapping(mapping);
        if (audits != null) {
            response.setAudits(audits);
        }
        byte[] serialized = toJson(response);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Preview outcome: {}", new String(serialized));
        }
        return Response.ok().entity(serialized).build();
    }

    @GET
    @Path("/ping")
    @Operation(summary = "Ping", description = "Simple liveness check method used in liveness checks. Must not be protected via authetication.")
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string")), description = "Return 'pong'"))
    public Response ping() {
        LOG.debug("Ping...  responding with 'pong'.");
        return Response.ok().entity(toJson("pong")).build();
    }

    @GET
    @Path("/version")
    @Operation(summary = "Version", description = "Retrieves AtlasMap core library version.")
    @ApiResponses(@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string")), description = "Return 'pong'"))
    public Response version() {
        String version = this.atlasContextFactory.getProperties().get(AtlasContextFactory.PROPERTY_ATLASMAP_CORE_VERSION);
        LOG.debug("Answering AtlasMap version: {}", version);
        return Response.ok().entity(toJson(version)).build();
    }

    @GET
    @Path("/library/list")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "List Library Classes",
        description = "Retrieves a list of available Java library class names from uploaded JARs.")
    @ApiResponses(@ApiResponse(
        responseCode = "200", content = @Content(schema = @Schema(type = "ArrayList<String>")),
        description = "Return a list of loadable class names"))
    public Response listLibraryClasses(@Context UriInfo uriInfo) {
        ArrayList<String> classNames;
        try {
            classNames = libraryLoader.getLibraryClassNames();
        } catch (Exception e) {
            if (LOG.isDebugEnabled()) {
                LOG.error("Library class retrieval error.", e);
            }
            throw new WebApplicationException("Error retrieving class names from uploaded JARs.");
        }
        byte[] serialized = toJson(classNames);
        if (LOG.isDebugEnabled()) {
            LOG.debug(new String(serialized));
        }
        return Response.ok().entity(serialized).build();
    }

    @PUT
    @Path("/library")
    @Operation(summary = "Upload Library", description = "Upload a Java library archive file")
    @Consumes({MediaType.APPLICATION_OCTET_STREAM})
    @ApiResponses(@ApiResponse(
            responseCode = "200", description = "Library upload successful."))
    public Response uploadLibrary(InputStream requestIn) {
        if (requestIn == null) {
            throw new WebApplicationException("No library file found in request body");
        }

        try {
            libraryLoader.addJarFromStream(requestIn);
        } catch (Exception e) {
            if (LOG.isDebugEnabled()) {
                LOG.error("", e);
            }
            StringBuilder buf = new StringBuilder();
            buf.append("Failed to import a jar file. This error occurs when:\n")
                .append(("\t1. The jar file is not compatible with the JVM AtlasMap backend server is running on\n"))
                .append("\t2. The jar file is broken\n")
                .append("\t3. There is a missing file under META-INF/services, i.e. Java service declaration for custom transformation, custom transformation model, custom mapping builder, etc\n");
            throw new WebApplicationException(buf.toString(), e);
        }
        return Response.ok().build();
    }

    @GET
    @Path("/mappingBuilders")
    @Operation(summary = "List mapping builder classes",
        description = "List mapping builder classes which defines custom mapping logic")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiResponses(@ApiResponse(
            responseCode = "200", content = @Content(schema = @Schema(type = "ArrayList<String>")),
            description = "Return a list of loadable class names"))
    public Response listMappingBuilderClasses(@Context UriInfo uriInfo) {
        ArrayList<String> classNames;
        try {
            classNames = libraryLoader.getSubTypesOf(AtlasMappingBuilder.class, false);
        } catch (Exception e) {
            if (LOG.isDebugEnabled()) {
                LOG.error("Library class retrieval error.", e);
            }
            throw new WebApplicationException("Error retrieving class names from uploaded JARs.");
        }
        byte[] serialized = toJson(classNames);
        if (LOG.isDebugEnabled()) {
            LOG.debug(new String(serialized));
        }
        return Response.ok().entity(serialized).build();
    }

    
    public AtlasLibraryLoader getLibraryLoader() {
        return this.libraryLoader;
    }

    protected Response validateMapping(Integer mappingDefinitionId, AtlasMapping mapping, UriInfo uriInfo) throws IOException, AtlasException {
        AtlasSession session;
        synchronized (atlasContextFactory) {
            AtlasContext context = atlasContextFactory.createContext(mapping);
            session = context.createSession();
            context.processValidation(session);
        }

        Validations validations = session.getValidations();
        if (session.getValidations() == null) {
            validations = new Validations();
        }

        return Response.ok().entity(toJson(validations)).build();
    }

    private byte[] toJson(Object value) {
        try {
            return Json.mapper().writeValueAsBytes(value);
        } catch (JsonProcessingException e) {
            throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
        }
    }

    private <T> T fromJson(InputStream value, Class<T>clazz) {
        try {
            if (LOG.isDebugEnabled()) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(value));
                StringBuffer buf = new StringBuffer();
                String line;
                while ((line = reader.readLine()) != null) {
                    buf.append(line);
                }
                LOG.debug(buf.toString());
                return Json.withClassLoader(this.libraryLoader).readValue(buf.toString(), clazz);
            }
            return Json.withClassLoader(this.libraryLoader).readValue(value, clazz);
        } catch (IOException e) {
            throw new WebApplicationException(e, Status.BAD_REQUEST);
        }
    }

    private String getMappingSubDirectory(Integer mappingDefinitionId) {
        return this.mappingFolder + File.separator + mappingDefinitionId;
    }

    private ADMArchiveHandler loadExplodedMappingDirectory(Integer mappingDefinitionId) {
        java.nio.file.Path mappingDirPath = Paths.get(getMappingSubDirectory(mappingDefinitionId));
        File mappingDirFile = mappingDirPath.toFile();
        if (!mappingDirFile.exists()) {
            mappingDirFile.mkdirs();
        }

        ADMArchiveHandler admHandler = new ADMArchiveHandler(this.libraryLoader);
        admHandler.setIgnoreLibrary(true);
        try {
            admHandler.load(mappingDirPath);
        } catch (Exception e) {
            LOG.error("Unexpected error while loading mapping directory.\n" + e.getMessage(), e);
            throw new WebApplicationException(e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);
        }
        return admHandler;
    }
    
	private byte[] readFromStream(InputStream stream) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();

		byte[] buffer = new byte[1000];
		int wasRead = 0;
		do {
			wasRead = stream.read(buffer);
			if (wasRead > 0) {
				baos.write(buffer, 0, wasRead);
			}
		} while (wasRead > -1);
		return baos.toByteArray();
	}

    private void setClassLoader() {
        BundleWiring bundleWiring = bundleContext.getBundle().adapt(BundleWiring.class);
        if (bundleWiring != null) {
            this.libraryLoader.addAlternativeLoader(bundleWiring.getClassLoader());
        } else {
            this.libraryLoader.addAlternativeLoader(Thread.currentThread().getContextClassLoader());
        }
    }

}
