/**
 * This file is part of Eclipse Steady.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * SPDX-FileCopyrightText: Copyright (c) 2018-2020 SAP SE or an SAP affiliate company and Eclipse Steady contributors
 */
package org.eclipse.steady.backend.rest;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceException;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.annotation.JsonView;

import org.eclipse.steady.backend.component.ApplicationExporter;
import org.eclipse.steady.backend.model.Application;
import org.eclipse.steady.backend.model.Bug;
import org.eclipse.steady.backend.model.ConstructId;
import org.eclipse.steady.backend.model.ConstructIdFilter;
import org.eclipse.steady.backend.model.ConstructSearchResult;
import org.eclipse.steady.backend.model.Dependency;
import org.eclipse.steady.backend.model.DependencyIntersection;
import org.eclipse.steady.backend.model.DependencyUpdate;
import org.eclipse.steady.backend.model.GoalExecution;
import org.eclipse.steady.backend.model.LibraryId;
import org.eclipse.steady.backend.model.Path;
import org.eclipse.steady.backend.model.PathNode;
import org.eclipse.steady.backend.model.Space;
import org.eclipse.steady.backend.model.Tenant;
import org.eclipse.steady.backend.model.TouchPoint;
import org.eclipse.steady.backend.model.Trace;
import org.eclipse.steady.backend.model.VulnerableDependency;
import org.eclipse.steady.backend.model.view.Views;
import org.eclipse.steady.backend.repo.AffectedLibraryRepository;
import org.eclipse.steady.backend.repo.ApplicationRepository;
import org.eclipse.steady.backend.repo.BugRepository;
import org.eclipse.steady.backend.repo.ConstructIdRepository;
import org.eclipse.steady.backend.repo.DependencyRepository;
import org.eclipse.steady.backend.repo.GoalExecutionRepository;
import org.eclipse.steady.backend.repo.LibraryRepository;
import org.eclipse.steady.backend.repo.PathRepository;
import org.eclipse.steady.backend.repo.SpaceRepository;
import org.eclipse.steady.backend.repo.TenantRepository;
import org.eclipse.steady.backend.repo.TracesRepository;
import org.eclipse.steady.backend.repo.V_AppVulndepRepository;
import org.eclipse.steady.backend.util.DependencyUtil;
import org.eclipse.steady.backend.util.Message;
import org.eclipse.steady.backend.util.ServiceWrapper;
import org.eclipse.steady.shared.connectivity.ServiceConnectionException;
import org.eclipse.steady.shared.enums.ConstructType;
import org.eclipse.steady.shared.enums.ExportFormat;
import org.eclipse.steady.shared.enums.GoalType;
import org.eclipse.steady.shared.enums.Scope;
import org.eclipse.steady.shared.enums.VulnDepOrigin;
import org.eclipse.steady.shared.json.model.diff.JarDiffResult;
import org.eclipse.steady.shared.json.model.metrics.Counter;
import org.eclipse.steady.shared.json.model.metrics.Metrics;
import org.eclipse.steady.shared.json.model.metrics.Ratio;
import org.eclipse.steady.shared.util.Constants;
import org.eclipse.steady.shared.util.StopWatch;
import org.eclipse.steady.shared.util.VulasConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.DispatcherServlet;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;

/**
 * RESTful interface for application information.
 */
@RestController
@CrossOrigin(origins = "*")
@RequestMapping(path = "/apps")
@OpenAPIDefinition(
    security = {@SecurityRequirement(name = "tenant"), @SecurityRequirement(name = "space")})
public class ApplicationController {

  private static Logger log = LoggerFactory.getLogger(ApplicationController.class);

  private final ApplicationRepository appRepository;

  private final DependencyRepository depRepository;

  private final GoalExecutionRepository gexeRepository;

  private final TracesRepository traceRepository;

  private final LibraryRepository libRepository;

  private final AffectedLibraryRepository affLibRepository;

  private final PathRepository pathRepository;

  private final BugRepository bugRepository;

  private final SpaceRepository spaceRepository;

  private final TenantRepository tenantRepository;

  private final ConstructIdRepository cidRepository;

  private final V_AppVulndepRepository appVulDepRepository;

  private final ApplicationExporter appExporter;

  private final Filter cacheFilter;

  /** Constant <code>SENDER_EMAIL="vulas.backend.smtp.sender"</code> */
  public static final String SENDER_EMAIL = "vulas.backend.smtp.sender";

  /** Constant <code>ALL_APPS_CSV_SUBJECT="vulas.backend.allApps.mailSubject"</code> */
  public static final String ALL_APPS_CSV_SUBJECT = "vulas.backend.allApps.mailSubject";

