diff --git a/src/main/java/io/papermc/fill/controller/Publish3Controller.java b/src/main/java/io/papermc/fill/controller/Publish3Controller.java new file mode 100644 index 0000000..757597b --- /dev/null +++ b/src/main/java/io/papermc/fill/controller/Publish3Controller.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 PaperMC + * + * 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 io.papermc.fill.controller; + +import io.papermc.fill.database.ProjectEntity; +import io.papermc.fill.database.ProjectRepository; +import io.papermc.fill.database.VersionEntity; +import io.papermc.fill.database.VersionRepository; +import io.papermc.fill.exception.ProjectNotFoundException; +import io.papermc.fill.exception.VersionNotFoundException; +import io.papermc.fill.model.request.v3.AllocateRequest; +import io.papermc.fill.model.response.v3.AllocateBuildNumberResponse; +import io.papermc.fill.service.PublishingService; +import io.papermc.fill.util.http.Responses; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Parameter; +import java.util.OptionalInt; +import org.jspecify.annotations.NullMarked; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.CacheControl; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@Hidden +@NullMarked +@RestController +public class Publish3Controller { + private final ProjectRepository projects; + private final VersionRepository versions; + private final PublishingService publishing; + + @Autowired + public Publish3Controller( + final ProjectRepository projects, + final VersionRepository versions, + final PublishingService publishing + ) { + this.projects = projects; + this.versions = versions; + this.publishing = publishing; + } + + @CrossOrigin(methods = RequestMethod.GET) + @PostMapping("/v3/projects/{project}/versions/{version}/publishing") + public ResponseEntity allocateBuild( + @Parameter(description = "The key of the project") + @PathVariable("project") + final String projectKey, + @Parameter(description = "The key of the version") + @PathVariable("version") + final String versionKey, + @RequestBody + final AllocateRequest request + ) { + final ProjectEntity project = this.projects.findByKey(projectKey).orElseThrow(ProjectNotFoundException::new); + final VersionEntity version = this.versions.findByProjectAndKey(project, versionKey).orElseThrow(VersionNotFoundException::new); + final int number = this.publishing.allocateBuildNumber(request.session(), version, OptionalInt.empty()); + final AllocateBuildNumberResponse response = new AllocateBuildNumberResponse(number); + return Responses.ok(response, CacheControl.noStore()); + } +} diff --git a/src/main/java/io/papermc/fill/controller/PublishController.java b/src/main/java/io/papermc/fill/controller/PublishController.java index d3662a6..c2cd698 100644 --- a/src/main/java/io/papermc/fill/controller/PublishController.java +++ b/src/main/java/io/papermc/fill/controller/PublishController.java @@ -182,7 +182,7 @@ public ResponseEntity publish( } } - final int number = this.publishing.getNextBuildNumber(version, OptionalInt.of(request.build())); + final int number = this.publishing.allocateBuildNumber(request.id().toString(), version, OptionalInt.of(request.build())); if (this.builds.findByVersionAndNumber(version, number).isPresent()) { throw createPublishFailedException(request, "Build already exists", new DuplicateBuildException()); diff --git a/src/main/java/io/papermc/fill/controller/advice/ExceptionRestControllerAdvice.java b/src/main/java/io/papermc/fill/controller/advice/ExceptionRestControllerAdvice.java index 9179fe9..cc72d16 100644 --- a/src/main/java/io/papermc/fill/controller/advice/ExceptionRestControllerAdvice.java +++ b/src/main/java/io/papermc/fill/controller/advice/ExceptionRestControllerAdvice.java @@ -28,6 +28,7 @@ import io.papermc.fill.exception.FamilyNotFoundException; import io.papermc.fill.exception.ProjectNotFoundException; import io.papermc.fill.exception.PublishFailedException; +import io.papermc.fill.exception.SessionConflictException; import io.papermc.fill.exception.SunsetException; import io.papermc.fill.exception.VersionNotFoundException; import io.papermc.fill.model.response.ErrorResponse; @@ -112,7 +113,8 @@ public ResponseEntity on405MethodNotAllowed(final HttpRequestMethodNotSupport @ExceptionHandler({ DuplicateBuildException.class, DuplicateFamilyException.class, - DuplicateVersionException.class + DuplicateVersionException.class, + SessionConflictException.class }) public ResponseEntity on409Conflict(final Throwable throwable) { return Responses.conflict(new ErrorResponse( @@ -120,6 +122,7 @@ public ResponseEntity on409Conflict(final Throwable throwable) { case final DuplicateBuildException _ -> "build_already_exists"; case final DuplicateFamilyException _ -> "family_already_exists"; case final DuplicateVersionException _ -> "version_already_exists"; + case final SessionConflictException _ -> "session_conflict"; default -> throw new IllegalStateException("Unexpected value: " + throwable); }, throwable.getMessage() diff --git a/src/main/java/io/papermc/fill/database/VersionEntity.java b/src/main/java/io/papermc/fill/database/VersionEntity.java index 87ad524..9f4b981 100644 --- a/src/main/java/io/papermc/fill/database/VersionEntity.java +++ b/src/main/java/io/papermc/fill/database/VersionEntity.java @@ -20,6 +20,8 @@ import io.papermc.fill.model.Version; import io.papermc.fill.util.git.GitRepository; import java.time.Instant; +import java.util.HashSet; +import java.util.Set; import org.bson.types.ObjectId; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -42,6 +44,9 @@ public class VersionEntity extends AbstractEntity implements Version { private @Nullable GitRepository gitRepository; private Support support; private @Nullable Java java; + private @Nullable Set publishingSessions; + private @Nullable String publishingSession; + private int nextBuildNumber; @Deprecated private @Nullable ObjectId mostRecentPromotedBuild; @@ -110,6 +115,28 @@ public void setJava(final @Nullable Java java) { this.java = java; } + public boolean isPastPublishingSession(final String publishingSession) { + final boolean isCurrentPublishingSession = publishingSession.equals(this.publishingSession); + final boolean isPastPublishingSession = this.publishingSessions != null && this.publishingSessions.contains(publishingSession); + return !isCurrentPublishingSession && isPastPublishingSession; + } + + public void setPublishingSession(final String publishingSession) { + this.publishingSession = publishingSession; + if (this.publishingSessions == null) { + this.publishingSessions = new HashSet<>(); + } + this.publishingSessions.add(publishingSession); + } + + public int nextBuildNumber() { + return this.nextBuildNumber; + } + + public void setNextBuildNumber(final int nextBuildNumber) { + this.nextBuildNumber = nextBuildNumber; + } + @Deprecated public @Nullable ObjectId mostRecentPromotedBuild() { return this.mostRecentPromotedBuild; diff --git a/src/main/java/io/papermc/fill/exception/SessionConflictException.java b/src/main/java/io/papermc/fill/exception/SessionConflictException.java new file mode 100644 index 0000000..12152d7 --- /dev/null +++ b/src/main/java/io/papermc/fill/exception/SessionConflictException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 PaperMC + * + * 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 io.papermc.fill.exception; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class SessionConflictException extends AppException { + public SessionConflictException(final String message) { + super(message); + } +} diff --git a/src/main/java/io/papermc/fill/model/request/v3/AllocateRequest.java b/src/main/java/io/papermc/fill/model/request/v3/AllocateRequest.java new file mode 100644 index 0000000..ac2a475 --- /dev/null +++ b/src/main/java/io/papermc/fill/model/request/v3/AllocateRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 PaperMC + * + * 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 io.papermc.fill.model.request.v3; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record AllocateRequest( + String session +) { +} diff --git a/src/main/java/io/papermc/fill/model/response/v3/AllocateBuildNumberResponse.java b/src/main/java/io/papermc/fill/model/response/v3/AllocateBuildNumberResponse.java new file mode 100644 index 0000000..28c9cb9 --- /dev/null +++ b/src/main/java/io/papermc/fill/model/response/v3/AllocateBuildNumberResponse.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 PaperMC + * + * 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 io.papermc.fill.model.response.v3; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record AllocateBuildNumberResponse( + int number +) { +} diff --git a/src/main/java/io/papermc/fill/service/PublishingService.java b/src/main/java/io/papermc/fill/service/PublishingService.java index 801e265..3b745a6 100644 --- a/src/main/java/io/papermc/fill/service/PublishingService.java +++ b/src/main/java/io/papermc/fill/service/PublishingService.java @@ -21,7 +21,8 @@ @NullMarked public interface PublishingService { - int getNextBuildNumber( + int allocateBuildNumber( + final String session, final VersionEntity version, @Deprecated(forRemoval = true) final OptionalInt requested diff --git a/src/main/java/io/papermc/fill/service/PublishingServiceImpl.java b/src/main/java/io/papermc/fill/service/PublishingServiceImpl.java index d9f2ccc..0b078b7 100644 --- a/src/main/java/io/papermc/fill/service/PublishingServiceImpl.java +++ b/src/main/java/io/papermc/fill/service/PublishingServiceImpl.java @@ -15,13 +15,12 @@ */ package io.papermc.fill.service; -import com.google.common.annotations.VisibleForTesting; import io.papermc.fill.database.BuildRepository; import io.papermc.fill.database.VersionEntity; +import io.papermc.fill.database.VersionRepository; +import io.papermc.fill.exception.SessionConflictException; import io.papermc.fill.model.Numbered; -import java.util.List; import java.util.OptionalInt; -import java.util.function.Supplier; import org.jspecify.annotations.NullMarked; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,44 +32,45 @@ public class PublishingServiceImpl implements PublishingService { private static final Logger LOGGER = LoggerFactory.getLogger(PublishingServiceImpl.class); + private final VersionRepository versions; private final BuildRepository builds; @Autowired public PublishingServiceImpl( + final VersionRepository versions, final BuildRepository builds ) { + this.versions = versions; this.builds = builds; } @Override - public int getNextBuildNumber( + public int allocateBuildNumber( + final String session, final VersionEntity version, @Deprecated(forRemoval = true) final OptionalInt requested ) { - if (requested.isPresent()) { - LOGGER.warn("Manually requested build number {} for version {}", requested, version._id()); + if (version.isPastPublishingSession(session)) { + throw new SessionConflictException(String.format("Session %s expired.", session)); } - return getNextBuildNumber(() -> this.builds.findAllByVersion(version).toList(), requested); - } - @VisibleForTesting - static int getNextBuildNumber( - final Supplier> builds, - @Deprecated(forRemoval = true) - final OptionalInt requested - ) { + final int allocatedBuildNumber; if (requested.isPresent()) { - return requested.getAsInt(); - } - final OptionalInt maxBuildNumber = builds - .get() - .stream() - .mapToInt(Numbered::number) - .max(); - if (maxBuildNumber.isPresent()) { - return maxBuildNumber.getAsInt() + 1; + LOGGER.warn("Manually requested build number {} for version {}", requested, version._id()); + allocatedBuildNumber = requested.getAsInt(); + } else if (version.nextBuildNumber() == 0) { + final OptionalInt maxBuildNumber = this.builds + .findAllByVersion(version) + .mapToInt(Numbered::number) + .max(); + allocatedBuildNumber = maxBuildNumber.orElse(0) + 1; + } else { + allocatedBuildNumber = version.nextBuildNumber(); } - return 1; + version.setPublishingSession(session); + version.setNextBuildNumber(allocatedBuildNumber + 1); + this.versions.save(version); + return allocatedBuildNumber; } } diff --git a/src/test/java/io/papermc/fill/service/PublishingServiceImplTest.java b/src/test/java/io/papermc/fill/service/PublishingServiceImplTest.java deleted file mode 100644 index f840463..0000000 --- a/src/test/java/io/papermc/fill/service/PublishingServiceImplTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2024 PaperMC - * - * 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 io.papermc.fill.service; - -import io.papermc.fill.model.Numbered; -import java.util.List; -import java.util.OptionalInt; -import java.util.function.Supplier; -import org.jspecify.annotations.NullMarked; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@NullMarked -public class PublishingServiceImplTest { - @Test - void returnsRequestedBuildNumberWhenPresent() { - final Supplier> builds = () -> List.of(numbered(1), numbered(2), numbered(3)); - assertEquals(42, PublishingServiceImpl.getNextBuildNumber(builds, OptionalInt.of(42))); - } - - @Test - void returnsOneWhenThereAreNoBuilds() { - final Supplier> builds = () -> List.of(); - assertEquals(1, PublishingServiceImpl.getNextBuildNumber(builds, OptionalInt.empty())); - } - - @Test - void returnsOneMoreThanHighestBuildNumber() { - final Supplier> builds = () -> List.of(numbered(1), numbered(3), numbered(7)); - assertEquals(8, PublishingServiceImpl.getNextBuildNumber(builds, OptionalInt.empty())); - } - - private static Numbered numbered(final int number) { - return () -> number; - } -}