/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.nifi.web.api; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiParam; import com.wordnik.swagger.annotations.ApiResponse; import com.wordnik.swagger.annotations.ApiResponses; import com.wordnik.swagger.annotations.Authorization; import org.apache.nifi.cluster.coordination.ClusterCoordinator; import org.apache.nifi.cluster.coordination.http.replication.RequestReplicator; import org.apache.nifi.cluster.protocol.NodeIdentifier; import org.apache.nifi.controller.repository.claim.ContentDirection; import org.apache.nifi.stream.io.StreamUtils; import org.apache.nifi.web.DownloadableContent; import org.apache.nifi.web.NiFiServiceFacade; import org.apache.nifi.web.api.dto.provenance.ProvenanceEventDTO; import org.apache.nifi.web.api.entity.ProvenanceEventEntity; import org.apache.nifi.web.api.entity.SubmitReplayRequestEntity; import org.apache.nifi.web.api.request.LongParameter; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.HttpMethod; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; /** * RESTful endpoint for querying data provenance. */ @Path("/provenance-events") @Api( value = "/provenance-events", description = "Endpoint for accessing data flow provenance." ) public class ProvenanceEventResource extends ApplicationResource { private NiFiServiceFacade serviceFacade; /** * Gets the content for the input of the specified event. * * @param clusterNodeId The id of the node within the cluster this content is on. Required if clustered. * @param id The id of the provenance event associated with this content. * @return The content stream */ @GET @Consumes(MediaType.WILDCARD) @Produces(MediaType.WILDCARD) @Path("{id}/content/input") @ApiOperation( value = "Gets the input content for a provenance event", authorizations = { @Authorization(value = "Read Component Data - /data/{component-type}/{uuid}", type = "") } ) @ApiResponses( value = { @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), @ApiResponse(code = 401, message = "Client could not be authenticated."), @ApiResponse(code = 403, message = "Client is not authorized to make this request."), @ApiResponse(code = 404, message = "The specified resource could not be found."), @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") } ) public Response getInputContent( @ApiParam( value = "The id of the node where the content exists if clustered.", required = false ) @QueryParam("clusterNodeId") final String clusterNodeId, @ApiParam( value = "The provenance event id.", required = true ) @PathParam("id") final LongParameter id) { // ensure proper input if (id == null) { throw new IllegalArgumentException("The event id must be specified."); } // replicate if cluster manager if (isReplicateRequest()) { // determine where this request should be sent if (clusterNodeId == null) { throw new IllegalArgumentException("The id of the node in the cluster is required."); } else { return replicate(HttpMethod.GET, clusterNodeId); } } // get the uri of the request final String uri = generateResourceUri("provenance", "events", String.valueOf(id.getLong()), "content", "input"); // get an input stream to the content final DownloadableContent content = serviceFacade.getContent(id.getLong(), uri, ContentDirection.INPUT); // generate a streaming response final StreamingOutput response = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException, WebApplicationException { try (InputStream is = content.getContent()) { // stream the content to the response StreamUtils.copy(is, output); // flush the response output.flush(); } } }; // use the appropriate content type String contentType = content.getType(); if (contentType == null) { contentType = MediaType.APPLICATION_OCTET_STREAM; } return generateOkResponse(response).type(contentType).header("Content-Disposition", String.format("attachment; filename=\"%s\"", content.getFilename())).build(); } /** * Gets the content for the output of the specified event. * * @param clusterNodeId The id of the node within the cluster this content is on. Required if clustered. * @param id The id of the provenance event associated with this content. * @return The content stream */ @GET @Consumes(MediaType.WILDCARD) @Produces(MediaType.WILDCARD) @Path("{id}/content/output") @ApiOperation( value = "Gets the output content for a provenance event", authorizations = { @Authorization(value = "Read Component Data - /data/{component-type}/{uuid}", type = "") } ) @ApiResponses( value = { @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), @ApiResponse(code = 401, message = "Client could not be authenticated."), @ApiResponse(code = 403, message = "Client is not authorized to make this request."), @ApiResponse(code = 404, message = "The specified resource could not be found."), @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") } ) public Response getOutputContent( @ApiParam( value = "The id of the node where the content exists if clustered.", required = false ) @QueryParam("clusterNodeId") final String clusterNodeId, @ApiParam( value = "The provenance event id.", required = true ) @PathParam("id") final LongParameter id) { // ensure proper input if (id == null) { throw new IllegalArgumentException("The event id must be specified."); } // replicate if cluster manager if (isReplicateRequest()) { // determine where this request should be sent if (clusterNodeId == null) { throw new IllegalArgumentException("The id of the node in the cluster is required."); } else { return replicate(HttpMethod.GET, clusterNodeId); } } // get the uri of the request final String uri = generateResourceUri("provenance", "events", String.valueOf(id.getLong()), "content", "output"); // get an input stream to the content final DownloadableContent content = serviceFacade.getContent(id.getLong(), uri, ContentDirection.OUTPUT); // generate a streaming response final StreamingOutput response = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException, WebApplicationException { try (InputStream is = content.getContent()) { // stream the content to the response StreamUtils.copy(is, output); // flush the response output.flush(); } } }; // use the appropriate content type String contentType = content.getType(); if (contentType == null) { contentType = MediaType.APPLICATION_OCTET_STREAM; } return generateOkResponse(response).type(contentType).header("Content-Disposition", String.format("attachment; filename=\"%s\"", content.getFilename())).build(); } /** * Gets the details for a provenance event. * * @param id The id of the event * @param clusterNodeId The id of node in the cluster that the event/flowfile originated from. This is only required when clustered. * @return A provenanceEventEntity */ @GET @Consumes(MediaType.WILDCARD) @Produces(MediaType.APPLICATION_JSON) @Path("{id}") @ApiOperation( value = "Gets a provenance event", response = ProvenanceEventEntity.class, authorizations = { @Authorization(value = "Read Component Data - /data/{component-type}/{uuid}", type = "") } ) @ApiResponses( value = { @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), @ApiResponse(code = 401, message = "Client could not be authenticated."), @ApiResponse(code = 403, message = "Client is not authorized to make this request."), @ApiResponse(code = 404, message = "The specified resource could not be found."), @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") } ) public Response getProvenanceEvent( @ApiParam( value = "The id of the node where this event exists if clustered.", required = false ) @QueryParam("clusterNodeId") final String clusterNodeId, @ApiParam( value = "The provenance event id.", required = true ) @PathParam("id") final LongParameter id) { // ensure the id is specified if (id == null) { throw new IllegalArgumentException("Provenance event id must be specified."); } // replicate if cluster manager if (isReplicateRequest()) { // since we're cluster we must specify the cluster node identifier if (clusterNodeId == null) { throw new IllegalArgumentException("The cluster node identifier must be specified."); } return replicate(HttpMethod.GET, clusterNodeId); } // get the provenance event final ProvenanceEventDTO event = serviceFacade.getProvenanceEvent(id.getLong()); event.setClusterNodeId(clusterNodeId); // populate the cluster node address final ClusterCoordinator coordinator = getClusterCoordinator(); if (coordinator != null) { final NodeIdentifier nodeId = coordinator.getNodeIdentifier(clusterNodeId); event.setClusterNodeAddress(nodeId.getApiAddress() + ":" + nodeId.getApiPort()); } // create a response entity final ProvenanceEventEntity entity = new ProvenanceEventEntity(); entity.setProvenanceEvent(event); // generate the response return clusterContext(generateOkResponse(entity)).build(); } /** * Creates a new replay request for the content associated with the specified provenance event id. * * @param httpServletRequest request * @param replayRequestEntity The replay request * @return A provenanceEventEntity */ @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Path("replays") @ApiOperation( value = "Replays content from a provenance event", response = ProvenanceEventEntity.class, authorizations = { @Authorization(value = "Read Component Data - /data/{component-type}/{uuid}", type = ""), @Authorization(value = "Write Component Data - /data/{component-type}/{uuid}", type = "") } ) @ApiResponses( value = { @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), @ApiResponse(code = 401, message = "Client could not be authenticated."), @ApiResponse(code = 403, message = "Client is not authorized to make this request."), @ApiResponse(code = 404, message = "The specified resource could not be found."), @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") } ) public Response submitReplay( @Context final HttpServletRequest httpServletRequest, @ApiParam( value = "The replay request.", required = true ) final SubmitReplayRequestEntity replayRequestEntity) { // ensure the event id is specified if (replayRequestEntity == null || replayRequestEntity.getEventId() == null) { throw new IllegalArgumentException("The id of the event must be specified."); } // replicate if cluster manager if (isReplicateRequest()) { // determine where this request should be sent if (replayRequestEntity.getClusterNodeId() == null) { throw new IllegalArgumentException("The id of the node in the cluster is required."); } else { return replicate(HttpMethod.POST, replayRequestEntity, replayRequestEntity.getClusterNodeId()); } } // handle expects request (usually from the cluster manager) final String expects = httpServletRequest.getHeader(RequestReplicator.REQUEST_VALIDATION_HTTP_HEADER); if (expects != null) { return generateContinueResponse().build(); } // submit the provenance replay request final ProvenanceEventDTO event = serviceFacade.submitReplay(replayRequestEntity.getEventId()); event.setClusterNodeId(replayRequestEntity.getClusterNodeId()); // populate the cluster node address final ClusterCoordinator coordinator = getClusterCoordinator(); if (coordinator != null) { final NodeIdentifier nodeId = coordinator.getNodeIdentifier(replayRequestEntity.getClusterNodeId()); event.setClusterNodeAddress(nodeId.getApiAddress() + ":" + nodeId.getApiPort()); } // create a response entity final ProvenanceEventEntity entity = new ProvenanceEventEntity(); entity.setProvenanceEvent(event); // generate the response URI uri = URI.create(generateResourceUri("provenance-events", event.getId())); return clusterContext(generateCreatedResponse(uri, entity)).build(); } // setters public void setServiceFacade(NiFiServiceFacade serviceFacade) { this.serviceFacade = serviceFacade; } }