  /**
   * <p>dispatcherServlet.</p>
   *
   * @return a {@link org.springframework.web.servlet.DispatcherServlet} object.
   */
  @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet dispatcherServlet = new DispatcherServlet();
    dispatcherServlet.setDispatchOptionsRequest(true);
    return dispatcherServlet;
  }

  @Autowired
  ApplicationController(
      ApplicationRepository appRepository,
      GoalExecutionRepository gexeRepository,
      DependencyRepository depRepository,
      TracesRepository traceRepository,
      LibraryRepository libRepository,
      PathRepository pathRepository,
      BugRepository bugRepository,
      SpaceRepository tokenRepository,
      ConstructIdRepository cidRepository,
      AffectedLibraryRepository affLibRepository,
      TenantRepository tenantRepository,
      V_AppVulndepRepository appVulDepRepository,
      ApplicationExporter appExporter,
      Filter cacheFilter) {
    this.appRepository = appRepository;
    this.gexeRepository = gexeRepository;
    this.depRepository = depRepository;
    this.traceRepository = traceRepository;
    this.libRepository = libRepository;
    this.pathRepository = pathRepository;
    this.bugRepository = bugRepository;
    this.spaceRepository = tokenRepository;
    this.cidRepository = cidRepository;
    this.affLibRepository = affLibRepository;
    this.tenantRepository = tenantRepository;
    this.appVulDepRepository = appVulDepRepository;
    this.appExporter = appExporter;
    this.cacheFilter = cacheFilter;
  }

  // TODO: The space headers must become mandatory once we get (most of) users to switch to vulas3
  /**
   * <p>createApplication.</p>
   *
   * @param application a {@link org.eclipse.steady.backend.model.Application} object.
   * @param skipResponseBody a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   * @return a {@link org.springframework.http.ResponseEntity} object.
   */
  @RequestMapping(
      value = "",
      method = RequestMethod.POST,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<Application> createApplication(
      @RequestBody Application application,
      @RequestParam(value = "skipResponseBody", required = false, defaultValue = "false")
          Boolean skipResponseBody,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<Application>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    }

    if (!DependencyUtil.isValidDependencyCollection(application))
      return new ResponseEntity<Application>(HttpStatus.BAD_REQUEST);

    try {
      final String group = application.getMvnGroup();
      final String artifact = application.getArtifact();
      final String version = application.getVersion();

      ApplicationRepository.FILTER.findOne(
          this.appRepository.findByGAV(group, artifact, version, s));

      // Return CONFLICT to indicate that resource with this digest already exists
      return new ResponseEntity<Application>(HttpStatus.CONFLICT);
    } catch (EntityNotFoundException e) {
      try {
        application.setSpace(s);
        final Application app = this.appRepository.customSave(application);
        if (skipResponseBody) {
          return new ResponseEntity<Application>(HttpStatus.CREATED);
        } else return new ResponseEntity<Application>(app, HttpStatus.CREATED);
      } catch (PersistenceException e1) {
        return new ResponseEntity<Application>(HttpStatus.INTERNAL_SERVER_ERROR);
      }
    }
  }

  /**
   * Deletes multiple {@link Application} versions at once.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if bug with given digest does not
   *       exist, 422 {@link HttpStatus#UNPROCESSABLE_ENTITY} if the value of
   *       path variable (digest) does not equal the corresponding field in the
   *       body, 200 {@link HttpStatus#OK} if the library was successfully
   *       re-created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param keep a {@link java.lang.Integer} object.
   * @param mode a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}",
      method = RequestMethod.DELETE,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<Application>> purgeApplicationVersions(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @RequestParam(value = "keep", required = false, defaultValue = "3") Integer keep,
      @RequestParam(value = "mode", required = false, defaultValue = "versions") String mode,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<List<Application>>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Application>>(HttpStatus.NOT_FOUND);
    }

    try {
      // Check args
      if (keep < 0
          || keep > 100
          || mode == null
          || (!mode.equalsIgnoreCase("DAYS") && !mode.equalsIgnoreCase("VERSIONS"))) {
        log.error(
            "Invalid value for arg 'keep' (anything between 0 and 100, is ["
                + keep
                + "]) and/or arg 'mode' (should be DAYS or VERSIONS, is ["
                + mode
                + "])");
        return new ResponseEntity<List<Application>>(HttpStatus.BAD_REQUEST);
      }

      final List<Application> apps = this.appRepository.findByGA(mvnGroup, artifact, s);

      // None found, return 404
      if (apps == null || apps.isEmpty())
        return new ResponseEntity<List<Application>>(HttpStatus.NOT_FOUND);

      // Sort them after creation date
      final SortedSet<Application> sorted_apps =
          new TreeSet<Application>(
              new Comparator<Application>() {
                public int compare(Application a1, Application a2) {
                  return a1.getCreatedAt().compareTo(a2.getCreatedAt());
                }
              });
      sorted_apps.addAll(apps);

      // The deleted apps
      final List<Application> deleted_apps = new ArrayList<Application>();

      // Delete all except those from the last KEEP days
      if (mode.equalsIgnoreCase("DAYS")) {
        final long milli_treshold = System.currentTimeMillis() - (keep * 24 * 60 * 60 * 1000);
        for (Application app : sorted_apps) {
          if (app.getCreatedAt().getTime().getTime() < milli_treshold) {
            deleted_apps.add(app);
            try {
              this.appRepository.deleteAnalysisResults(app, true);
              this.appRepository.delete(app);
            } catch (Exception e) {
              log.error("Error while deleting app " + app + ": " + e.getMessage(), e);
            }
          }
        }
      }
      // Delete all except the last KEEP versions
      else {
        for (Application app : sorted_apps) {
          if (sorted_apps.size() - deleted_apps.size() > keep) {
            deleted_apps.add(app);
            try {
              this.appRepository.deleteAnalysisResults(app, true);
              this.appRepository.delete(app);
            } catch (Exception e) {
              log.error("Error while deleting app " + app + ": " + e.getMessage(), e);
            }
          }
        }
      }

      if (deleted_apps.size() > 0) {
        log.info("Deleted the following [" + deleted_apps.size() + "] apps:");
        for (Application app : deleted_apps) log.info("    " + app);
      }

      return new ResponseEntity<List<Application>>(deleted_apps, HttpStatus.OK);
    } catch (Exception e) {
      return new ResponseEntity<List<Application>>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  //	/**
  //	 * Re-creates the {@link Application} with a given GAV.
  //	 * @param digest
  //	 * @return 404 {@link HttpStatus#NOT_FOUND} if bug with given digest does not exist,
  //	 * 		   422 {@link HttpStatus.UNPROCESSABLE_ENTITY} if the value of path variable (digest) does
  // not equal the corresponding field in the body
  //	 *		   400 {@link HttpStatus.BAD_REQUEST} if the set of application dependencies is not valid
  // (contains duplicated entries or the parent are not listed in the main set)
  //	 * 		   200 {@link HttpStatus#OK} if the library was successfully re-created
  //	 */
  //	@RequestMapping(value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/lastscan", method =
  // RequestMethod.PUT, produces = {"application/json;charset=UTF-8"})
  //	@JsonView(Views.Default.class)
  //	public ResponseEntity<Application> updateApplicationLastScan(
  //			@PathVariable String mvnGroup, @PathVariable String artifact, @PathVariable String version,
  //			@RequestParam(value="skipResponseBody", required=false, defaultValue="false") Boolean
  // skipResponseBody,
  //			@ApiIgnore @RequestHeader(value=Constants.HTTP_SPACE_HEADER, required=false) String space) {
  //		Space s = null;
  //		try {
  //			s = this.spaceRepository.getSpace(space);
  //		} catch (Exception e){
  //			log.error("Error retrieving space: " + e);
  //			return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
  //		}
  //		try{
  //			final Application existing_app =
  // ApplicationRepository.FILTER.findOne(this.appRepository.findByGAV(mvnGroup, artifact, version,
  // s));
  //			existing_app.setLastScan(Calendar.getInstance());
  //			Application managed_app = appRepository.save(existing_app);
  //			if(skipResponseBody){
  //				return new ResponseEntity<Application>(HttpStatus.OK);
  //			}
  //			else
  //				return new ResponseEntity<Application>(managed_app, HttpStatus.OK);
  //		} catch (EntityNotFoundException e) {
  //			return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
  //		} catch (PersistenceException e) {
  //			return new ResponseEntity<Application>(HttpStatus.INTERNAL_SERVER_ERROR);
  //		}
  //	}

  /**
   * Re-creates the {@link Application} with a given GAV.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if bug with given digest does not exist,
   * 		   422 {@link HttpStatus#UNPROCESSABLE_ENTITY} if the value of path variable (digest) does not equal the corresponding field in the body
   *		   400 {@link HttpStatus#BAD_REQUEST} if the set of application dependencies is not valid (contains duplicated entries or the parent are not listed in the main set)
   * 		   200 {@link HttpStatus#OK} if the library was successfully re-created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param application a {@link org.eclipse.steady.backend.model.Application} object.
   * @param skipResponseBody a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}",
      method = RequestMethod.PUT,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<Application> updateApplication(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestBody Application application,
      @RequestParam(value = "skipResponseBody", required = false, defaultValue = "false")
          Boolean skipResponseBody,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<Application>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    }

    if (!DependencyUtil.isValidDependencyCollection(application))
      return new ResponseEntity<Application>(HttpStatus.BAD_REQUEST);

    try {
      // TODO: Ensure consistency of path variable and JSON content Check whether
      /*final String group = _app.getMvnGroup();
      final String artifact = _app.getArtifact();
      final String version = _app.getVersion();*/

      // Check whether it exists
      // TODO: Checking whether the application exists is also done inside customSave
      // to avoid performing the query twice we could propagate the EntityNotFoundException from
      // customSave )
      final Application existing_app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));

      // Remove all deps and app constructs
      // existing_app.clean();
      // this.appRepository.customSave(existing_app);

      // TODO: Is this the best way to pass the space to customSave?
      application.setSpace(s);

      // Save
      final Application managed_app = this.appRepository.customSave(application);
      if (skipResponseBody) {
        return new ResponseEntity<Application>(HttpStatus.OK);
      } else return new ResponseEntity<Application>(managed_app, HttpStatus.OK);
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    } catch (PersistenceException e) {
      return new ResponseEntity<Application>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * <p>getApplications.</p>
   *
   * @return sorted set of all {@link Application}s of the respective tenant and space
   * @param skipEmpty a {@link java.lang.Boolean} object.
   * @param g a {@link java.lang.String} object.
   * @param a a {@link java.lang.String} object.
   * @param v a {@link java.lang.String} object.
   * @param asOfTimestamp a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<Collection<Application>> getApplications(
      @RequestParam(value = "skipEmpty", required = false, defaultValue = "false")
          Boolean skipEmpty,
      //			@RequestParam(value="includeVulnerableFlag", required=false, defaultValue="false")
      // Boolean includeVulnerableFlag,
      @RequestParam(value = "group", required = false, defaultValue = "*") String g,
      @RequestParam(value = "artifact", required = false, defaultValue = "*") String a,
      @RequestParam(value = "version", required = false, defaultValue = "*") String v,
      @RequestParam(value = "asOf", required = false, defaultValue = "0") String asOfTimestamp,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Collection<Application>>(HttpStatus.NOT_FOUND);
    }

    try {
      if (g.equals("*") && a.equals("*") && v.equals("*")) {
        Collection<Application> results =
            this.appRepository.getApplications(
                skipEmpty, s.getSpaceToken(), Long.parseLong(asOfTimestamp));
        //				if(includeVulnerableFlag){
        //					if(s.isDefault())
        //						return new ResponseEntity<Collection<Application>>(HttpStatus.BAD_REQUEST);
        //					results = new ArrayList<Application>();
        //					for(Application app : all){
        //						if
        // (this.appVulDepRepository.isAppVulnerableCC(space,app.getMvnGroup(),app.getArtifact(),app.getVersion())){
        //							app.setHasVulnerabilities(true);
        //						}
        //						else if
        // (this.appVulDepRepository.isAppVulnerableConfig(space,app.getMvnGroup(),app.getArtifact(),app.getVersion())){
        //							app.setHasVulnerabilities(true);
        //						}
        //						else
        //							app.setHasVulnerabilities(false);
        //						results.add(app);
        //					}
        //				}
        //				else {
        //					results = all;
        //				}
        return new ResponseEntity<Collection<Application>>(results, HttpStatus.OK);
      } else {
        String search_string_g = g.replace('*', '%');
        String search_string_a = a.replace('*', '%');
        String search_string_v = v.replace('*', '%');

        // Perform the search
        Collection<Application> search =
            appRepository.searchByGAV(search_string_g, search_string_a, search_string_v);
        Collection<Application> result = null;
        if (skipEmpty) {
          result = new ArrayList<Application>();
          // TODO 16-03-2018: this search was implemented in refactoring2 to enable the pull from
          // SVM. To check whther and how it works with spaces
          Collection<Application> apps =
              this.appRepository.getApplications(
                  skipEmpty, s.getSpaceToken(), Long.parseLong(asOfTimestamp));
          for (Application f : search) if (apps.contains(f)) result.add(f);
        } else result = search;
        return new ResponseEntity<Collection<Application>>(result, HttpStatus.OK);
      }
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<Collection<Application>>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Compiles a list of all {@link Application}s of the respective {@link Tenant}, which is either sent by email (as attachment) or returned as part of the HTTP response.
   *
   * @param separator a {@link java.lang.String} object.
   * @param includeSpaceProperties an array of {@link java.lang.String} objects.
   * @param includeGoalConfiguration an array of {@link java.lang.String} objects.
   * @param includeGoalSystemInfo an array of {@link java.lang.String} objects.
   * @param vuln an array of {@link java.lang.String} objects.
   * @param to an array of {@link java.lang.String} objects.
   * @param format a {@link java.lang.String} object.
   * @param tenant a {@link java.lang.String} object.
   * @param request a {@link javax.servlet.http.HttpServletRequest} object.
   * @param response a {@link javax.servlet.http.HttpServletResponse} object.
   */
  @RequestMapping(value = "/export", method = RequestMethod.GET)
  public void exportApplications(
      @RequestParam(value = "separator", required = false, defaultValue = ";")
          final String separator,
      @RequestParam(value = "includeSpaceProperties", required = false, defaultValue = "")
          final String[] includeSpaceProperties,
      @RequestParam(value = "includeGoalConfiguration", required = false, defaultValue = "")
          final String[] includeGoalConfiguration,
      @RequestParam(value = "includeGoalSystemInfo", required = false, defaultValue = "")
          final String[] includeGoalSystemInfo,
      @RequestParam(value = "vuln", required = false, defaultValue = "") final String[] vuln,
      @RequestParam(value = "to", required = false, defaultValue = "") final String[] to,
      @RequestParam(value = "format", required = false, defaultValue = "csv") final String format,
      @RequestHeader(value = Constants.HTTP_TENANT_HEADER, required = false) final String tenant,
      HttpServletRequest request,
      HttpServletResponse response) {

    // Get the tenant
    Tenant t = null;
    try {
      if (tenant != null && !tenant.equals(""))
        t = TenantRepository.FILTER.findOne(this.tenantRepository.findBySecondaryKey(tenant));
      else t = tenantRepository.findDefault();
    } catch (EntityNotFoundException enfe) {
      log.error("Tenant [" + tenant + "] not found");
      throw new RuntimeException("Tenant [" + tenant + "] not found");
    }

    // Export format
    final ExportFormat exp_format = ExportFormat.parseFormat(format, ExportFormat.CSV);

    // Send export per email
    if (to != null && to.length > 0) {
      try {
        final String req = request.getQueryString();

        // Build mesage
        final Message msg = new Message();
        msg.setSender(VulasConfiguration.getGlobal().getConfiguration().getString(SENDER_EMAIL));
        msg.setSubject(
            VulasConfiguration.getGlobal().getConfiguration().getString(ALL_APPS_CSV_SUBJECT));
        msg.setBody(req);
        for (String recipient : to) msg.addRecipient(recipient);

        // Write apps to CSV and send email (async)
        this.appExporter.produceExportAsync(
            t,
            null,
            separator,
            includeSpaceProperties,
            includeGoalConfiguration,
            includeGoalSystemInfo,
            vuln,
            false,
            false,
            exp_format,
            msg);

        // Short response
        response.setContentType(ExportFormat.TXT_PLAIN);
        final BufferedWriter writer =
            new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
        writer.write("Result of request will be sent by email to the specified recipients");
        writer.newLine();
        writer.flush();
        response.flushBuffer();
      } catch (IllegalStateException e) {
        log.error(e.getMessage());
        throw new RuntimeException(e.getMessage());
      } catch (FileNotFoundException e) {
        log.error(
            "Error while reading all tenant apps (as [" + exp_format + "]): " + e.getMessage(), e);
        throw new RuntimeException("IOError writing file to output stream");
      } catch (IOException e) {
        log.error(
            "Error while reading all tenant apps (as [" + exp_format + "]): " + e.getMessage(), e);
        throw new RuntimeException("IOError writing file to output stream");
      } catch (Exception e) {
        log.error(
            "Error while reading all tenant apps (as [" + exp_format + "]): " + e.getMessage(), e);
        throw new RuntimeException("IOError writing file to output stream");
      }
    }
    // Write to response
    else {
      try {
        // Write apps to CSV
        final java.nio.file.Path csv =
            this.appExporter.produceExport(
                t,
                null,
                separator,
                includeSpaceProperties,
                includeGoalConfiguration,
                includeGoalSystemInfo,
                vuln,
                false,
                false,
                exp_format);

        // Headers
        response.setContentType(ExportFormat.getHttpContentType(exp_format));
        response.setHeader(
            "Content-Disposition", "attachment; filename=" + csv.getFileName().toString());
        final BufferedReader reader =
            new BufferedReader(new InputStreamReader(new FileInputStream(csv.toFile())));
        String line = null;
        final BufferedWriter writer =
            new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
        while ((line = reader.readLine()) != null) {
          writer.write(line);
          writer.newLine();
          writer.flush();
        }

        // Finish up
        reader.close();
        writer.flush();
        response.flushBuffer();
      } catch (FileNotFoundException e) {
        log.error(
            "Error while reading all tenant apps (as [" + exp_format + "]): " + e.getMessage(), e);
        throw new RuntimeException("IOError writing file to output stream");
      } catch (IOException e) {
        log.error(
            "Error while reading all tenant apps (as [" + exp_format + "]): " + e.getMessage(), e);
        throw new RuntimeException("IOError writing file to output stream");
      } catch (Exception e) {
        log.error(
            "Error while reading all tenant apps (as [" + exp_format + "]): " + e.getMessage(), e);
        throw new RuntimeException("IOError writing file to output stream");
      }
    }
  }

  /**
   * Cleans an {@link Application} so that it does not have any of the following:
   * {@link ConstructId}s, {@link Dependency}s, {@link Trace}s, to be continued
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param clean a {@link java.lang.Boolean} object.
   * @param cleanGoalHistory a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}",
      method = RequestMethod.POST,
      produces = {"application/json;charset=UTF-8"})
  public ResponseEntity<Application> cleanApplication(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestParam(value = "clean", required = true) Boolean clean,
      @RequestParam(value = "cleanGoalHistory", required = false, defaultValue = "false")
          Boolean cleanGoalHistory,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<Application>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    }

    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    }

    if (clean) {
      // Delete traces, paths and goal history
      this.appRepository.deleteAnalysisResults(app, cleanGoalHistory);

      // Remove all deps and app constructs
      app.clean();
      this.appRepository.customSave(app);
    } else {
      log.warn("Parameter [clean] is set to [false], application " + app + " will not be cleaned");
    }

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

  /**
   * Searches for {@link ConstructId}s in all of the {@link Application}'s dependencies.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param searchString a {@link java.lang.String} object.
   * @param constructTypes an array of {@link org.eclipse.steady.shared.enums.ConstructType} objects.
   * @param wildcardSearch a boolean.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/search",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<Set<ConstructSearchResult>> searchConstructsInAppDependencies(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestParam(value = "searchString", required = true, defaultValue = "") String searchString,
      @RequestParam(value = "constructTypes", required = false, defaultValue = "")
          ConstructType[] constructTypes,
      @RequestParam(value = "wildcardSearch", required = false, defaultValue = "true")
          boolean wildcardSearch,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Set<ConstructSearchResult>>(HttpStatus.NOT_FOUND);
    }

    Application app = null;
    try {
      // Ensure that app exists
      try {
        app =
            ApplicationRepository.FILTER.findOne(
                this.appRepository.findByGAV(mvnGroup, artifact, version, s));
      } catch (EntityNotFoundException e) {
        return new ResponseEntity<Set<ConstructSearchResult>>(HttpStatus.NOT_FOUND);
      }

      // Search string must be at least 4 chars long
      if (searchString == null || searchString.length() <= 3) {
        log.error("Search string must be at least 3 characters long");
        return new ResponseEntity<Set<ConstructSearchResult>>(HttpStatus.BAD_REQUEST);
      }

      // Types
      ConstructType[] types = null;
      if (constructTypes == null || constructTypes.length == 0) {
        types = ConstructType.getAllAsArray();
      } else {
        types = constructTypes;
      }

      String search_string = searchString;
      // Wildcard search
      if (wildcardSearch) search_string = search_string.replace('*', '%');

      // Perform the search
      Set<ConstructSearchResult> result =
          appRepository.searchDepConstructs(
              app.getMvnGroup(),
              app.getArtifact(),
              app.getVersion(),
              app.getSpace(),
              types,
              search_string);

      return new ResponseEntity<Set<ConstructSearchResult>>(result, HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<Set<ConstructSearchResult>>(HttpStatus.NOT_FOUND);
    } catch (Exception e) {
      log.error(
          "Error while searching for constructs in dependencies of app "
              + app
              + ": "
              + e.getMessage());
      return new ResponseEntity<Set<ConstructSearchResult>>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * <p>isApplicationExisting.</p>
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}",
      method = RequestMethod.OPTIONS)
  public ResponseEntity<Application> isApplicationExisting(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    }

    try {
      ApplicationRepository.FILTER.findOne(
          this.appRepository.findByGAV(mvnGroup, artifact, version, s));
      return new ResponseEntity<Application>(HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Returns the {@link Application} with the given Group Artifact Version (GAV).
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param inclTraces a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.CountDetails.class)
  public ResponseEntity<Application> getApplication(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestParam(value = "inclTraces", required = false, defaultValue = "true")
          Boolean inclTraces,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    }
    try {
      final Application app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));

      // TODO: Not including the traces does not significantly improve the perf, what else?
      if (inclTraces) {
        final List<Trace> traces = this.traceRepository.findByApp(app);
        if (!traces.isEmpty()) app.setTraces(traces);
        for (Dependency dep : app.getDependencies()) {
          dep.setTraces(this.traceRepository.findTracesOfLibrary(app, dep.getLib()));
        }
      }

      return new ResponseEntity<Application>(app, HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * <p>getApplicationConstructIds.</p>
   *
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   * @return a {@link org.springframework.http.ResponseEntity} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/constructIds",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  public ResponseEntity<Collection<ConstructId>> getApplicationConstructIds(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Collection<ConstructId>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<Collection<ConstructId>>(HttpStatus.NOT_FOUND);
    }

    // Return
    return new ResponseEntity<Collection<ConstructId>>(app.getConstructs(), HttpStatus.OK);
  }

  /**
   * <p>createGoalExecution.</p>
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application group artifact version of the goal execution is do not exist, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param goalExecution a {@link org.eclipse.steady.backend.model.GoalExecution} object.
   * @param skipResponseBody a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/goals",
      method = RequestMethod.POST,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  public ResponseEntity<GoalExecution> createGoalExecution(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestBody GoalExecution goalExecution,
      @RequestParam(value = "skipResponseBody", required = false, defaultValue = "false")
          Boolean skipResponseBody,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<GoalExecution>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }

    // Ensure consistency of path variable and JSON content
    // if(!app.equalsIgnoringSpace(goalExecution.getApp()))
    //	return new ResponseEntity<GoalExecution>(HttpStatus.UNPROCESSABLE_ENTITY);

    // Does it already exist (maybe upload is called multiple times on the same files)
    GoalExecution gexe = null;
    try {
      gexe =
          GoalExecutionRepository.FILTER.findOne(
              this.gexeRepository.findByAppGoalStartedAtClient(
                  app, goalExecution.getGoal(), goalExecution.getStartedAtClient()));
    } catch (EntityNotFoundException e) {
      gexe = this.gexeRepository.customSave(app, goalExecution);
    }

    // Save and return
    if (skipResponseBody) return new ResponseEntity<GoalExecution>(HttpStatus.CREATED);
    else return new ResponseEntity<GoalExecution>(gexe, HttpStatus.CREATED);
  }

  /**
   * <p>updateGoalExecution.</p>
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application group artifact version of the goal execution is do not exist, 200 {@link HttpStatus#OK} if the goal execution was successfully updates
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param executionId a {@link java.lang.String} object.
   * @param goalExecution a {@link org.eclipse.steady.backend.model.GoalExecution} object.
   * @param skipResponseBody a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/goals/{executionId}",
      method = RequestMethod.PUT,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  public ResponseEntity<GoalExecution> updateGoalExecution(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String executionId,
      @RequestBody GoalExecution goalExecution,
      @RequestParam(value = "skipResponseBody", required = false, defaultValue = "false")
          Boolean skipResponseBody,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<GoalExecution>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }

    try {
      GoalExecutionRepository.FILTER.findOne(this.gexeRepository.findByExecutionId(executionId));
      GoalExecution managed_gexe = this.gexeRepository.customSave(app, goalExecution);

      if (skipResponseBody) return new ResponseEntity<GoalExecution>(HttpStatus.OK);
      else return new ResponseEntity<GoalExecution>(managed_gexe, HttpStatus.OK);
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * <p>isGoalExecutionExisting.</p>
   *
   * @return  404 {@link HttpStatus#NOT_FOUND} if application with given GAV or the given executionId do not exist, 200 {@link HttpStatus#OK} if the executionId for the given Application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param executionId a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/goals/{executionId}",
      method = RequestMethod.OPTIONS)
  public ResponseEntity<GoalExecution> isGoalExecutionExisting(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String executionId,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }

    try {
      GoalExecutionRepository.FILTER.findOne(this.gexeRepository.findByExecutionId(executionId));
      return new ResponseEntity<GoalExecution>(HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Returns the {@link GoalExecution} for the given {@link Application} and
   * having the given identifier.
   *
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param id a {@link java.lang.Long} object.
   * @param space a {@link java.lang.String} object.
   * @return a {@link org.springframework.http.ResponseEntity} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/goals/{id}",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.GoalDetails.class)
  public ResponseEntity<GoalExecution> getGoalExecution(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable Long id,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    try {
      final Application app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }

    // Ensure that goal execution exists
    final GoalExecution gexe = this.gexeRepository.findById(id).orElse(null);
    if (gexe == null) return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);

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

  /**
   * Returns the latest {@link GoalExecution} for the given {@link Application}
   * and having the given {@link GoalType}.
   *
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param type a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   * @return a {@link org.springframework.http.ResponseEntity} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/goals/latest",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.GoalDetails.class)
  public ResponseEntity<GoalExecution> getLatestGoalExecution(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestParam(value = "type", required = false, defaultValue = "") String type,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }

    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);
    }

    GoalType gt = null;
    try {
      gt = (type == null || type.equals("") ? null : GoalType.parseGoal(type));
    } catch (IllegalArgumentException e) {
      log.error("Illegal goal type: " + type);
      return new ResponseEntity<GoalExecution>(HttpStatus.BAD_REQUEST);
    }

    final GoalExecution gexe = this.gexeRepository.findLatestGoalExecution(app, gt);
    if (gexe == null) return new ResponseEntity<GoalExecution>(HttpStatus.NOT_FOUND);

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

  /**
   * <p>getGoalExecutions.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/goals",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<GoalExecution>> getGoalExecutions(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<GoalExecution>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<GoalExecution>>(HttpStatus.NOT_FOUND);
    }

    // Save and return
    return new ResponseEntity<List<GoalExecution>>(
        this.gexeRepository.findByApp(app), HttpStatus.OK);
  }

  /**
   * <p>deleteGoalExecutions.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/goals",
      method = RequestMethod.DELETE)
  public ResponseEntity<List<GoalExecution>> deleteGoalExecutions(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<List<GoalExecution>>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<GoalExecution>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<GoalExecution>>(HttpStatus.NOT_FOUND);
    }

    // Save and return
    List<GoalExecution> gexes = this.gexeRepository.findByApp(app);
    for (GoalExecution gexe : gexes) {
      try {
        this.gexeRepository.delete(gexe);
      } catch (Exception e) {
        log.error("Error while deleting goal execution " + gexe + ": " + e.getMessage());
      }
    }
    return new ResponseEntity<List<GoalExecution>>(HttpStatus.OK);
  }
  /**
   * Returns a collection of {@link Bug}s relevant for the {@link Application} with the given GAV.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param historical a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/bugs",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.BugDetails.class)
  public ResponseEntity<List<Bug>> getApplicationBugs(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestParam(value = "historical", required = false, defaultValue = "false")
          Boolean historical,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Bug>>(HttpStatus.NOT_FOUND);
    }
    try {
      // To throw an exception if the entity is not found
      ApplicationRepository.FILTER.findOne(
          this.appRepository.findByGAV(mvnGroup, artifact, version, s));
      if (historical == true)
        return new ResponseEntity<List<Bug>>(
            this.appRepository.findBugsByGAV(mvnGroup, artifact, version, s), HttpStatus.OK);
      else {
        List<Bug> bugs = new ArrayList<Bug>();
        TreeSet<VulnerableDependency> vd_list =
            this.appRepository.findJPQLVulnerableDependenciesByGAV(mvnGroup, artifact, version, s);
        this.affLibRepository.computeAffectedLib(vd_list);
        for (VulnerableDependency vd : vd_list) {
          if (vd.getAffectedVersion() == 1) bugs.add(vd.getBug());
        }
        return new ResponseEntity<List<Bug>>(bugs, HttpStatus.OK);
      }

    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<List<Bug>>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Returns a collection of {@link DependencyIntersection}s for the {@link Application} with the given GAV.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps/intersect",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<DependencyIntersection>> findDependencyIntersections(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<DependencyIntersection>>(HttpStatus.NOT_FOUND);
    }
    try {
      final StopWatch sw =
          new StopWatch(
                  "Find dependency intersections for application: "
                      + mvnGroup
                      + ":"
                      + artifact
                      + ":"
                      + version)
              .start();

      // Throws an exception if the entity is not found
      final Application a =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));

      final List<DependencyIntersection> is =
          this.depRepository.findDepIntersections(
              a.getMvnGroup(), a.getArtifact(), a.getVersion(), s);

      sw.stop();
      return new ResponseEntity<List<DependencyIntersection>>(is, HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<List<DependencyIntersection>>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Returns a collection of {@link Dependency} for the {@link Application} with the given GAV.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<Dependency>> getDependencies(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Dependency>>(HttpStatus.NOT_FOUND);
    }
    try {
      final StopWatch sw =
          new StopWatch(
                  "Query dependencies for application: "
                      + mvnGroup
                      + ":"
                      + artifact
                      + ":"
                      + version)
              .start();

      final List<Dependency> deps = depRepository.findByGAV(mvnGroup, artifact, version, s);

      for (Dependency dep : deps) {
        dep.setTotalTracedExecConstructCount(
            this.traceRepository.countTracesOfExecConstructLibrary(
                dep.getApp(), dep.getLib().getDigest()));
        dep.setTotalReachExecConstructCount(
            this.cidRepository.countReachableExecConstructsLibrary(dep));
      }
      sw.stop();
      return new ResponseEntity<List<Dependency>>(deps, HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<List<Dependency>>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Returns a collection of {@link Bug}s relevant for the {@link Application} with the given GAV.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param digest a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps/{digest}",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.DepDetails.class) // extends View LibDetails that allows to see the properties
  public ResponseEntity<Dependency> getDependency(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String digest,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Dependency>(HttpStatus.NOT_FOUND);
    }
    try {
      // Throws an exception if the entity is not found
      final Application a =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));

      final Dependency dep = a.getDependency(digest);
      if (dep == null) {
        log.error(
            "App "
                + a.toString()
                + " has no dependency with digest ["
                + digest
                + "]: No bugs will be returned");
        return new ResponseEntity<Dependency>(HttpStatus.NOT_FOUND);
      }
      dep.setTraces(this.traceRepository.findTracesOfLibrary(a, dep.getLib()));

      return new ResponseEntity<Dependency>(dep, HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<Dependency>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Provides application-specific metrics regarding application size and the size of all application dependencies.
   * Requires that the given dependency has a valid library Id.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param excludedScopes an array of {@link org.eclipse.steady.shared.enums.Scope} objects.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/metrics",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  public ResponseEntity<Metrics> getApplicationMetrics(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestParam(value = "excludedScopes", required = false, defaultValue = "")
          Scope[] excludedScopes,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Metrics>(HttpStatus.NOT_FOUND);
    }
    try {
      // Throws an exception if the entity is not found
      final Application a =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
      final ConstructIdFilter af = a.countConstructTypes();

      final Metrics metrics = new Metrics();

      // Packages
      final Ratio packages = new Ratio("package_ratio", af.countPack(), af.countPack());
      int count_packages;
      if (excludedScopes == null || excludedScopes.length == 0)
        count_packages =
            this.appRepository.countDepConstructTypes(
                a.getMvnGroup(),
                a.getArtifact(),
                a.getVersion(),
                a.getSpace(),
                new ConstructType[] {ConstructType.PACK});
      else
        count_packages =
            this.appRepository.countDepConstructTypes(
                a.getMvnGroup(),
                a.getArtifact(),
                a.getVersion(),
                a.getSpace(),
                new ConstructType[] {ConstructType.PACK},
                excludedScopes);
      packages.incrementTotal(count_packages);
      metrics.addRatio(packages);

      // Classes
      final Ratio classes = new Ratio("class_ratio", af.countClass(), af.countClass());
      int count_classes;
      if (excludedScopes == null || excludedScopes.length == 0)
        count_classes =
            this.appRepository.countDepConstructTypes(
                a.getMvnGroup(),
                a.getArtifact(),
                a.getVersion(),
                a.getSpace(),
                new ConstructType[] {ConstructType.CLAS});
      else
        count_classes =
            this.appRepository.countDepConstructTypes(
                a.getMvnGroup(),
                a.getArtifact(),
                a.getVersion(),
                a.getSpace(),
                new ConstructType[] {ConstructType.CLAS},
                excludedScopes);
      classes.incrementTotal(count_classes);
      metrics.addRatio(classes);

      // Executable constructs (METH, CONS, INIT)
      final Ratio execs = new Ratio("executable_ratio", af.countExecutable(), af.countExecutable());
      int count_execs;
      if (excludedScopes == null || excludedScopes.length == 0)
        count_execs =
            this.appRepository.countDepConstructTypes(
                a.getMvnGroup(),
                a.getArtifact(),
                a.getVersion(),
                a.getSpace(),
                new ConstructType[] {ConstructType.INIT, ConstructType.CONS, ConstructType.METH});
      else
        count_execs =
            this.appRepository.countDepConstructTypes(
                a.getMvnGroup(),
                a.getArtifact(),
                a.getVersion(),
                a.getSpace(),
                new ConstructType[] {ConstructType.INIT, ConstructType.CONS, ConstructType.METH},
                excludedScopes);
      execs.incrementTotal(count_execs);
      metrics.addRatio(execs);

      // Number of app dependencies
      final Counter dep_counter = new Counter("dep_counter");
      int count_deps;
      if (excludedScopes == null || excludedScopes.length == 0)
        count_deps =
            this.appRepository.countDependencies(
                a.getMvnGroup(), a.getArtifact(), a.getVersion(), a.getSpace());
      else
        count_deps =
            this.appRepository.countDependencies(
                a.getMvnGroup(), a.getArtifact(), a.getVersion(), a.getSpace(), excludedScopes);
      dep_counter.increment(count_deps);
      metrics.addCounter(dep_counter);

      return new ResponseEntity<Metrics>(metrics, HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<Metrics>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Provides application-specific metrics related to updating the current dependency to the provided one.
   * Requires that the given dependency has a valid library Id.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param digest a {@link java.lang.String} object.
   * @param otherVersion a {@link org.eclipse.steady.backend.model.LibraryId} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps/{digest}/updateMetrics",
      method = RequestMethod.POST,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.DepDetails.class) // extends View LibDetails that allows to see the properties
  public ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate> getUpdateMetrics(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String digest,
      @RequestBody LibraryId otherVersion,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
          HttpStatus.NOT_FOUND);
    }

    try {

      // To throw an exception if the entity is not found
      final Application a =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));

      final Dependency dep = a.getDependency(digest);
      if (dep == null) {
        log.error(
            "App "
                + a.toString()
                + " has no dependency with digest ["
                + digest
                + "]: No update metrics will be returned");
        return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
            HttpStatus.NOT_FOUND);
      }

      // Pre-requisite: The dependency has to have a library id known to Maven
      if (dep.getLib().getLibraryId() == null) {
        log.error(
            "App " + a.toString() + " dependency with digest [" + digest + "] has no library id");
        return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
            HttpStatus.BAD_REQUEST);
      }

      dep.setTraces(this.traceRepository.findTracesOfLibrary(a, dep.getLib()));
      final JarDiffResult jdr =
          ServiceWrapper.getInstance()
              .diffJars(dep.getLib().getLibraryId().toSharedType(), otherVersion.toSharedType());
      final org.eclipse.steady.shared.json.model.metrics.Metrics metrics =
          new org.eclipse.steady.shared.json.model.metrics.Metrics();
      final DependencyUpdate depUpdate =
          new DependencyUpdate(dep.getLib().getLibraryId(), otherVersion);
      //			final org.eclipse.steady.backend.model.DependencyUpdate depUpdate = new
      // org.eclipse.steady.backend.model.DependencyUpdate(
      //																							new
      // com.sap.psr.vulas.shared.json.model.LibraryId(dep.getLib().getLibraryId().getMvnGroup(),dep.getLib().getLibraryId().getArtifact(),dep.getLib().getLibraryId().getVersion()),
      //																									new
      // com.sap.psr.vulas.shared.json.model.LibraryId(otherVersion.getMvnGroup(),otherVersion.getArtifact(),otherVersion.getVersion()));
      //

      // Metrics related to touch points (from traces or static analysis)
      final Collection<TouchPoint> touch_points = dep.getTouchPoints();

      if (touch_points != null) {

        final Ratio ratio_callees_deleted = new Ratio("callees_deleted");
        final Ratio api_stability = new Ratio("callee_stability"); // 1-callees_deleted
        final Ratio ratio_callers_to_modify = new Ratio("calls_to_modify");

        Set<ConstructId> callees = new HashSet<ConstructId>();
        Set<String> calls = new HashSet<String>();
        Set<ConstructId> deleted_callees = new HashSet<ConstructId>();

        for (TouchPoint tp : touch_points) {
          // Only A2L is relevant, as only this direction requires modification at app side
          if (tp.getDirection() == TouchPoint.Direction.A2L) {
            callees.add(tp.getTo());
            if (!calls.contains(tp.getFrom().toString().concat(tp.getTo().toString()))) {
              calls.add(tp.getFrom().toString().concat(tp.getTo().toString()));
              ratio_callers_to_modify.incrementTotal();
              if (jdr.isDeleted(tp.getTo().toSharedType())) {
                deleted_callees.add(tp.getTo());
                ratio_callers_to_modify.incrementCount();
                log.debug(
                    "Touch point callee "
                        + tp.getTo().toString()
                        + " deleted from "
                        + dep.getLib().getLibraryId().toString()
                        + " to "
                        + otherVersion);
              }
            }
          }
        }

        ratio_callees_deleted.setCount(deleted_callees.size());
        ratio_callees_deleted.setTotal(callees.size());

        api_stability.setCount(callees.size() - deleted_callees.size());
        api_stability.setTotal(callees.size());

        metrics.addRatio(api_stability);
        metrics.addRatio(ratio_callees_deleted);
        metrics.addRatio(ratio_callers_to_modify);
      }

      // Metrics related to reachable and traced constructs
      final Set<ConstructId> reachable_cids = new HashSet<ConstructId>();
      reachable_cids.addAll(dep.getReachableConstructIds());
      reachable_cids.addAll(dep.getTracedConstructs());
      if (reachable_cids != null) {

        final Ratio reachable_deleted = new Ratio("reachable_deleted", 0, reachable_cids.size());
        final Ratio reachable_body_changed =
            new Ratio("reachable_body_changed", 0, reachable_cids.size());
        final Ratio reachable_body_stability =
            new Ratio("reachable_body_stability", 0, reachable_cids.size());

        for (ConstructId cid : reachable_cids) {
          if (jdr.isDeleted(cid.toSharedType())) {
            reachable_deleted.incrementCount();
            log.debug(
                "Reachable construct "
                    + cid.toString()
                    + " deleted from "
                    + dep.getLib().getLibraryId().toString()
                    + " to "
                    + otherVersion);
          }
          if (jdr.isBodyChanged(cid.toSharedType())) {
            reachable_body_changed.incrementCount();
            log.debug(
                "Reachable construct "
                    + cid.toString()
                    + " changed from "
                    + dep.getLib().getLibraryId().toString()
                    + " to "
                    + otherVersion);
          }
        }
        reachable_body_stability.setCount(
            reachable_body_changed.getTotal() - reachable_body_changed.getCount());
        reachable_body_stability.setTotal(reachable_body_changed.getTotal());
        metrics.addRatio(reachable_body_stability);
        metrics.addRatio(reachable_deleted);
        metrics.addRatio(reachable_body_changed);
      }

      // App-independent metrics
      final List<ConstructId> old_constructs = this.libRepository.findConstructIds(digest);
      final Ratio ratio_constructs_body_stability = new Ratio("jar_constructs_body_stability");
      final Ratio ratio_constructs_deleted = new Ratio("jar_constructs_deleted");
      final Ratio ratio_constructs_body_changed = new Ratio("jar_constructs_body_changed");
      for (ConstructId c : old_constructs) {
        // Only consider METH and CONS
        if (c.getType().equals(ConstructType.CONS) || c.getType().equals(ConstructType.METH)) {
          ratio_constructs_deleted.incrementTotal();
          ratio_constructs_body_stability.incrementTotal();
          if (jdr.isDeleted(c.toSharedType())) {
            ratio_constructs_deleted.incrementCount();
            log.debug(
                "Construct "
                    + c.toString()
                    + " deleted from "
                    + dep.getLib().getLibraryId().toString()
                    + " to "
                    + otherVersion);
          } else {
            ratio_constructs_body_changed.incrementTotal();
            if (jdr.isBodyChanged(c.toSharedType())) {
              ratio_constructs_body_changed.incrementCount();
              log.debug(
                  "Construct "
                      + c.toString()
                      + " changed from "
                      + dep.getLib().getLibraryId().toString()
                      + " to "
                      + otherVersion);
            } else {
              ratio_constructs_body_stability.incrementCount();
            }
          }
        }
      }
      metrics.addRatio(ratio_constructs_body_stability);
      metrics.addRatio(ratio_constructs_deleted);
      metrics.addRatio(ratio_constructs_body_changed);

      depUpdate.setMetrics(metrics);

      return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
          depUpdate, HttpStatus.OK);
    } catch (ServiceConnectionException sce) {
      return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
          HttpStatus.INTERNAL_SERVER_ERROR);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
          HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Provides application-specific changes required to update the current dependency to the provided one.
   * Requires that the given dependency has a valid library Id.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param digest a {@link java.lang.String} object.
   * @param otherVersion a {@link org.eclipse.steady.backend.model.LibraryId} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps/{digest}/updateChanges",
      method = RequestMethod.POST,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.DepDetails.class) // extends View LibDetails that allows to see the properties
  public ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate> getUpdateChanges(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String digest,
      @RequestBody LibraryId otherVersion,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
          HttpStatus.NOT_FOUND);
    }
    try {

      // To throw an exception if the entity is not found
      final Application a =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));

      final Dependency dep = a.getDependency(digest);
      if (dep == null) {
        log.error(
            "App "
                + a.toString()
                + " has no dependency with digest ["
                + digest
                + "]: No update changes will be returned");
        return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
            HttpStatus.NOT_FOUND);
      }

      // Pre-requisite: The dependency has to have a library id known to Maven
      if (dep.getLib().getLibraryId() == null) {
        log.error(
            "App " + a.toString() + " dependency with digest [" + digest + "] has no library id");
        return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
            HttpStatus.BAD_REQUEST);
      }

      dep.setTraces(this.traceRepository.findTracesOfLibrary(a, dep.getLib()));
      final JarDiffResult jdr =
          ServiceWrapper.getInstance()
              .diffJars(dep.getLib().getLibraryId().toSharedType(), otherVersion.toSharedType());

      final DependencyUpdate depUpdate =
          new DependencyUpdate(dep.getLib().getLibraryId(), otherVersion);

      // Metrics related to touch points (from traces or static analysis)
      final Collection<TouchPoint> touch_points = dep.getTouchPoints();

      if (touch_points != null) {

        Set<TouchPoint> callsToModify = new HashSet<TouchPoint>();

        for (TouchPoint tp : touch_points) {
          // Only A2L is relevant, as only this direction requires modification at app side
          if (tp.getDirection() == TouchPoint.Direction.A2L) {
            if (jdr.isDeleted(tp.getTo().toSharedType())) {
              callsToModify.add(tp);
              log.debug(
                  "Touch point callee "
                      + tp.getTo().toString()
                      + " deleted from "
                      + dep.getLib().getLibraryId().toString()
                      + " to "
                      + otherVersion);
            }
          }
        }

        depUpdate.setCallsToModify(callsToModify);
      }

      return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
          depUpdate, HttpStatus.OK);
    } catch (ServiceConnectionException sce) {
      return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
          HttpStatus.INTERNAL_SERVER_ERROR);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<org.eclipse.steady.backend.model.DependencyUpdate>(
          HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Returns a collection of {@link VulnerableDependency}s relevant for the {@link Application} with the given GAV.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param historical a {@link java.lang.Boolean} object.
   * @param affected a {@link java.lang.Boolean} object.
   * @param includeAffectedUnconfirmed a {@link java.lang.Boolean} object.
   * @param addExcemptionInfo a {@link java.lang.Boolean} object.
   * @param lastChange a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/vulndeps",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<TreeSet<VulnerableDependency>> getAppVulnerableDependencies(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestParam(value = "includeHistorical", required = false, defaultValue = "false")
          Boolean historical, // affected==0
      @RequestParam(value = "includeAffected", required = false, defaultValue = "true")
          Boolean affected, // affected==1
      @RequestParam(value = "includeAffectedUnconfirmed", required = false, defaultValue = "true")
          Boolean includeAffectedUnconfirmed, // affectedConfirmed==0
      @RequestParam(value = "addExcemptionInfo", required = false, defaultValue = "false")
          Boolean addExcemptionInfo, // consider configuration setting
      // "vulas.report.exceptionScopeBlacklist" and
      // "vulas.report.exceptionExcludeBugs"
      @RequestParam(value = "lastChange", required = false, defaultValue = "")
          String lastChange, // a timestamp identifier which is used to cache the response or not
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<TreeSet<VulnerableDependency>>(HttpStatus.NOT_FOUND);
    }

    try {
      // Throw an exception if the entity is not found
      final Application app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));

      // All of them (no matter the scope)
      final TreeSet<VulnerableDependency> vd_all =
          this.appRepository.findAppVulnerableDependencies(app, addExcemptionInfo, true);

      // The set to be returned
      final TreeSet<VulnerableDependency> vd_list = new TreeSet<VulnerableDependency>();

      // Update traced and reachable flags
      // Populate the set to be returned depending on the historical, affected, unconfirmed flags
      for (VulnerableDependency vd : vd_all) {
        if ((includeAffectedUnconfirmed && vd.getAffectedVersionConfirmed() == 0)
            || (vd.getAffectedVersionConfirmed() == 1
                && ((historical && vd.getAffectedVersion() == 0)
                    || (affected && vd.getAffectedVersion() == 1)))) {

          // Update CVE data (if needed)
          this.bugRepository.updateCachedCveData(vd.getBug(), false);

          vd_list.add(vd);
        }
      }

      // If the request has a valid `lastChange` querystring parameter,
      // then we instruct Nginx to cache the response for 2 months
      HttpHeaders headers = new HttpHeaders();
      if (lastChange != null && !lastChange.equals("")) {
        headers.add("X-Accel-Expires", "5256000");
      }

      return new ResponseEntity<TreeSet<VulnerableDependency>>(vd_list, headers, HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<TreeSet<VulnerableDependency>>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * Returns a collection of all {@link VulnerableDependency}s of all applications.
   *
   * @param unconfirmedOnly if true, only non-assessed {@link VulnerableDependency}s are considered
   * @return a {@link org.springframework.http.ResponseEntity} object.
   */
  @RequestMapping(
      value = "/vulndeps",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<TreeSet<VulnerableDependency>> getVulnerableDependencies(
      @RequestParam(value = "unconfirmedOnly", required = false, defaultValue = "true")
          Boolean unconfirmedOnly) {
    try {
      TreeSet<VulnerableDependency> vd_list = new TreeSet<VulnerableDependency>();
      List<Entry<BigInteger, String>> dep_bug = null;
      if (unconfirmedOnly) dep_bug = this.appVulDepRepository.findUnconfirmedVulnDeps();
      else dep_bug = this.appVulDepRepository.findConfirmedVulnDeps();
      // for (int i=0; i<dep_bug.size();i++) {
      for (Entry<BigInteger, String> entry : dep_bug) {
        try {
          VulnerableDependency vd =
              new VulnerableDependency(
                  this.depRepository.findById(entry.getKey().longValue()).orElse(null),
                  BugRepository.FILTER.findOne(this.bugRepository.findByBugId(entry.getValue())));
          vd_list.add(vd);
        } catch (EntityNotFoundException e) {
          log.warn("Could not create vulnerable dependency, entity not found :" + e.getMessage());
        }
      }
      //	}
      this.affLibRepository.computeAffectedLib(vd_list);
      this.appRepository.updateFlags(vd_list, true);

      // Old code using JPQL query
      //			TreeSet<VulnerableDependency> all_vd =
      // this.appRepository.findJPQLVulnerableDependencies();
      //			this.affLibRepository.computeAffectedLib(all_vd);
      //			TreeSet<VulnerableDependency> vd_list = new TreeSet<VulnerableDependency>();
      //			if(unconfirmedOnly==true){
      //				for (VulnerableDependency vd : all_vd){
      //					if(vd.getAffectedVersionConfirmed()==0)
      //						vd_list.add(vd);
      //				}
      //			}
      //			else
      //				vd_list=all_vd;
      //			this.appRepository.updateFlags(vd_list,true);
      return new ResponseEntity<TreeSet<VulnerableDependency>>(vd_list, HttpStatus.OK);

    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<TreeSet<VulnerableDependency>>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Returns a {@link VulnerableDependency}s containing the details of the bugId affecting the dependency digest for the {@link Application} with the given GAV.
   *
   * @return 404 {@link HttpStatus#NOT_FOUND} if application with given GAV does not exist, 200 {@link HttpStatus#OK} if the application is found
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param digest a {@link java.lang.String} object.
   * @param bugid a {@link java.lang.String} object.
   * @param vulnDepOrigin a {@link org.eclipse.steady.shared.enums.VulnDepOrigin} object.
   * @param bundledGroup a {@link java.lang.String} object.
   * @param bundledArtifact a {@link java.lang.String} object.
   * @param bundledVersion a {@link java.lang.String} object.
   * @param bundledLibrary a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/vulndeps/{digest}/bugs/{bugid}",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.VulnDepDetails.class)
  public ResponseEntity<VulnerableDependency> getVulnerableDependencyBugDetails(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String digest,
      @PathVariable String bugid,
      @RequestParam(value = "origin", required = true, defaultValue = "CC")
          VulnDepOrigin vulnDepOrigin,
      @RequestParam(value = "bundledGroup", required = false, defaultValue = "")
          String bundledGroup,
      @RequestParam(value = "bundledArtifact", required = false, defaultValue = "")
          String bundledArtifact,
      @RequestParam(value = "bundledVersion", required = false, defaultValue = "")
          String bundledVersion,
      @RequestParam(value = "bundledLibrary", required = false, defaultValue = "")
          String bundledLibrary,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<VulnerableDependency>(HttpStatus.NOT_FOUND);
    }
    try {
      // To throw an exception if the entity is not found
      final Application a =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));

      if ((vulnDepOrigin.equals(VulnDepOrigin.BUNDLEDCC) && bundledLibrary == null)
          || (vulnDepOrigin.equals(VulnDepOrigin.BUNDLEDAFFLIBID)
              && (bundledGroup == null || bundledArtifact == null || bundledVersion == null)))
        return new ResponseEntity<VulnerableDependency>(HttpStatus.BAD_REQUEST);

      return new ResponseEntity<VulnerableDependency>(
          appRepository.getVulnerableDependencyBugDetails(
              a,
              digest,
              bugid,
              vulnDepOrigin,
              bundledLibrary,
              bundledGroup,
              bundledArtifact,
              bundledVersion),
          HttpStatus.OK);
    } catch (EntityNotFoundException enfe) {
      return new ResponseEntity<VulnerableDependency>(HttpStatus.NOT_FOUND);
    }
  }

  /**
   * <p>createTraces.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param traces an array of {@link org.eclipse.steady.backend.model.Trace} objects.
   * @param skipResponseBody a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/traces",
      method = RequestMethod.POST,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<Trace>> createTraces(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestBody Trace[] traces,
      @RequestParam(value = "skipResponseBody", required = false, defaultValue = "false")
          Boolean skipResponseBody,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<List<Trace>>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Trace>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<Trace>>(HttpStatus.NOT_FOUND);
    }

    // Ensure consistency of path variable and JSON content
    for (Trace trace : traces)
      if (!app.equalsIgnoreSpace(trace.getApp()))
        return new ResponseEntity<List<Trace>>(HttpStatus.UNPROCESSABLE_ENTITY);

    // Save and return
    if (skipResponseBody) {
      this.traceRepository.customSave(app, traces);
      return new ResponseEntity<List<Trace>>(HttpStatus.OK);
    } else
      return new ResponseEntity<List<Trace>>(
          this.traceRepository.customSave(app, traces), HttpStatus.OK);
  }

  /**
   * <p>getTraces.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/traces",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<Trace>> getTraces(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Trace>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<Trace>>(HttpStatus.NOT_FOUND);
    }

    // Save and return
    return new ResponseEntity<List<Trace>>(this.traceRepository.findByApp(app), HttpStatus.OK);
  }

  /**
   * Returns a {@link Collection} of all application {@link Dependency}s including their reachable {@link ConstructId}s.
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/reachableConstructIds",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.DepDetails.class)
  public ResponseEntity<Collection<Dependency>> getReachableContructIds(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Collection<Dependency>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<Collection<Dependency>>(HttpStatus.NOT_FOUND);
    }

    // Save and return
    return new ResponseEntity<Collection<Dependency>>(app.getDependencies(), HttpStatus.OK);
  }

  /**
   * <p>createPaths.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param paths an array of {@link org.eclipse.steady.backend.model.Path} objects.
   * @param skipResponseBody a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/paths",
      method = RequestMethod.POST,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<Path>> createPaths(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @RequestBody Path[] paths,
      @RequestParam(value = "skipResponseBody", required = false, defaultValue = "false")
          Boolean skipResponseBody,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<List<Path>>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // Ensure consistency of path variable and JSON content
    for (Path path : paths)
      if (!app.equalsIgnoreSpace(path.getApp()))
        return new ResponseEntity<List<Path>>(HttpStatus.UNPROCESSABLE_ENTITY);

    // Save and return
    if (skipResponseBody) {
      this.pathRepository.customSave(app, paths);
      return new ResponseEntity<List<Path>>(HttpStatus.OK);
    } else
      return new ResponseEntity<List<Path>>(
          this.pathRepository.customSave(app, paths), HttpStatus.OK);
  }

  /**
   * <p>getPaths.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/paths",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<Path>> getPaths(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // Select and return
    return new ResponseEntity<List<Path>>(this.pathRepository.findPathsForApp(app), HttpStatus.OK);
  }

  /**
   * <p>getVulndepPaths.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param digest a {@link java.lang.String} object.
   * @param bugId a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps/{digest}/paths/{bugId}",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<Path>> getVulndepPaths(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String digest,
      @PathVariable String bugId,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // Ensure that dependency exists
    final Dependency dep = app.getDependency(digest);
    if (dep == null) {
      log.error(
          "App "
              + app.toString()
              + " has no dependency with digest ["
              + digest
              + "]: No paths will be returned for bug ["
              + bugId
              + "]");
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // Ensure that bug exists
    Bug bug = null;
    try {
      bug = BugRepository.FILTER.findOne(this.bugRepository.findByBugId(bugId));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // Select and return
    return new ResponseEntity<List<Path>>(
        this.pathRepository.findPathsForLibraryBug(app, dep.getLib(), bug.getId()), HttpStatus.OK);
  }

  /**
   * <p>getVulndepConstructPaths.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param digest a {@link java.lang.String} object.
   * @param bugId a {@link java.lang.String} object.
   * @param qname a {@link java.lang.String} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps/{digest}/paths/{bugId}/{qname}",
      method = RequestMethod.GET,
      produces = {"application/json;charset=UTF-8"})
  @JsonView(Views.Default.class)
  public ResponseEntity<List<Path>> getVulndepConstructPaths(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String digest,
      @PathVariable String bugId,
      @PathVariable String qname,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // Ensure that dependency exists
    final Dependency dep = app.getDependency(digest);
    if (dep == null) {
      log.error(
          "App "
              + app.toString()
              + " has no dependency with digest ["
              + digest
              + "]: No paths will be returned for bug ["
              + bugId
              + "] and construct ["
              + qname
              + "]");
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // Ensure that bug exists
    Bug bug = null;
    try {
      bug = BugRepository.FILTER.findOne(this.bugRepository.findByBugId(bugId));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<List<Path>>(HttpStatus.NOT_FOUND);
    }

    // select paths
    List<Path> paths =
        this.pathRepository.findPathsForLibraryBugConstructName(app, dep.getLib(), bug, qname);
    // select dependencies for each path node of each path
    for (Path p : paths) {
      for (PathNode pn : p.getPath()) {
        if (pn.getLib() != null) {
          try {
            final Dependency d =
                DependencyRepository.FILTER.findOne(
                    this.depRepository.findByAppAndLib(app, pn.getLib().getDigest()));
            pn.setDep(d);
          } catch (EntityNotFoundException nf) {
            log.error(
                "Error while retrieving dependency with sha1 ["
                    + pn.getLib().getDigest()
                    + "] in app ["
                    + app
                    + "], "
                    + "found in pathNode ["
                    + pn.getConstructId().toString()
                    + "]");
          }
        }
      }
    }

    // Select and return
    return new ResponseEntity<List<Path>>(paths, HttpStatus.OK);
  }

  /**
   * <p>createReachableConstructIds.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param digest a {@link java.lang.String} object.
   * @param constructIds an array of {@link org.eclipse.steady.backend.model.ConstructId} objects.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps/{digest}/reachableConstructIds",
      method = RequestMethod.POST,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  public ResponseEntity<Set<ConstructId>> createReachableConstructIds(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String digest,
      @RequestBody ConstructId[] constructIds,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<Set<ConstructId>>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Set<ConstructId>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<Set<ConstructId>>(HttpStatus.NOT_FOUND);
    }

    // Ensure that dependency exists
    Dependency dep = app.getDependency(digest);
    if (dep == null) {
      if (constructIds == null || constructIds.length == 0) {
        log.warn(
            "App "
                + app.toString()
                + " has no dependency with digest ["
                + digest
                + "]: ["
                + (constructIds == null ? "-" : constructIds.length)
                + "] reachable constructs cannot be saved");
      } else {
        log.warn(
            "App "
                + app.toString()
                + " has no dependency with digest ["
                + digest
                + "]: ["
                + (constructIds == null ? "-" : constructIds.length)
                + "] reachable constructs such as "
                + constructIds[0].toString()
                + " cannot be saved");
      }
      return new ResponseEntity<Set<ConstructId>>(HttpStatus.NOT_FOUND);
    }
    // Save and return
    return new ResponseEntity<Set<ConstructId>>(
        this.depRepository.saveReachableConstructIds(dep, constructIds), HttpStatus.OK);
  }

  /**
   * <p>createTouchPoints.</p>
   *
   * @return 409 {@link HttpStatus#CONFLICT} if bug with given bug ID already exists, 201 {@link HttpStatus#CREATED} if the bug was successfully created
   * @param mvnGroup a {@link java.lang.String} object.
   * @param artifact a {@link java.lang.String} object.
   * @param version a {@link java.lang.String} object.
   * @param digest a {@link java.lang.String} object.
   * @param touchPoints an array of {@link org.eclipse.steady.backend.model.TouchPoint} objects.
   * @param skipResponseBody a {@link java.lang.Boolean} object.
   * @param space a {@link java.lang.String} object.
   */
  @RequestMapping(
      value = "/{mvnGroup:.+}/{artifact:.+}/{version:.+}/deps/{digest}/touchPoints",
      method = RequestMethod.POST,
      consumes = {"application/json;charset=UTF-8"},
      produces = {"application/json;charset=UTF-8"})
  public ResponseEntity<Set<TouchPoint>> createTouchPoints(
      @PathVariable String mvnGroup,
      @PathVariable String artifact,
      @PathVariable String version,
      @PathVariable String digest,
      @RequestBody TouchPoint[] touchPoints,
      @RequestParam(value = "skipResponseBody", required = false, defaultValue = "false")
          Boolean skipResponseBody,
      @Parameter(hidden = true)
          @RequestHeader(value = Constants.HTTP_SPACE_HEADER, required = false)
          String space) {

    Space s = null;
    try {
      s = this.spaceRepository.getSpace(space);
      if (s.isReadOnly()) return new ResponseEntity<Set<TouchPoint>>(HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
      log.error("Error retrieving space: " + e);
      return new ResponseEntity<Set<TouchPoint>>(HttpStatus.NOT_FOUND);
    }
    // Ensure that app exists
    Application app = null;
    try {
      app =
          ApplicationRepository.FILTER.findOne(
              this.appRepository.findByGAV(mvnGroup, artifact, version, s));
    } catch (EntityNotFoundException e) {
      return new ResponseEntity<Set<TouchPoint>>(HttpStatus.NOT_FOUND);
    }

    // Ensure that dependency exists
    Dependency dep = app.getDependency(digest);
    if (dep == null) {
      if (touchPoints == null || touchPoints.length == 0) {
        log.warn(
            "App "
                + app.toString()
                + " has no dependency with digest ["
                + digest
                + "]: ["
                + (touchPoints == null ? "-" : touchPoints.length)
                + "] touch points cannot be saved");
      } else {
        log.warn(
            "App "
                + app.toString()
                + " has no dependency with digest ["
                + digest
                + "]: ["
                + (touchPoints == null ? "-" : touchPoints.length)
                + "] touch points such as "
                + touchPoints[0].toString()
                + " cannot be saved");
      }
      return new ResponseEntity<Set<TouchPoint>>(HttpStatus.NOT_FOUND);
    }
    // Save and return
    if (skipResponseBody) {
      this.depRepository.saveTouchPoints(dep, touchPoints);
      return new ResponseEntity<Set<TouchPoint>>(HttpStatus.OK);
    } else
      return new ResponseEntity<Set<TouchPoint>>(
          this.depRepository.saveTouchPoints(dep, touchPoints), HttpStatus.OK);
  }
}
