diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2023-01-30 22:22:59 +0000 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2023-02-02 21:56:07 +0000 |
| commit | 49b3015a16287bb4486aa64c5c26f05f7c22089c (patch) | |
| tree | 2c2e3ef49181ed740ac938b00a00ae5958d96d30 /opendc-web/opendc-web-server/src/main/java | |
| parent | 6927c51885bb3073b310150c4f40c64eea44a919 (diff) | |
refactor(web/server): Remove unnecessary service indirections
This change removes the unnecessary service classes where they are only
used to forward data from the resource to the entities. Furthermore,
DTOs are now moved from the service layer to the resources.
Diffstat (limited to 'opendc-web/opendc-web-server/src/main/java')
22 files changed, 742 insertions, 906 deletions
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java index 14fd3e2a..c5fb208e 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java @@ -22,18 +22,22 @@ package org.opendc.web.server.model; +import io.quarkus.hibernate.orm.panache.Panache; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import java.time.Instant; -import java.util.List; import java.util.Map; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.FetchType; +import javax.persistence.ForeignKey; import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; -import javax.persistence.OneToOne; import javax.persistence.Table; import org.hibernate.annotations.Type; import org.opendc.web.proto.JobState; @@ -54,8 +58,8 @@ import org.opendc.web.proto.JobState; """) }) public class Job extends PanacheEntity { - @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER) - @JoinColumn(name = "scenario_id", nullable = false) + @ManyToOne(optional = false, fetch = FetchType.EAGER) + @JoinColumn(name = "scenario_id", foreignKey = @ForeignKey(name = "fk_jobs_scenario"), nullable = false) public Scenario scenario; @Column(name = "created_by", nullable = false, updatable = false) @@ -74,12 +78,14 @@ public class Job extends PanacheEntity { * The instant at which the job was updated. */ @Column(name = "updated_at", nullable = false) - public Instant updatedAt = createdAt; + public Instant updatedAt; /** * The state of the job. */ - @Column(nullable = false) + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) public JobState state = JobState.PENDING; /** @@ -102,6 +108,7 @@ public class Job extends PanacheEntity { this.createdBy = createdBy; this.scenario = scenario; this.createdAt = createdAt; + this.updatedAt = createdAt; this.repeats = repeats; } @@ -114,10 +121,10 @@ public class Job extends PanacheEntity { * Find {@link Job}s in the specified {@link JobState}. * * @param state The state of the jobs to find. - * @return The list of jobs that are in the specified state. + * @return A query for jobs that are in the specified state. */ - public static List<Job> findByState(JobState state) { - return find("state", state).list(); + public static PanacheQuery<Job> findByState(JobState state) { + return find("state", state); } /** @@ -137,6 +144,24 @@ public class Job extends PanacheEntity { .and("updatedAt", time) .and("runtime", runtime) .and("results", results)); + Panache.getEntityManager().refresh(this); return count > 0; } + + /** + * Determine whether the job is allowed to transition to <code>newState</code>. + * + * @param newState The new state to transition to. + * @return <code>true</code> if the transition to the new state is legal, <code>false</code> otherwise. + */ + public boolean canTransitionTo(JobState newState) { + // Note that we always allow transitions from the state + return newState == this.state + || switch (this.state) { + case PENDING -> newState == JobState.CLAIMED; + case CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED; + case RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED; + case FINISHED, FAILED -> false; + }; + } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java index 4c3af570..3a406683 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java @@ -23,9 +23,9 @@ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import java.util.HashSet; -import java.util.List; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -48,8 +48,12 @@ import org.opendc.web.proto.Targets; @Entity @Table( name = "portfolios", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_portfolios_number", columnList = "project_id, number")}) + uniqueConstraints = { + @UniqueConstraint( + name = "uk_portfolios_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_portfolios_number", columnList = "project_id, number")}) @NamedQueries({ @NamedQuery(name = "Portfolio.findByProject", query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId"), @NamedQuery( @@ -112,11 +116,10 @@ public class Portfolio extends PanacheEntity { * Find all {@link Portfolio}s that belong to the specified project * * @param projectId The unique identifier of the project. - * @return The list of portfolios that belong to the specified project. + * @return The query of portfolios that belong to the specified project. */ - public static List<Portfolio> findByProject(long projectId) { - return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery<Portfolio> findByProject(long projectId) { + return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java index c10fcc64..1238f58d 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java @@ -23,20 +23,25 @@ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import java.io.Serializable; -import java.util.List; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.EmbeddedId; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.MapsId; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; +import org.hibernate.annotations.Type; import org.opendc.web.proto.user.ProjectRole; /** @@ -64,15 +69,22 @@ public class ProjectAuthorization extends PanacheEntityBase { /** * The project that the user is authorized to participate in. */ - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.LAZY) @MapsId("projectId") - @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false) + @JoinColumn( + name = "project_id", + updatable = false, + insertable = false, + nullable = false, + foreignKey = @ForeignKey(name = "fk_project_authorizations")) public Project project; /** * The role of the user in the project. */ - @Column(nullable = false) + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) public ProjectRole role; /** @@ -93,11 +105,10 @@ public class ProjectAuthorization extends PanacheEntityBase { * List all projects for the user with the specified <code>userId</code>. * * @param userId The identifier of the user that is requesting the list of projects. - * @return A list of projects that the user has received authorization for. + * @return A query returning projects that the user has received authorization for. */ - public static List<ProjectAuthorization> findByUser(String userId) { - return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)) - .list(); + public static PanacheQuery<ProjectAuthorization> findByUser(String userId) { + return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java index 9381f9be..016e931b 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java @@ -23,18 +23,22 @@ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; +import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; import javax.persistence.Index; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; -import javax.persistence.OneToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.hibernate.annotations.Type; @@ -46,8 +50,12 @@ import org.opendc.web.proto.OperationalPhenomena; @Entity @Table( name = "scenarios", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_scenarios_number", columnList = "project_id, number")}) + uniqueConstraints = { + @UniqueConstraint( + name = "uk_scenarios_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_scenarios_number", columnList = "project_id, number")}) @NamedQueries({ @NamedQuery(name = "Scenario.findByProject", query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId"), @NamedQuery( @@ -68,14 +76,14 @@ public class Scenario extends PanacheEntity { * The {@link Project} to which this scenario belongs. */ @ManyToOne(optional = false) - @JoinColumn(name = "project_id", nullable = false) + @JoinColumn(name = "project_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_project")) public Project project; /** * The {@link Portfolio} to which this scenario belongs. */ @ManyToOne(optional = false) - @JoinColumn(name = "portfolio_id", nullable = false) + @JoinColumn(name = "portfolio_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_portfolio")) public Portfolio portfolio; /** @@ -100,6 +108,7 @@ public class Scenario extends PanacheEntity { * Topology details of the scenario. */ @ManyToOne(optional = false) + @JoinColumn(name = "topology_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_topology")) public Topology topology; /** @@ -118,8 +127,11 @@ public class Scenario extends PanacheEntity { /** * The {@link Job} associated with the scenario. */ - @OneToOne(cascade = {CascadeType.ALL}) - public Job job; + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "scenario", + fetch = FetchType.LAZY) + public List<Job> jobs = new ArrayList<>(); /** * Construct a {@link Scenario} object. @@ -152,11 +164,10 @@ public class Scenario extends PanacheEntity { * Find all {@link Scenario}s that belong to the specified project * * @param projectId The unique identifier of the project. - * @return The list of scenarios that belong to the specified project. + * @return The query of scenarios that belong to the specified project. */ - public static List<Scenario> findByProject(long projectId) { - return find("#Scenario.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery<Scenario> findByProject(long projectId) { + return find("#Scenario.findByProject", Parameters.with("projectId", projectId)); } /** @@ -164,13 +175,12 @@ public class Scenario extends PanacheEntity { * * @param projectId The unique identifier of the project. * @param number The number of the portfolio. - * @return The list of scenarios that belong to the specified project and portfolio.. + * @return The query of scenarios that belong to the specified project and portfolio.. */ - public static List<Scenario> findByPortfolio(long projectId, int number) { + public static PanacheQuery<Scenario> findByPortfolio(long projectId, int number) { return find( - "#Scenario.findByPortfolio", - Parameters.with("projectId", projectId).and("number", number)) - .list(); + "#Scenario.findByPortfolio", + Parameters.with("projectId", projectId).and("number", number)); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java index 6ec83f78..05a1ac12 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java @@ -23,6 +23,7 @@ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import java.time.Instant; import java.util.List; @@ -44,8 +45,12 @@ import org.opendc.web.proto.Room; @Entity @Table( name = "topologies", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_topologies_number", columnList = "project_id, number")}) + uniqueConstraints = { + @UniqueConstraint( + name = "uk_topologies_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_topologies_number", columnList = "project_id, number")}) @NamedQueries({ @NamedQuery(name = "Topology.findByProject", query = "SELECT t FROM Topology t WHERE t.project.id = :projectId"), @NamedQuery( @@ -112,11 +117,10 @@ public class Topology extends PanacheEntity { * Find all [Topology]s that belong to [project][projectId]. * * @param projectId The unique identifier of the project. - * @return The list of topologies that belong to the specified project. + * @return The query of topologies that belong to the specified project. */ - public static List<Topology> findByProject(long projectId) { - return find("#Topology.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery<Topology> findByProject(long projectId) { + return find("#Topology.findByProject", Parameters.with("projectId", projectId)); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java index f73c8494..36d27abc 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java @@ -26,11 +26,13 @@ import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Table; /** * A workload trace available for simulation. */ @Entity +@Table(name = "traces") public class Trace extends PanacheEntityBase { /** * The unique identifier of the trace. diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java new file mode 100644 index 00000000..44d2d569 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest; + +import org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; + +/** + * DTO-conversions for the base protocol. + */ +public final class BaseProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private BaseProtocol() {} + + /** + * Convert a {@link Workload} entity into a DTO. + */ + public static org.opendc.web.proto.Workload toDto(Workload workload) { + return new org.opendc.web.proto.Workload(toDto(workload.trace), workload.samplingFraction); + } + + /** + * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. + */ + public static org.opendc.web.proto.Trace toDto(Trace trace) { + return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java index 2b1efb02..7316c93f 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java @@ -43,7 +43,7 @@ public final class TraceResource { @GET public List<org.opendc.web.proto.Trace> getAll() { Stream<Trace> entities = Trace.streamAll(); - return entities.map(TraceResource::toUserDto).toList(); + return entities.map(TraceResource::toDto).toList(); } /** @@ -58,13 +58,13 @@ public final class TraceResource { throw new WebApplicationException("Trace not found", 404); } - return toUserDto(trace); + return toDto(trace); } /** * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. */ - public static org.opendc.web.proto.Trace toUserDto(Trace trace) { + public static org.opendc.web.proto.Trace toDto(Trace trace) { return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java index 134c6814..dff52526 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java @@ -33,6 +33,8 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; import org.opendc.web.server.service.JobService; /** @@ -43,14 +45,14 @@ import org.opendc.web.server.service.JobService; @RolesAllowed("runner") public final class JobResource { /** - * The {@link JobService} responsible for managing the jobs. + * The {@link JobService} for helping manage the job lifecycle. */ private final JobService jobService; /** * Construct a {@link JobResource} instance. * - * @param jobService The {@link JobService} responsible for managing the jobs. + * @param jobService The {@link JobService} for managing the job lifecycle. */ public JobResource(JobService jobService) { this.jobService = jobService; @@ -61,7 +63,9 @@ public final class JobResource { */ @GET public List<org.opendc.web.proto.runner.Job> queryPending() { - return jobService.listPending(); + return Job.findByState(JobState.PENDING).list().stream() + .map(RunnerProtocol::toDto) + .toList(); } /** @@ -70,12 +74,13 @@ public final class JobResource { @GET @Path("{job}") public org.opendc.web.proto.runner.Job get(@PathParam("job") long id) { - org.opendc.web.proto.runner.Job job = jobService.findById(id); + Job job = Job.findById(id); + if (job == null) { throw new WebApplicationException("Job not found", 404); } - return job; + return RunnerProtocol.toDto(job); } /** @@ -87,17 +92,19 @@ public final class JobResource { @Transactional public org.opendc.web.proto.runner.Job update( @PathParam("job") long id, @Valid org.opendc.web.proto.runner.Job.Update update) { - try { - var job = jobService.updateState(id, update.getState(), update.getRuntime(), update.getResults()); - if (job == null) { - throw new WebApplicationException("Job not found", 404); - } + Job job = Job.findById(id); + if (job == null) { + throw new WebApplicationException("Job not found", 404); + } - return job; + try { + jobService.updateJob(job, update.getState(), update.getRuntime(), update.getResults()); } catch (IllegalArgumentException e) { throw new WebApplicationException(e, 400); } catch (IllegalStateException e) { throw new WebApplicationException(e, 409); } + + return RunnerProtocol.toDto(job); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java new file mode 100644 index 00000000..6bf65d97 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.runner; + +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.rest.BaseProtocol; + +/** + * DTO-conversions for the runner protocol. + */ +public final class RunnerProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private RunnerProtocol() {} + + /** + * Convert a {@link Job} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Job toDto(Job job) { + return new org.opendc.web.proto.runner.Job( + job.id, toDto(job.scenario), job.state, job.createdAt, job.updatedAt, job.runtime, job.results); + } + + /** + * Convert a {@link Scenario} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Scenario toDto(Scenario scenario) { + return new org.opendc.web.proto.runner.Scenario( + scenario.id, + scenario.number, + toDto(scenario.portfolio), + scenario.name, + BaseProtocol.toDto(scenario.workload), + toDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName); + } + + /** + * Convert a {@link Portfolio} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Portfolio toDto(Portfolio portfolio) { + return new org.opendc.web.proto.runner.Portfolio( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Topology} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Topology toDto(Topology topology) { + return new org.opendc.web.proto.runner.Topology( + topology.id, topology.number, topology.name, topology.rooms, topology.createdAt, topology.updatedAt); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java index e8e05f97..d1fc980d 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java @@ -23,10 +23,12 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; import javax.validation.Valid; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -34,7 +36,8 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.server.service.PortfolioService; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; /** * A resource representing the portfolios of a project. @@ -44,11 +47,6 @@ import org.opendc.web.server.service.PortfolioService; @RolesAllowed("openid") public final class PortfolioResource { /** - * The service for managing the user portfolios. - */ - private final PortfolioService portfolioService; - - /** * The identity of the current user. */ private final SecurityIdentity identity; @@ -56,11 +54,9 @@ public final class PortfolioResource { /** * Construct a {@link PortfolioResource}. * - * @param portfolioService The {@link PortfolioService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public PortfolioResource(PortfolioService portfolioService, SecurityIdentity identity) { - this.portfolioService = portfolioService; + public PortfolioResource(SecurityIdentity identity) { this.identity = identity; } @@ -69,7 +65,17 @@ public final class PortfolioResource { */ @GET public List<org.opendc.web.proto.user.Portfolio> getAll(@PathParam("project") long projectId) { - return portfolioService.findByUser(identity.getPrincipal().getName(), projectId); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return Portfolio.findByProject(projectId).list().stream() + .map((p) -> UserProtocol.toDto(p, auth)) + .toList(); } /** @@ -77,14 +83,29 @@ public final class PortfolioResource { */ @POST @Transactional + @Consumes("application/json") public org.opendc.web.proto.user.Portfolio create( @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Portfolio.Create request) { - var portfolio = portfolioService.create(identity.getPrincipal().getName(), projectId, request); - if (portfolio == null) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { throw new WebApplicationException("Project not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); } - return portfolio; + var now = Instant.now(); + var project = auth.project; + int number = project.allocatePortfolio(now); + + Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets()); + + project.portfolios.add(portfolio); + portfolio.persist(); + + return UserProtocol.toDto(portfolio, auth); } /** @@ -94,12 +115,21 @@ public final class PortfolioResource { @Path("{portfolio}") public org.opendc.web.proto.user.Portfolio get( @PathParam("project") long projectId, @PathParam("portfolio") int number) { - var portfolio = portfolioService.findByUser(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, number); + if (portfolio == null) { throw new WebApplicationException("Portfolio not found", 404); } - return portfolio; + return UserProtocol.toDto(portfolio, auth); } /** @@ -110,11 +140,22 @@ public final class PortfolioResource { @Transactional public org.opendc.web.proto.user.Portfolio delete( @PathParam("project") long projectId, @PathParam("portfolio") int number) { - var portfolio = portfolioService.delete(identity.getPrincipal().getName(), projectId, number); - if (portfolio == null) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Portfolio entity = Portfolio.findByProject(projectId, number); + if (entity == null) { throw new WebApplicationException("Portfolio not found", 404); } - return portfolio; + entity.delete(); + return UserProtocol.toDto(entity, auth); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java index a6db7c54..a058cd31 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java @@ -23,28 +23,39 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; import javax.validation.Valid; +import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.proto.user.Scenario; -import org.opendc.web.server.service.ScenarioService; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; +import org.opendc.web.server.service.UserAccountingService; /** * A resource representing the scenarios of a portfolio. */ @Path("/projects/{project}/portfolios/{portfolio}/scenarios") @RolesAllowed("openid") +@Produces("application/json") public final class PortfolioScenarioResource { /** - * The service for managing the user scenarios. + * The service for managing the user accounting. */ - private final ScenarioService scenarioService; + private final UserAccountingService accountingService; /** * The identity of the current user. @@ -54,11 +65,11 @@ public final class PortfolioScenarioResource { /** * Construct a {@link PortfolioScenarioResource}. * - * @param scenarioService The {@link ScenarioService} instance to use. + * @param accountingService The {@link UserAccountingService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public PortfolioScenarioResource(ScenarioService scenarioService, SecurityIdentity identity) { - this.scenarioService = scenarioService; + public PortfolioScenarioResource(UserAccountingService accountingService, SecurityIdentity identity) { + this.accountingService = accountingService; this.identity = identity; } @@ -66,8 +77,19 @@ public final class PortfolioScenarioResource { * Get all scenarios that belong to the specified portfolio. */ @GET - public List<Scenario> get(@PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber) { - return scenarioService.findAll(identity.getPrincipal().getName(), projectId, portfolioNumber); + public List<org.opendc.web.proto.user.Scenario> get( + @PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return org.opendc.web.server.model.Scenario.findByPortfolio(projectId, portfolioNumber).list().stream() + .map((s) -> UserProtocol.toDto(s, auth)) + .toList(); } /** @@ -75,15 +97,63 @@ public final class PortfolioScenarioResource { */ @POST @Transactional + @Consumes("application/json") public org.opendc.web.proto.user.Scenario create( @PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber, @Valid org.opendc.web.proto.user.Scenario.Create request) { - var scenario = scenarioService.create(identity.getPrincipal().getName(), projectId, portfolioNumber, request); - if (scenario == null) { + // User must have access to project + String userId = identity.getPrincipal().getName(); + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { throw new WebApplicationException("Portfolio not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); + + if (portfolio == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); + if (topology == null) { + throw new WebApplicationException("Referred topology does not exist", 400); + } + + Trace trace = Trace.findById(request.getWorkload().getTrace()); + if (trace == null) { + throw new WebApplicationException("Referred trace does not exist", 400); } - return scenario; + var now = Instant.now(); + var project = auth.project; + int number = project.allocateScenario(now); + + Scenario scenario = new Scenario( + project, + portfolio, + number, + request.getName(), + new Workload(trace, request.getWorkload().getSamplingFraction()), + topology, + request.getPhenomena(), + request.getSchedulerName()); + scenario.persist(); + + Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); + job.persist(); + + // Fail the job if there is not enough budget for the simulation + if (!accountingService.hasSimulationBudget(userId)) { + job.state = JobState.FAILED; + } + + scenario.jobs.add(job); + portfolio.scenarios.add(scenario); + + return UserProtocol.toDto(scenario, auth); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java index b0b8eb4e..da47c3ff 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java @@ -23,6 +23,7 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; @@ -35,7 +36,9 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.server.service.ProjectService; +import org.opendc.web.proto.user.ProjectRole; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; /** * A resource representing the created projects. @@ -45,11 +48,6 @@ import org.opendc.web.server.service.ProjectService; @RolesAllowed("openid") public final class ProjectResource { /** - * The service for managing the user projects. - */ - private final ProjectService projectService; - - /** * The identity of the current user. */ private final SecurityIdentity identity; @@ -57,11 +55,9 @@ public final class ProjectResource { /** * Construct a {@link ProjectResource}. * - * @param projectService The {@link ProjectService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public ProjectResource(ProjectService projectService, SecurityIdentity identity) { - this.projectService = projectService; + public ProjectResource(SecurityIdentity identity) { this.identity = identity; } @@ -70,7 +66,9 @@ public final class ProjectResource { */ @GET public List<org.opendc.web.proto.user.Project> getAll() { - return projectService.findByUser(identity.getPrincipal().getName()); + return ProjectAuthorization.findByUser(identity.getPrincipal().getName()).list().stream() + .map(UserProtocol::toDto) + .toList(); } /** @@ -80,7 +78,17 @@ public final class ProjectResource { @Transactional @Consumes("application/json") public org.opendc.web.proto.user.Project create(@Valid org.opendc.web.proto.user.Project.Create request) { - return projectService.create(identity.getPrincipal().getName(), request.getName()); + Instant now = Instant.now(); + Project entity = new Project(request.getName(), now); + entity.persist(); + + ProjectAuthorization authorization = + new ProjectAuthorization(entity, identity.getPrincipal().getName(), ProjectRole.OWNER); + + entity.authorizations.add(authorization); + authorization.persist(); + + return UserProtocol.toDto(authorization); } /** @@ -89,12 +97,14 @@ public final class ProjectResource { @GET @Path("{project}") public org.opendc.web.proto.user.Project get(@PathParam("project") long id) { - var project = projectService.findByUser(identity.getPrincipal().getName(), id); - if (project == null) { + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); + + if (auth == null) { throw new WebApplicationException("Project not found", 404); } - return project; + return UserProtocol.toDto(auth); } /** @@ -104,15 +114,18 @@ public final class ProjectResource { @Path("{project}") @Transactional public org.opendc.web.proto.user.Project delete(@PathParam("project") long id) { - try { - var project = projectService.delete(identity.getPrincipal().getName(), id); - if (project == null) { - throw new WebApplicationException("Project not found", 404); - } + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); - return project; - } catch (IllegalArgumentException e) { - throw new WebApplicationException(e.getMessage(), 403); + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canDelete()) { + throw new WebApplicationException("Not allowed to delete project", 403); } + + auth.project.updatedAt = Instant.now(); + org.opendc.web.proto.user.Project project = UserProtocol.toDto(auth); + auth.project.delete(); + return project; } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java index a6838148..cf933c32 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java @@ -23,6 +23,7 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; import javax.ws.rs.DELETE; @@ -31,7 +32,8 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.server.service.ScenarioService; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; /** * A resource representing the scenarios of a portfolio. @@ -41,11 +43,6 @@ import org.opendc.web.server.service.ScenarioService; @RolesAllowed("openid") public final class ScenarioResource { /** - * The service for managing the user scenarios. - */ - private final ScenarioService scenarioService; - - /** * The identity of the current user. */ private final SecurityIdentity identity; @@ -53,27 +50,52 @@ public final class ScenarioResource { /** * Construct a {@link ScenarioResource}. * - * @param scenarioService The {@link ScenarioService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public ScenarioResource(ScenarioService scenarioService, SecurityIdentity identity) { - this.scenarioService = scenarioService; + public ScenarioResource(SecurityIdentity identity) { this.identity = identity; } /** + * Obtain the scenarios belonging to a project. + */ + @GET + public List<org.opendc.web.proto.user.Scenario> getAll(@PathParam("project") long projectId) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + return Scenario.findByProject(projectId).list().stream() + .map((s) -> UserProtocol.toDto(s, auth)) + .toList(); + } + + /** * Obtain a scenario by its identifier. */ @GET @Path("{scenario}") public org.opendc.web.proto.user.Scenario get( @PathParam("project") long projectId, @PathParam("scenario") int number) { - var scenario = scenarioService.findOne(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + Scenario scenario = Scenario.findByProject(projectId, number); + if (scenario == null) { throw new WebApplicationException("Scenario not found", 404); } - return scenario; + return UserProtocol.toDto(scenario, auth); } /** @@ -84,11 +106,22 @@ public final class ScenarioResource { @Transactional public org.opendc.web.proto.user.Scenario delete( @PathParam("project") long projectId, @PathParam("scenario") int number) { - var scenario = scenarioService.delete(identity.getPrincipal().getName(), projectId, number); - if (scenario == null) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Scenario entity = Scenario.findByProject(projectId, number); + if (entity == null) { throw new WebApplicationException("Scenario not found", 404); } - return scenario; + entity.delete(); + return UserProtocol.toDto(entity, auth); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java index 54afc1ce..2b66b64b 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java @@ -23,6 +23,7 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; @@ -36,7 +37,9 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.server.service.TopologyService; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Topology; /** * A resource representing the constructed datacenter topologies. @@ -46,11 +49,6 @@ import org.opendc.web.server.service.TopologyService; @RolesAllowed("openid") public final class TopologyResource { /** - * The service for managing the user topologies. - */ - private final TopologyService topologyService; - - /** * The identity of the current user. */ private final SecurityIdentity identity; @@ -58,11 +56,9 @@ public final class TopologyResource { /** * Construct a {@link TopologyResource}. * - * @param topologyService The {@link TopologyService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public TopologyResource(TopologyService topologyService, SecurityIdentity identity) { - this.topologyService = topologyService; + public TopologyResource(SecurityIdentity identity) { this.identity = identity; } @@ -71,7 +67,17 @@ public final class TopologyResource { */ @GET public List<org.opendc.web.proto.user.Topology> getAll(@PathParam("project") long projectId) { - return topologyService.findAll(identity.getPrincipal().getName(), projectId); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return Topology.findByProject(projectId).list().stream() + .map((t) -> UserProtocol.toDto(t, auth)) + .toList(); } /** @@ -82,13 +88,26 @@ public final class TopologyResource { @Transactional public org.opendc.web.proto.user.Topology create( @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Topology.Create request) { - var topology = topologyService.create(identity.getPrincipal().getName(), projectId, request); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + if (auth == null) { throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); } - return topology; + Instant now = Instant.now(); + Project project = auth.project; + int number = project.allocateTopology(now); + + Topology topology = new Topology(project, number, request.getName(), now, request.getRooms()); + + project.topologies.add(topology); + topology.persist(); + + return UserProtocol.toDto(topology, auth); } /** @@ -98,13 +117,21 @@ public final class TopologyResource { @Path("{topology}") public org.opendc.web.proto.user.Topology get( @PathParam("project") long projectId, @PathParam("topology") int number) { - var topology = topologyService.findOne(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } + + Topology topology = Topology.findByProject(projectId, number); if (topology == null) { throw new WebApplicationException("Topology not found", 404); } - return topology; + return UserProtocol.toDto(topology, auth); } /** @@ -118,13 +145,26 @@ public final class TopologyResource { @PathParam("project") long projectId, @PathParam("topology") int number, @Valid org.opendc.web.proto.user.Topology.Update request) { - var topology = topologyService.update(identity.getPrincipal().getName(), projectId, number, request); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { throw new WebApplicationException("Topology not found", 404); } - return topology; + entity.updatedAt = Instant.now(); + entity.rooms = request.getRooms(); + + return UserProtocol.toDto(entity, auth); } /** @@ -135,12 +175,24 @@ public final class TopologyResource { @Transactional public org.opendc.web.proto.user.Topology delete( @PathParam("project") long projectId, @PathParam("topology") int number) { - var topology = topologyService.delete(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { throw new WebApplicationException("Topology not found", 404); } - return topology; + entity.updatedAt = Instant.now(); + entity.delete(); + return UserProtocol.toDto(entity, auth); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java new file mode 100644 index 00000000..8196a9d6 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.user; + +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.rest.BaseProtocol; + +/** + * DTO-conversions for the user protocol. + */ +public final class UserProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private UserProtocol() {} + + /** + * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. + */ + public static org.opendc.web.proto.user.Project toDto(ProjectAuthorization auth) { + Project project = auth.project; + return new org.opendc.web.proto.user.Project( + project.id, project.name, project.createdAt, project.updatedAt, auth.role); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. + */ + public static org.opendc.web.proto.user.Portfolio toDto(Portfolio portfolio, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Portfolio( + portfolio.id, + portfolio.number, + toDto(auth), + portfolio.name, + portfolio.targets, + portfolio.scenarios.stream().map(UserProtocol::toSummaryDto).toList()); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio.Summary} DTO. + */ + public static org.opendc.web.proto.user.Portfolio.Summary toSummaryDto(Portfolio portfolio) { + return new org.opendc.web.proto.user.Portfolio.Summary( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. + */ + public static org.opendc.web.proto.user.Topology toDto(Topology topology, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Topology( + topology.id, + topology.number, + toDto(auth), + topology.name, + topology.rooms, + topology.createdAt, + topology.updatedAt); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology.Summary} DTO. + */ + public static org.opendc.web.proto.user.Topology.Summary toSummaryDto(Topology topology) { + return new org.opendc.web.proto.user.Topology.Summary( + topology.id, topology.number, topology.name, topology.createdAt, topology.updatedAt); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. + */ + public static org.opendc.web.proto.user.Scenario toDto(Scenario scenario, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Scenario( + scenario.id, + scenario.number, + toDto(auth), + toSummaryDto(scenario.portfolio), + scenario.name, + BaseProtocol.toDto(scenario.workload), + toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + scenario.jobs.stream().map(UserProtocol::toDto).toList()); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario.Summary} DTO. + */ + public static org.opendc.web.proto.user.Scenario.Summary toSummaryDto(Scenario scenario) { + return new org.opendc.web.proto.user.Scenario.Summary( + scenario.id, + scenario.number, + scenario.name, + BaseProtocol.toDto(scenario.workload), + toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + scenario.jobs.stream().map(UserProtocol::toDto).toList()); + } + + /** + * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. + */ + public static org.opendc.web.proto.user.Job toDto(Job job) { + return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java index 47f44d27..ed0eaf9c 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java @@ -23,122 +23,59 @@ package org.opendc.web.server.service; import java.time.Instant; -import java.util.List; import java.util.Map; import javax.enterprise.context.ApplicationScoped; import org.opendc.web.proto.JobState; import org.opendc.web.server.model.Job; /** - * Service for managing {@link Job}s. + * A service for managing the lifecycle of a job and ensuring that the user does not consume + * too much simulation resources. */ @ApplicationScoped public final class JobService { /** - * The service for managing the user accounting. + * The {@link UserAccountingService} responsible for accounting the simulation time of users. */ private final UserAccountingService accountingService; /** * Construct a {@link JobService} instance. * - * @param accountingService The {@link UserAccountingService} instance to use. + * @param accountingService The {@link UserAccountingService} for accounting the simulation time of users. */ public JobService(UserAccountingService accountingService) { this.accountingService = accountingService; } /** - * Query the pending simulation jobs. - */ - public List<org.opendc.web.proto.runner.Job> listPending() { - return Job.findByState(JobState.PENDING).stream() - .map(JobService::toRunnerDto) - .toList(); - } - - /** - * Find a job by its identifier. - */ - public org.opendc.web.proto.runner.Job findById(long id) { - Job job = Job.findById(id); - - if (job == null) { - return null; - } - - return toRunnerDto(job); - } - - /** - * Atomically update the state of a {@link Job}. + * Update the job state. * - * @param id The identifier of the job. - * @param newState The next state for the job. - * @param runtime The runtime of the job (in seconds). - * @param results The potential results of the job. + * @param job The {@link Job} to update. + * @param newState The new state to transition the job to. + * @param runtime The runtime (in seconds) consumed by the simulation jbo so far. + * @param results The results to attach to the job. + * @throws IllegalArgumentException if the state transition is invalid. + * @throws IllegalStateException if someone tries to update the job concurrently. */ - public org.opendc.web.proto.runner.Job updateState( - long id, JobState newState, int runtime, Map<String, ?> results) { - Job entity = Job.findById(id); - if (entity == null) { - return null; - } + public void updateJob(Job job, JobState newState, int runtime, Map<String, ?> results) { + JobState state = job.state; - JobState state = entity.state; - if (!isTransitionLegal(state, newState)) { + if (!job.canTransitionTo(newState)) { throw new IllegalArgumentException("Invalid transition from %s to %s".formatted(state, newState)); } Instant now = Instant.now(); JobState nextState = newState; - int consumedBudget = Math.min(1, runtime - entity.runtime); + int consumedBudget = Math.min(1, runtime - job.runtime); // Check whether the user still has any simulation budget left - if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) - && nextState == JobState.RUNNING) { + if (accountingService.consumeSimulationBudget(job.createdBy, consumedBudget) && nextState == JobState.RUNNING) { nextState = JobState.FAILED; // User has consumed all their budget; cancel the job } - if (!entity.updateAtomically(nextState, now, runtime, results)) { + if (!job.updateAtomically(nextState, now, runtime, results)) { throw new IllegalStateException("Conflicting update"); } - - return toRunnerDto(entity); - } - - /** - * Determine whether the transition from [this] to [newState] is legal. - */ - public static boolean isTransitionLegal(JobState currentState, JobState newState) { - // Note that we always allow transitions from the state - return newState == currentState - || switch (currentState) { - case PENDING -> newState == JobState.CLAIMED; - case CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED; - case RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED; - case FINISHED, FAILED -> false; - }; - } - - /** - * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. - */ - public static org.opendc.web.proto.user.Job toUserDto(Job job) { - return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); - } - - /** - * Convert a {@link Job} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Job toRunnerDto(Job job) { - return new org.opendc.web.proto.runner.Job( - job.id, - ScenarioService.toRunnerDto(job.scenario), - job.state, - job.createdAt, - job.updatedAt, - job.runtime, - job.results); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java deleted file mode 100644 index 94da5195..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2023 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service; - -import java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.server.model.Portfolio; -import org.opendc.web.server.model.ProjectAuthorization; - -/** - * Service for managing {@link Portfolio}s. - */ -@ApplicationScoped -public final class PortfolioService { - /** - * List all {@link Portfolio}s that belong a certain project. - */ - public List<org.opendc.web.proto.user.Portfolio> findByUser(String userId, long projectId) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Portfolio.findByProject(projectId).stream() - .map((p) -> toUserDto(p, auth)) - .toList(); - } - - /** - * Find a {@link Portfolio} with the specified <code>number</code> belonging to <code>projectId</code>. - */ - public org.opendc.web.proto.user.Portfolio findByUser(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Portfolio portfolio = Portfolio.findByProject(projectId, number); - - if (portfolio == null) { - return null; - } - - return toUserDto(portfolio, auth); - } - - /** - * Delete the portfolio with the specified <code>number</code> belonging to <code>projectId</code>. - */ - public org.opendc.web.proto.user.Portfolio delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Portfolio entity = Portfolio.findByProject(projectId, number); - if (entity == null) { - return null; - } - - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Portfolio} with the specified name. - */ - public org.opendc.web.proto.user.Portfolio create( - String userId, long projectId, org.opendc.web.proto.user.Portfolio.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - var now = Instant.now(); - var project = auth.project; - int number = project.allocatePortfolio(now); - - Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets()); - - project.portfolios.add(portfolio); - portfolio.persist(); - - return toUserDto(portfolio, auth); - } - - /** - * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. - */ - public static org.opendc.web.proto.user.Portfolio toUserDto(Portfolio portfolio, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Portfolio( - portfolio.id, - portfolio.number, - ProjectService.toUserDto(auth), - portfolio.name, - portfolio.targets, - portfolio.scenarios.stream().map(ScenarioService::toSummaryDto).toList()); - } - - /** - * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio.Summary} DTO. - */ - public static org.opendc.web.proto.user.Portfolio.Summary toSummaryDto(Portfolio portfolio) { - return new org.opendc.web.proto.user.Portfolio.Summary( - portfolio.id, portfolio.number, portfolio.name, portfolio.targets); - } - - /** - * Convert a {@link Portfolio} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Portfolio toRunnerDto(Portfolio portfolio) { - return new org.opendc.web.proto.runner.Portfolio( - portfolio.id, portfolio.number, portfolio.name, portfolio.targets); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java deleted file mode 100644 index aeef664e..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2023 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service; - -import java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.proto.user.ProjectRole; -import org.opendc.web.server.model.Project; -import org.opendc.web.server.model.ProjectAuthorization; - -/** - * Service for managing {@link Project}s. - */ -@ApplicationScoped -public final class ProjectService { - /** - * List all projects for the user with the specified <code>userId</code>. - */ - public List<org.opendc.web.proto.user.Project> findByUser(String userId) { - return ProjectAuthorization.findByUser(userId).stream() - .map(ProjectService::toUserDto) - .toList(); - } - - /** - * Obtain the project with the specified <code>id</code> for the user with the specified <code>userId</code>. - */ - public org.opendc.web.proto.user.Project findByUser(String userId, long id) { - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); - - if (auth == null) { - return null; - } - - return toUserDto(auth); - } - - /** - * Create a new {@link Project} for the user with the specified <code>userId</code>. - */ - public org.opendc.web.proto.user.Project create(String userId, String name) { - Instant now = Instant.now(); - Project entity = new Project(name, now); - entity.persist(); - - ProjectAuthorization authorization = new ProjectAuthorization(entity, userId, ProjectRole.OWNER); - - entity.authorizations.add(authorization); - authorization.persist(); - - return toUserDto(authorization); - } - - /** - * Delete a project by its identifier. - * - * @param userId The user that invokes the action. - * @param id The identifier of the project. - */ - public org.opendc.web.proto.user.Project delete(String userId, long id) { - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); - - if (auth == null) { - return null; - } - - if (!auth.canDelete()) { - throw new IllegalArgumentException("Not allowed to delete project"); - } - - auth.project.updatedAt = Instant.now(); - org.opendc.web.proto.user.Project project = toUserDto(auth); - auth.project.delete(); - return project; - } - - /** - * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. - */ - public static org.opendc.web.proto.user.Project toUserDto(ProjectAuthorization auth) { - Project project = auth.project; - return new org.opendc.web.proto.user.Project( - project.id, project.name, project.createdAt, project.updatedAt, auth.role); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java deleted file mode 100644 index 6a70db1e..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (c) 2023 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service; - -import java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.proto.JobState; -import org.opendc.web.server.model.Job; -import org.opendc.web.server.model.Portfolio; -import org.opendc.web.server.model.ProjectAuthorization; -import org.opendc.web.server.model.Scenario; -import org.opendc.web.server.model.Topology; -import org.opendc.web.server.model.Trace; -import org.opendc.web.server.model.Workload; - -/** - * Service for managing {@link Scenario}s. - */ -@ApplicationScoped -public final class ScenarioService { - /** - * The service for managing the user accounting. - */ - private final UserAccountingService accountingService; - - /** - * Construct a {@link ScenarioService} instance. - * - * @param accountingService The {@link UserAccountingService} instance to use. - */ - public ScenarioService(UserAccountingService accountingService) { - this.accountingService = accountingService; - } - - /** - * List all {@link Scenario}s that belong a certain portfolio. - */ - public List<org.opendc.web.proto.user.Scenario> findAll(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Scenario.findByPortfolio(projectId, number).stream() - .map((s) -> toUserDto(s, auth)) - .toList(); - } - - /** - * Obtain a {@link Scenario} by identifier. - */ - public org.opendc.web.proto.user.Scenario findOne(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Scenario scenario = Scenario.findByProject(projectId, number); - - if (scenario == null) { - return null; - } - - return toUserDto(scenario, auth); - } - - /** - * Delete the specified scenario. - */ - public org.opendc.web.proto.user.Scenario delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Scenario entity = Scenario.findByProject(projectId, number); - if (entity == null) { - return null; - } - - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Scenario} with the specified data. - */ - public org.opendc.web.proto.user.Scenario create( - String userId, long projectId, int portfolioNumber, org.opendc.web.proto.user.Scenario.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); - - if (portfolio == null) { - return null; - } - - Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); - if (topology == null) { - throw new IllegalArgumentException("Referred topology does not exist"); - } - - Trace trace = Trace.findById(request.getWorkload().getTrace()); - if (trace == null) { - throw new IllegalArgumentException("Referred trace does not exist"); - } - - var now = Instant.now(); - var project = auth.project; - int number = project.allocateScenario(now); - - Scenario scenario = new Scenario( - project, - portfolio, - number, - request.getName(), - new Workload(trace, request.getWorkload().getSamplingFraction()), - topology, - request.getPhenomena(), - request.getSchedulerName()); - Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); - - // Fail the job if there is not enough budget for the simulation - if (!accountingService.hasSimulationBudget(userId)) { - job.state = JobState.FAILED; - } - - scenario.job = job; - portfolio.scenarios.add(scenario); - scenario.persist(); - - return toUserDto(scenario, auth); - } - - /** - * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. - */ - public static org.opendc.web.proto.user.Scenario toUserDto(Scenario scenario, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Scenario( - scenario.id, - scenario.number, - ProjectService.toUserDto(auth), - PortfolioService.toSummaryDto(scenario.portfolio), - scenario.name, - toDto(scenario.workload), - TopologyService.toSummaryDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName, - JobService.toUserDto(scenario.job)); - } - - /** - * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario.Summary} DTO. - */ - public static org.opendc.web.proto.user.Scenario.Summary toSummaryDto(Scenario scenario) { - return new org.opendc.web.proto.user.Scenario.Summary( - scenario.id, - scenario.number, - scenario.name, - toDto(scenario.workload), - TopologyService.toSummaryDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName, - JobService.toUserDto(scenario.job)); - } - - /** - * Convert a {@link Scenario} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Scenario toRunnerDto(Scenario scenario) { - return new org.opendc.web.proto.runner.Scenario( - scenario.id, - scenario.number, - PortfolioService.toRunnerDto(scenario.portfolio), - scenario.name, - toDto(scenario.workload), - TopologyService.toRunnerDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName); - } - - /** - * Convert a {@link Workload} entity into a DTO. - */ - public static org.opendc.web.proto.Workload toDto(Workload workload) { - return new org.opendc.web.proto.Workload(toDto(workload.trace), workload.samplingFraction); - } - - /** - * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. - */ - public static org.opendc.web.proto.Trace toDto(Trace trace) { - return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java deleted file mode 100644 index 1961995f..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (c) 2023 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service; - -import java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.server.model.Project; -import org.opendc.web.server.model.ProjectAuthorization; -import org.opendc.web.server.model.Topology; - -/** - * Service for managing {@link Topology}s. - */ -@ApplicationScoped -public final class TopologyService { - /** - * List all {@link Topology}s that belong a certain project. - */ - public List<org.opendc.web.proto.user.Topology> findAll(String userId, long projectId) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Topology.findByProject(projectId).stream() - .map((t) -> toUserDto(t, auth)) - .toList(); - } - - /** - * Find the {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code>. - */ - public org.opendc.web.proto.user.Topology findOne(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Topology topology = Topology.findByProject(projectId, number); - - if (topology == null) { - return null; - } - - return toUserDto(topology, auth); - } - - /** - * Delete the {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code> - */ - public org.opendc.web.proto.user.Topology delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Topology entity = Topology.findByProject(projectId, number); - - if (entity == null) { - return null; - } - - entity.updatedAt = Instant.now(); - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Update a {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code>. - */ - public org.opendc.web.proto.user.Topology update( - String userId, long projectId, int number, org.opendc.web.proto.user.Topology.Update request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Topology entity = Topology.findByProject(projectId, number); - - if (entity == null) { - return null; - } - - entity.updatedAt = Instant.now(); - entity.rooms = request.getRooms(); - - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Topology} with the specified name. - */ - public org.opendc.web.proto.user.Topology create( - String userId, long projectId, org.opendc.web.proto.user.Topology.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Instant now = Instant.now(); - Project project = auth.project; - int number = project.allocateTopology(now); - - Topology topology = new Topology(project, number, request.getName(), now, request.getRooms()); - - project.topologies.add(topology); - topology.persist(); - - return toUserDto(topology, auth); - } - - /** - * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. - */ - public static org.opendc.web.proto.user.Topology toUserDto(Topology topology, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Topology( - topology.id, - topology.number, - ProjectService.toUserDto(auth), - topology.name, - topology.rooms, - topology.createdAt, - topology.updatedAt); - } - - /** - * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology.Summary} DTO. - */ - public static org.opendc.web.proto.user.Topology.Summary toSummaryDto(Topology topology) { - return new org.opendc.web.proto.user.Topology.Summary( - topology.id, topology.number, topology.name, topology.createdAt, topology.updatedAt); - } - - /** - * Convert a {@link Topology} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Topology toRunnerDto(Topology topology) { - return new org.opendc.web.proto.runner.Topology( - topology.id, topology.number, topology.name, topology.rooms, topology.createdAt, topology.updatedAt); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java index 84ebd6e4..0331eacf 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java @@ -28,17 +28,26 @@ import javax.transaction.Transactional; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.opendc.web.proto.JobState; -import org.opendc.web.proto.runner.Job; import org.opendc.web.runner.JobManager; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.rest.runner.RunnerProtocol; import org.opendc.web.server.service.JobService; /** - * Implementation of {@link JobManager} that interfaces directly with {@link JobService} without overhead of the REST API. + * Implementation of {@link JobManager} that interfaces directly with the database without overhead of the REST API. */ @ApplicationScoped public class QuarkusJobManager implements JobManager { + /** + * The {@link JobService} used to manage the job's lifecycle. + */ private final JobService jobService; + /** + * Construct a {@link QuarkusJobManager}. + * + * @param jobService The {@link JobService} for managing the job's lifecycle. + */ public QuarkusJobManager(JobService jobService) { this.jobService = jobService; } @@ -46,38 +55,60 @@ public class QuarkusJobManager implements JobManager { @Transactional @Nullable @Override - public Job findNext() { - var pending = jobService.listPending(); - if (pending.isEmpty()) { + public org.opendc.web.proto.runner.Job findNext() { + var job = Job.findByState(JobState.PENDING).firstResult(); + if (job == null) { return null; } - return pending.get(0); + return RunnerProtocol.toDto(job); } + @Transactional @Override public boolean claim(long id) { - try { - jobService.updateState(id, JobState.CLAIMED, 0, null); - return true; - } catch (IllegalStateException e) { - return false; - } + return updateState(id, JobState.CLAIMED, 0, null); } + @Transactional @Override public boolean heartbeat(long id, int runtime) { - Job res = jobService.updateState(id, JobState.RUNNING, runtime, null); - return res != null && !res.getState().equals(JobState.FAILED); + return updateState(id, JobState.RUNNING, runtime, null); } + @Transactional @Override public void fail(long id, int runtime) { - jobService.updateState(id, JobState.FAILED, runtime, null); + updateState(id, JobState.FAILED, runtime, null); } + @Transactional @Override public void finish(long id, int runtime, @NotNull Map<String, ?> results) { - jobService.updateState(id, JobState.FINISHED, runtime, results); + updateState(id, JobState.FINISHED, runtime, results); + } + + /** + * Helper method to update the state of a job. + * + * @param id The unique id of the job. + * @param newState The new state to transition to. + * @param runtime The runtime of the job. + * @param results The results of the job. + * @return <code>true</code> if the operation succeeded, <code>false</code> otherwise. + */ + private boolean updateState(long id, JobState newState, int runtime, Map<String, ?> results) { + Job job = Job.findById(id); + + if (job == null) { + return false; + } + + try { + jobService.updateJob(job, newState, runtime, results); + return true; + } catch (IllegalArgumentException | IllegalStateException e) { + return false; + } } } |
