ExecutionService.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.entrystore.Context;
import org.entrystore.Data;
import org.entrystore.Entry;
import org.entrystore.EntryType;
import org.entrystore.GraphType;
import org.entrystore.PrincipalManager;
import org.entrystore.PrincipalManager.AccessProperty;
import org.entrystore.ResourceType;
import org.entrystore.rest.springboot.model.api.ExecutePipelineResponse;
import org.entrystore.rest.springboot.model.exception.BadRequestException;
import org.entrystore.rest.springboot.model.exception.CustomResponseException;
import org.entrystore.rest.springboot.model.exception.DataConflictException;
import org.entrystore.rest.springboot.model.exception.EntityNotFoundException;
import org.entrystore.rest.springboot.model.exception.InternalServerErrorException;
import org.entrystore.transforms.Pipeline;
import org.entrystore.transforms.TransformException;
import org.entrystore.transforms.UpstreamServiceException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.List;
import java.util.Set;
@Slf4j
@Service
@RequiredArgsConstructor
public class ExecutionService {
private final ContextService contextService;
private final PrincipalManager principalManager;
public ExecutePipelineResponse execute(String contextId, URI pipelineUri, URI sourceUri) {
Context context = contextService.getContextOrThrow(contextId);
String prefix = context.getEntry().getResourceURI().toString() + "/";
if (!pipelineUri.toString().startsWith(prefix)) {
throw new BadRequestException("Pipeline entry must belong to the request context");
}
if (sourceUri != null && !sourceUri.toString().startsWith(prefix)) {
throw new BadRequestException("Source entry must belong to the request context");
}
principalManager.checkAuthenticatedUserAuthorized(context.getEntry(), AccessProperty.WriteResource);
Entry pipelineEntry = context.getByEntryURI(pipelineUri);
if (pipelineEntry == null) {
throw new EntityNotFoundException("Pipeline entry not found in context '" + contextId + "'");
}
principalManager.checkAuthenticatedUserAuthorized(pipelineEntry, AccessProperty.ReadResource);
Entry sourceEntry = null;
URI listURI = null;
if (sourceUri != null) {
sourceEntry = context.getByEntryURI(sourceUri);
if (sourceEntry == null) {
throw new EntityNotFoundException("Source entry not found in context '" + contextId + "'");
}
principalManager.checkAuthenticatedUserAuthorized(sourceEntry, AccessProperty.ReadResource);
if (sourceEntry.getEntryType() != EntryType.Local
|| sourceEntry.getResourceType() != ResourceType.InformationResource
|| sourceEntry.getMimetype() == null
|| !(sourceEntry.getResource() instanceof Data)) {
throw new DataConflictException("Source entry must be a local information resource with data");
}
Set<URI> lists = sourceEntry.getReferringListsInSameContext();
if (lists.size() == 1) {
listURI = lists.iterator().next();
} else if (lists.size() > 1) {
log.debug("Source entry {} belongs to {} referring lists; running pipeline without target list",
sourceUri, lists.size());
}
}
if (pipelineEntry.getGraphType() != GraphType.Pipeline) {
throw new DataConflictException("Pipeline entry must have GraphType.Pipeline");
}
Set<Entry> processed;
try {
processed = new Pipeline(pipelineEntry).run(sourceEntry, listURI);
} catch (IllegalStateException e) {
throw new BadRequestException("Pipeline execution failed", e);
} catch (UpstreamServiceException e) {
HttpStatus status = e.isTimeout() ? HttpStatus.GATEWAY_TIMEOUT : HttpStatus.BAD_GATEWAY;
String outcome = e.isTimeout() ? "did not respond in time" : "failed";
throw new CustomResponseException("Pipeline execution failed: a service it calls " + outcome, status, e);
} catch (TransformException e) {
throw new InternalServerErrorException("Pipeline execution failed", e);
}
if (processed == null || processed.isEmpty()) {
return new ExecutePipelineResponse(List.of());
}
return new ExecutePipelineResponse(processed.stream()
.map(entry -> entry.getEntryURI().toString())
.toList());
}
}