@Override
  public DownloadableContent getContent(final ContentRequestContext request) {
    // if clustered, send request to cluster manager
    if (properties.isClustered()
        && clusterCoordinator != null
        && clusterCoordinator.isConnected()) {
      // get the URI
      URI dataUri;
      try {
        dataUri = new URI(request.getDataUri());
      } catch (final URISyntaxException use) {
        throw new ClusterRequestException(use);
      }

      // set the request parameters
      final MultivaluedMap<String, String> parameters = new MultivaluedMapImpl();
      parameters.add(CLIENT_ID_PARAM, request.getClientId());

      // set the headers
      final Map<String, String> headers = new HashMap<>();
      if (StringUtils.isNotBlank(request.getProxiedEntitiesChain())) {
        headers.put("X-ProxiedEntitiesChain", request.getProxiedEntitiesChain());
      }

      // add the user's authorities (if any) to the headers
      final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      if (authentication != null) {
        final Object userDetailsObj = authentication.getPrincipal();
        if (userDetailsObj instanceof NiFiUserDetails) {
          // serialize user details object
          final String hexEncodedUserDetails =
              WebUtils.serializeObjectToHex((Serializable) userDetailsObj);

          // put serialized user details in header
          headers.put("X-ProxiedEntityUserDetails", hexEncodedUserDetails);
        }
      }

      // ensure we were able to detect the cluster node id
      if (request.getClusterNodeId() == null) {
        throw new IllegalArgumentException("Unable to determine the which node has the content.");
      }

      // get the target node and ensure it exists
      final NodeIdentifier nodeId =
          clusterCoordinator.getNodeIdentifier(request.getClusterNodeId());
      final Set<NodeIdentifier> targetNodes = Collections.singleton(nodeId);

      // replicate the request to the specific node
      NodeResponse nodeResponse;
      try {
        nodeResponse =
            requestReplicator
                .replicate(targetNodes, HttpMethod.GET, dataUri, parameters, headers)
                .awaitMergedResponse();
      } catch (InterruptedException e) {
        throw new IllegalClusterStateException(
            "Interrupted while waiting for a response from node");
      }

      final ClientResponse clientResponse = nodeResponse.getClientResponse();
      final MultivaluedMap<String, String> responseHeaders = clientResponse.getHeaders();

      // ensure an appropriate response
      if (Status.NOT_FOUND.getStatusCode() == clientResponse.getStatusInfo().getStatusCode()) {
        throw new ResourceNotFoundException(clientResponse.getEntity(String.class));
      } else if (Status.FORBIDDEN.getStatusCode() == clientResponse.getStatusInfo().getStatusCode()
          || Status.UNAUTHORIZED.getStatusCode()
              == clientResponse.getStatusInfo().getStatusCode()) {
        throw new AccessDeniedException(clientResponse.getEntity(String.class));
      } else if (Status.OK.getStatusCode() != clientResponse.getStatusInfo().getStatusCode()) {
        throw new IllegalStateException(clientResponse.getEntity(String.class));
      }

      // get the file name
      final String contentDisposition = responseHeaders.getFirst("Content-Disposition");
      final String filename = StringUtils.substringBetween(contentDisposition, "filename=\"", "\"");

      // get the content type
      final String contentType = responseHeaders.getFirst("Content-Type");

      // create the downloadable content
      return new DownloadableContent(filename, contentType, clientResponse.getEntityInputStream());
    } else {
      // example URIs:
      // http://localhost:8080/nifi-api/provenance/events/{id}/content/{input|output}
      // http://localhost:8080/nifi-api/flowfile-queues/{uuid}/flowfiles/{uuid}/content

      // get just the context path for comparison
      final String dataUri = StringUtils.substringAfter(request.getDataUri(), "/nifi-api");
      if (StringUtils.isBlank(dataUri)) {
        throw new IllegalArgumentException("The specified data reference URI is not valid.");
      }

      // flowfile listing content
      final Matcher flowFileMatcher = FLOWFILE_CONTENT_URI_PATTERN.matcher(dataUri);
      if (flowFileMatcher.matches()) {
        final String connectionId = flowFileMatcher.group(1);
        final String flowfileId = flowFileMatcher.group(2);

        return getFlowFileContent(connectionId, flowfileId, dataUri);
      }

      // provenance event content
      final Matcher provenanceMatcher = PROVENANCE_CONTENT_URI_PATTERN.matcher(dataUri);
      if (provenanceMatcher.matches()) {
        try {
          final Long eventId = Long.parseLong(provenanceMatcher.group(1));
          final ContentDirection direction =
              ContentDirection.valueOf(provenanceMatcher.group(2).toUpperCase());

          return getProvenanceEventContent(eventId, dataUri, direction);
        } catch (final IllegalArgumentException iae) {
          throw new IllegalArgumentException("The specified data reference URI is not valid.");
        }
      }

      // invalid uri
      throw new IllegalArgumentException("The specified data reference URI is not valid.");
    }
  }
  @Override
  @PreAuthorize("hasRole('ROLE_PROVENANCE')")
  public DownloadableContent getContent(final ContentRequestContext request) {
    // if clustered, send request to cluster manager
    if (properties.isClusterManager()) {
      // get the URI
      URI dataUri;
      try {
        dataUri = new URI(request.getDataUri());
      } catch (final URISyntaxException use) {
        throw new ClusterRequestException(use);
      }

      // set the request parameters
      final MultivaluedMap<String, String> parameters = new MultivaluedMapImpl();
      parameters.add(CLIENT_ID_PARAM, request.getClientId());

      // set the headers
      final Map<String, String> headers = new HashMap<>();
      if (StringUtils.isNotBlank(request.getProxiedEntitiesChain())) {
        headers.put("X-ProxiedEntitiesChain", request.getProxiedEntitiesChain());
      }

      // add the user's authorities (if any) to the headers
      final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      if (authentication != null) {
        final Object userDetailsObj = authentication.getPrincipal();
        if (userDetailsObj instanceof NiFiUserDetails) {
          // serialize user details object
          final String hexEncodedUserDetails =
              WebUtils.serializeObjectToHex((Serializable) userDetailsObj);

          // put serialized user details in header
          headers.put("X-ProxiedEntityUserDetails", hexEncodedUserDetails);
        }
      }

      // get the target node and ensure it exists
      final Node targetNode = clusterManager.getNode(request.getClusterNodeId());
      if (targetNode == null) {
        throw new UnknownNodeException("The specified cluster node does not exist.");
      }

      final Set<NodeIdentifier> targetNodes = new HashSet<>();
      targetNodes.add(targetNode.getNodeId());

      // replicate the request to the specific node
      final NodeResponse nodeResponse =
          clusterManager.applyRequest(HttpMethod.GET, dataUri, parameters, headers, targetNodes);
      final ClientResponse clientResponse = nodeResponse.getClientResponse();
      final MultivaluedMap<String, String> responseHeaders = clientResponse.getHeaders();

      // get the file name
      final String contentDisposition = responseHeaders.getFirst("Content-Disposition");
      final String filename = StringUtils.substringBetween(contentDisposition, "filename=\"", "\"");

      // get the content type
      final String contentType = responseHeaders.getFirst("Content-Type");

      // create the downloadable content
      return new DownloadableContent(filename, contentType, clientResponse.getEntityInputStream());
    } else {
      // example URI: http://localhost:8080/nifi-api/controller/provenance/events/1/content/input
      final String eventDetails = StringUtils.substringAfterLast(request.getDataUri(), "events/");
      final String rawEventId = StringUtils.substringBefore(eventDetails, "/content/");
      final String rawDirection = StringUtils.substringAfterLast(eventDetails, "/content/");

      // get the content type
      final Long eventId;
      final ContentDirection direction;
      try {
        eventId = Long.parseLong(rawEventId);
        direction = ContentDirection.valueOf(rawDirection.toUpperCase());
      } catch (final IllegalArgumentException iae) {
        throw new IllegalArgumentException("The specified data reference URI is not valid.");
      }
      return serviceFacade.getContent(eventId, request.getDataUri(), direction);
    }
  }