summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-server/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-server/src/main')
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java167
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java138
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java224
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java174
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java199
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java139
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt)61
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java167
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt)42
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt)32
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/SchedulerResource.kt)36
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/TraceResource.kt)44
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.kt)29
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.kt)36
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java110
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java78
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java161
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java159
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java131
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java127
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java198
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java132
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java73
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java81
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java136
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/DevSecurityOverrideFilter.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt)47
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/KotlinModuleCustomizer.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/KotlinModuleCustomizer.kt)19
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/QuarkusObjectMapperSupplier.java (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt)27
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java114
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt30
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt114
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt103
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt144
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt64
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt38
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt121
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt103
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt81
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt94
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt76
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt157
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt90
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt86
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt53
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt88
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt76
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt83
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt63
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt87
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt64
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt94
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt45
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt97
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt103
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt88
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt69
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt141
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt127
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt128
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt126
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt44
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt74
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt48
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt88
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt110
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt63
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt48
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt150
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt68
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application-test.properties2
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application.properties1
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql (renamed from opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql)74
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql24
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/hypersistence-utils.properties1
74 files changed, 2999 insertions, 3710 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
new file mode 100644
index 00000000..c5fb208e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2022 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.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.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.Table;
+import org.hibernate.annotations.Type;
+import org.opendc.web.proto.JobState;
+
+/**
+ * A simulation job to be run by the simulator.
+ */
+@Entity
+@Table(name = "jobs")
+@NamedQueries({
+ @NamedQuery(
+ name = "Job.updateOne",
+ query =
+ """
+ UPDATE Job j
+ SET j.state = :newState, j.updatedAt = :updatedAt, j.runtime = :runtime, j.results = :results
+ WHERE j.id = :id AND j.state = :oldState
+ """)
+})
+public class Job extends PanacheEntity {
+ @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)
+ public String createdBy;
+
+ @Column(name = "created_at", nullable = false, updatable = false)
+ public Instant createdAt;
+
+ /**
+ * The number of simulation runs to perform.
+ */
+ @Column(nullable = false, updatable = false)
+ public int repeats;
+
+ /**
+ * The instant at which the job was updated.
+ */
+ @Column(name = "updated_at", nullable = false)
+ public Instant updatedAt;
+
+ /**
+ * The state of the job.
+ */
+ @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType")
+ @Column(nullable = false, columnDefinition = "enum")
+ @Enumerated(EnumType.STRING)
+ public JobState state = JobState.PENDING;
+
+ /**
+ * The runtime of the job (in seconds).
+ */
+ @Column(nullable = false)
+ public int runtime = 0;
+
+ /**
+ * Experiment results in JSON
+ */
+ @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType")
+ @Column(columnDefinition = "jsonb")
+ public Map<String, ?> results = null;
+
+ /**
+ * Construct a {@link Job} instance.
+ */
+ public Job(Scenario scenario, String createdBy, Instant createdAt, int repeats) {
+ this.createdBy = createdBy;
+ this.scenario = scenario;
+ this.createdAt = createdAt;
+ this.updatedAt = createdAt;
+ this.repeats = repeats;
+ }
+
+ /**
+ * JPA constructor
+ */
+ protected Job() {}
+
+ /**
+ * Find {@link Job}s in the specified {@link JobState}.
+ *
+ * @param state The state of the jobs to find.
+ * @return A query for jobs that are in the specified state.
+ */
+ public static PanacheQuery<Job> findByState(JobState state) {
+ return find("state", state);
+ }
+
+ /**
+ * Atomically update this job.
+ *
+ * @param newState The new state to enter into.
+ * @param time The time at which the update occurs.
+ * @param results The results to possible set.
+ * @return <code>true</code> when the update succeeded`, <code>false</code> when there was a conflict.
+ */
+ public boolean updateAtomically(JobState newState, Instant time, int runtime, Map<String, ?> results) {
+ long count = update(
+ "#Job.updateOne",
+ Parameters.with("id", id)
+ .and("oldState", state)
+ .and("newState", newState)
+ .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
new file mode 100644
index 00000000..3a406683
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java
@@ -0,0 +1,138 @@
+/*
+ * 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.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.Set;
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Index;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
+import javax.persistence.OrderBy;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import org.hibernate.annotations.Type;
+import org.opendc.web.proto.Targets;
+
+/**
+ * A portfolio is the composition of multiple scenarios.
+ */
+@Entity
+@Table(
+ name = "portfolios",
+ 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(
+ name = "Portfolio.findOneByProject",
+ query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId AND p.number = :number")
+})
+public class Portfolio extends PanacheEntity {
+ /**
+ * The {@link Project} this portfolio belongs to.
+ */
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "project_id", nullable = false)
+ public Project project;
+
+ /**
+ * Unique number of the portfolio for the project.
+ */
+ @Column(nullable = false)
+ public int number;
+
+ /**
+ * The name of this portfolio.
+ */
+ @Column(nullable = false)
+ public String name;
+
+ /**
+ * The portfolio targets (metrics, repetitions).
+ */
+ @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType")
+ @Column(columnDefinition = "jsonb", nullable = false, updatable = false)
+ public Targets targets;
+
+ /**
+ * The scenarios in this portfolio.
+ */
+ @OneToMany(
+ cascade = {CascadeType.ALL},
+ mappedBy = "portfolio",
+ orphanRemoval = true)
+ @OrderBy("id ASC")
+ public Set<Scenario> scenarios = new HashSet<>();
+
+ /**
+ * Construct a {@link Portfolio} object.
+ */
+ public Portfolio(Project project, int number, String name, Targets targets) {
+ this.project = project;
+ this.number = number;
+ this.name = name;
+ this.targets = targets;
+ }
+
+ /**
+ * JPA constructor
+ */
+ protected Portfolio() {}
+
+ /**
+ * Find all {@link Portfolio}s that belong to the specified project
+ *
+ * @param projectId The unique identifier of the project.
+ * @return The query of portfolios that belong to the specified project.
+ */
+ public static PanacheQuery<Portfolio> findByProject(long projectId) {
+ return find("#Portfolio.findByProject", Parameters.with("projectId", projectId));
+ }
+
+ /**
+ * Find the {@link Portfolio} with the specified <code>number</code> belonging to the specified project.
+ *
+ * @param projectId The unique identifier of the project.
+ * @param number The number of the scenario.
+ * @return The portfolio or <code>null</code> if it does not exist.
+ */
+ public static Portfolio findByProject(long projectId, int number) {
+ return find(
+ "#Portfolio.findOneByProject",
+ Parameters.with("projectId", projectId).and("number", number))
+ .firstResult();
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java
new file mode 100644
index 00000000..5836e33f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2022 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.model;
+
+import io.quarkus.hibernate.orm.panache.Panache;
+import io.quarkus.hibernate.orm.panache.PanacheEntity;
+import io.quarkus.panache.common.Parameters;
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
+import javax.persistence.OrderBy;
+import javax.persistence.Table;
+
+/**
+ * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users.
+ */
+@Entity
+@Table(name = "projects")
+@NamedQueries({
+ @NamedQuery(
+ name = "Project.findByUser",
+ query =
+ """
+ SELECT a
+ FROM ProjectAuthorization a
+ WHERE a.key.userId = :userId
+ """),
+ @NamedQuery(
+ name = "Project.allocatePortfolio",
+ query =
+ """
+ UPDATE Project p
+ SET p.portfoliosCreated = :oldState + 1, p.updatedAt = :now
+ WHERE p.id = :id AND p.portfoliosCreated = :oldState
+ """),
+ @NamedQuery(
+ name = "Project.allocateTopology",
+ query =
+ """
+ UPDATE Project p
+ SET p.topologiesCreated = :oldState + 1, p.updatedAt = :now
+ WHERE p.id = :id AND p.topologiesCreated = :oldState
+ """),
+ @NamedQuery(
+ name = "Project.allocateScenario",
+ query =
+ """
+ UPDATE Project p
+ SET p.scenariosCreated = :oldState + 1, p.updatedAt = :now
+ WHERE p.id = :id AND p.scenariosCreated = :oldState
+ """)
+})
+public class Project extends PanacheEntity {
+ /**
+ * The name of the project.
+ */
+ @Column(nullable = false)
+ public String name;
+
+ /**
+ * The instant at which the project was created.
+ */
+ @Column(name = "created_at", nullable = false, updatable = false)
+ public Instant createdAt;
+
+ /**
+ * The instant at which the project was updated.
+ */
+ @Column(name = "updated_at", nullable = false)
+ public Instant updatedAt;
+
+ /**
+ * The portfolios belonging to this project.
+ */
+ @OneToMany(
+ cascade = {CascadeType.ALL},
+ mappedBy = "project",
+ orphanRemoval = true)
+ @OrderBy("id ASC")
+ public Set<Portfolio> portfolios = new HashSet<>();
+
+ /**
+ * The number of portfolios created for this project (including deleted portfolios).
+ */
+ @Column(name = "portfolios_created", nullable = false)
+ public int portfoliosCreated = 0;
+
+ /**
+ * The topologies belonging to this project.
+ */
+ @OneToMany(
+ cascade = {CascadeType.ALL},
+ mappedBy = "project",
+ orphanRemoval = true)
+ @OrderBy("id ASC")
+ public Set<Topology> topologies = new HashSet<>();
+
+ /**
+ * The number of topologies created for this project (including deleted topologies).
+ */
+ @Column(name = "topologies_created", nullable = false)
+ public int topologiesCreated = 0;
+
+ /**
+ * The scenarios belonging to this project.
+ */
+ @OneToMany(mappedBy = "project", orphanRemoval = true)
+ public Set<Scenario> scenarios = new HashSet<>();
+
+ /**
+ * The number of scenarios created for this project (including deleted scenarios).
+ */
+ @Column(name = "scenarios_created", nullable = false)
+ public int scenariosCreated = 0;
+
+ /**
+ * The users authorized to access the project.
+ */
+ @OneToMany(
+ cascade = {CascadeType.ALL},
+ mappedBy = "project",
+ orphanRemoval = true)
+ public Set<ProjectAuthorization> authorizations = new HashSet<>();
+
+ /**
+ * Construct a {@link Project} object.
+ */
+ public Project(String name, Instant createdAt) {
+ this.name = name;
+ this.createdAt = createdAt;
+ this.updatedAt = createdAt;
+ }
+
+ /**
+ * JPA constructor
+ */
+ protected Project() {}
+
+ /**
+ * Allocate the next portfolio number for the specified [project].
+ *
+ * @param time The time at which the new portfolio is created.
+ */
+ public int allocatePortfolio(Instant time) {
+ for (int i = 0; i < 4; i++) {
+ long count = update(
+ "#Project.allocatePortfolio",
+ Parameters.with("id", id).and("oldState", portfoliosCreated).and("now", time));
+ if (count > 0) {
+ return portfoliosCreated + 1;
+ } else {
+ Panache.getEntityManager().refresh(this);
+ }
+ }
+
+ throw new IllegalStateException("Failed to allocate next portfolio");
+ }
+
+ /**
+ * Allocate the next topology number for the specified [project].
+ *
+ * @param time The time at which the new topology is created.
+ */
+ public int allocateTopology(Instant time) {
+ for (int i = 0; i < 4; i++) {
+ long count = update(
+ "#Project.allocateTopology",
+ Parameters.with("id", id).and("oldState", topologiesCreated).and("now", time));
+ if (count > 0) {
+ return topologiesCreated + 1;
+ } else {
+ Panache.getEntityManager().refresh(this);
+ }
+ }
+
+ throw new IllegalStateException("Failed to allocate next topology");
+ }
+
+ /**
+ * Allocate the next scenario number for the specified [project].
+ *
+ * @param time The time at which the new scenario is created.
+ */
+ public int allocateScenario(Instant time) {
+ for (int i = 0; i < 4; i++) {
+ long count = update(
+ "#Project.allocateScenario",
+ Parameters.with("id", id).and("oldState", scenariosCreated).and("now", time));
+ if (count > 0) {
+ return scenariosCreated + 1;
+ } else {
+ Panache.getEntityManager().refresh(this);
+ }
+ }
+
+ throw new IllegalStateException("Failed to allocate next scenario");
+ }
+}
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
new file mode 100644
index 00000000..1238f58d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2022 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.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.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;
+
+/**
+ * An authorization for some user to participate in a project.
+ */
+@Entity
+@Table(name = "project_authorizations")
+@NamedQueries({
+ @NamedQuery(
+ name = "ProjectAuthorization.findByUser",
+ query =
+ """
+ SELECT a
+ FROM ProjectAuthorization a
+ WHERE a.key.userId = :userId
+ """),
+})
+public class ProjectAuthorization extends PanacheEntityBase {
+ /**
+ * The user identifier of the authorization.
+ */
+ @EmbeddedId
+ public ProjectAuthorization.Key key;
+
+ /**
+ * The project that the user is authorized to participate in.
+ */
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
+ @MapsId("projectId")
+ @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.
+ */
+ @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType")
+ @Column(nullable = false, columnDefinition = "enum")
+ @Enumerated(EnumType.STRING)
+ public ProjectRole role;
+
+ /**
+ * Construct a {@link ProjectAuthorization} object.
+ */
+ public ProjectAuthorization(Project project, String userId, ProjectRole role) {
+ this.key = new ProjectAuthorization.Key(project.id, userId);
+ this.project = project;
+ this.role = role;
+ }
+
+ /**
+ * JPA constructor
+ */
+ protected ProjectAuthorization() {}
+
+ /**
+ * 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 query returning projects that the user has received authorization for.
+ */
+ public static PanacheQuery<ProjectAuthorization> findByUser(String userId) {
+ return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId));
+ }
+
+ /**
+ * Find the project with <code>id</code> for the user with the specified <code>userId</code>.
+ *
+ * @param userId The identifier of the user that is requesting the list of projects.
+ * @param id The unique identifier of the project.
+ * @return The project with the specified identifier or <code>null</code> if it does not exist or is not accessible
+ * to the user with the specified identifier.
+ */
+ public static ProjectAuthorization findByUser(String userId, long id) {
+ return findById(new ProjectAuthorization.Key(id, userId));
+ }
+
+ /**
+ * Determine whether the authorization allows the user to edit the project.
+ */
+ public boolean canEdit() {
+ return switch (role) {
+ case OWNER, EDITOR -> true;
+ case VIEWER -> false;
+ };
+ }
+
+ /**
+ * Determine whether the authorization allows the user to delete the project.
+ */
+ public boolean canDelete() {
+ return role == ProjectRole.OWNER;
+ }
+
+ /**
+ * Key for representing a {@link ProjectAuthorization} object.
+ */
+ @Embeddable
+ public static class Key implements Serializable {
+ @Column(name = "project_id", nullable = false)
+ public long projectId;
+
+ @Column(name = "user_id", nullable = false)
+ public String userId;
+
+ public Key(long projectId, String userId) {
+ this.projectId = projectId;
+ this.userId = userId;
+ }
+
+ protected Key() {}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Key key = (Key) o;
+ return projectId == key.projectId && userId.equals(key.userId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(projectId, 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
new file mode 100644
index 00000000..016e931b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java
@@ -0,0 +1,199 @@
+/*
+ * 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.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.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import org.hibernate.annotations.Type;
+import org.opendc.web.proto.OperationalPhenomena;
+
+/**
+ * A single scenario to be explored by the simulator.
+ */
+@Entity
+@Table(
+ name = "scenarios",
+ 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(
+ name = "Scenario.findByPortfolio",
+ query =
+ """
+ SELECT s
+ FROM Scenario s
+ JOIN Portfolio p ON p.id = s.portfolio.id AND p.number = :number
+ WHERE s.project.id = :projectId
+ """),
+ @NamedQuery(
+ name = "Scenario.findOneByProject",
+ query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId AND s.number = :number")
+})
+public class Scenario extends PanacheEntity {
+ /**
+ * The {@link Project} to which this scenario belongs.
+ */
+ @ManyToOne(optional = 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, foreignKey = @ForeignKey(name = "fk_scenarios_portfolio"))
+ public Portfolio portfolio;
+
+ /**
+ * Unique number of the scenario for the project.
+ */
+ @Column(nullable = false)
+ public int number;
+
+ /**
+ * The name of the scenario.
+ */
+ @Column(nullable = false, updatable = false)
+ public String name;
+
+ /**
+ * Workload details of the scenario.
+ */
+ @Embedded
+ public Workload workload;
+
+ /**
+ * Topology details of the scenario.
+ */
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "topology_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_topology"))
+ public Topology topology;
+
+ /**
+ * Operational phenomena activated in the scenario.
+ */
+ @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType")
+ @Column(columnDefinition = "jsonb", nullable = false, updatable = false)
+ public OperationalPhenomena phenomena;
+
+ /**
+ * The name of the VM scheduler used in the scenario.
+ */
+ @Column(name = "scheduler_name", nullable = false, updatable = false)
+ public String schedulerName;
+
+ /**
+ * The {@link Job} associated with the scenario.
+ */
+ @OneToMany(
+ cascade = {CascadeType.ALL},
+ mappedBy = "scenario",
+ fetch = FetchType.LAZY)
+ public List<Job> jobs = new ArrayList<>();
+
+ /**
+ * Construct a {@link Scenario} object.
+ */
+ public Scenario(
+ Project project,
+ Portfolio portfolio,
+ int number,
+ String name,
+ Workload workload,
+ Topology topology,
+ OperationalPhenomena phenomena,
+ String schedulerName) {
+ this.project = project;
+ this.portfolio = portfolio;
+ this.number = number;
+ this.name = name;
+ this.workload = workload;
+ this.topology = topology;
+ this.phenomena = phenomena;
+ this.schedulerName = schedulerName;
+ }
+
+ /**
+ * JPA constructor
+ */
+ protected Scenario() {}
+
+ /**
+ * Find all {@link Scenario}s that belong to the specified project
+ *
+ * @param projectId The unique identifier of the project.
+ * @return The query of scenarios that belong to the specified project.
+ */
+ public static PanacheQuery<Scenario> findByProject(long projectId) {
+ return find("#Scenario.findByProject", Parameters.with("projectId", projectId));
+ }
+
+ /**
+ * Find all {@link Scenario}s that belong to the specified portfolio.
+ *
+ * @param projectId The unique identifier of the project.
+ * @param number The number of the portfolio.
+ * @return The query of scenarios that belong to the specified project and portfolio..
+ */
+ public static PanacheQuery<Scenario> findByPortfolio(long projectId, int number) {
+ return find(
+ "#Scenario.findByPortfolio",
+ Parameters.with("projectId", projectId).and("number", number));
+ }
+
+ /**
+ * Find the {@link Scenario} with the specified <code>number</code> belonging to the specified project.
+ *
+ * @param projectId The unique identifier of the project.
+ * @param number The number of the scenario.
+ * @return The scenario or <code>null</code> if it does not exist.
+ */
+ public static Scenario findByProject(long projectId, int number) {
+ return find(
+ "#Scenario.findOneByProject",
+ Parameters.with("projectId", projectId).and("number", number))
+ .firstResult();
+ }
+}
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
new file mode 100644
index 00000000..05a1ac12
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2022 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.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;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Index;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import org.hibernate.annotations.Type;
+import org.opendc.web.proto.Room;
+
+/**
+ * A datacenter design in OpenDC.
+ */
+@Entity
+@Table(
+ name = "topologies",
+ 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(
+ name = "Topology.findOneByProject",
+ query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number")
+})
+public class Topology extends PanacheEntity {
+ /**
+ * The {@link Project} to which the topology belongs.
+ */
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "project_id", nullable = false)
+ public Project project;
+
+ /**
+ * Unique number of the topology for the project.
+ */
+ @Column(nullable = false)
+ public int number;
+
+ /**
+ * The name of the topology.
+ */
+ @Column(nullable = false)
+ public String name;
+
+ /**
+ * The instant at which the topology was created.
+ */
+ @Column(name = "created_at", nullable = false, updatable = false)
+ public Instant createdAt;
+
+ /**
+ * The instant at which the topology was updated.
+ */
+ @Column(name = "updated_at", nullable = false)
+ public Instant updatedAt;
+
+ /**
+ * Datacenter design in JSON
+ */
+ @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType")
+ @Column(columnDefinition = "jsonb", nullable = false)
+ public List<Room> rooms;
+
+ /**
+ * Construct a {@link Topology} object.
+ */
+ public Topology(Project project, int number, String name, Instant createdAt, List<Room> rooms) {
+ this.project = project;
+ this.number = number;
+ this.name = name;
+ this.createdAt = createdAt;
+ this.updatedAt = createdAt;
+ this.rooms = rooms;
+ }
+
+ /**
+ * JPA constructor
+ */
+ protected Topology() {}
+
+ /**
+ * Find all [Topology]s that belong to [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @return The query of topologies that belong to the specified project.
+ */
+ public static PanacheQuery<Topology> findByProject(long projectId) {
+ return find("#Topology.findByProject", Parameters.with("projectId", projectId));
+ }
+
+ /**
+ * Find the [Topology] with the specified [number] belonging to [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @param number The number of the topology.
+ * @return The topology or `null` if it does not exist.
+ */
+ public static Topology findByProject(long projectId, int number) {
+ return find(
+ "#Topology.findOneByProject",
+ Parameters.with("projectId", projectId).and("number", number))
+ .firstResult();
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java
index 14a88c5a..36d27abc 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java
@@ -20,44 +20,53 @@
* SOFTWARE.
*/
-package org.opendc.web.server.model
+package org.opendc.web.server.model;
-import javax.persistence.Column
-import javax.persistence.Entity
-import javax.persistence.Id
-import javax.persistence.NamedQueries
-import javax.persistence.NamedQuery
-import javax.persistence.Table
+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.
- *
- * @param id The unique identifier of the trace.
- * @param name The name of the trace.
- * @param type The type of trace.
*/
@Entity
@Table(name = "traces")
-@NamedQueries(
- value = [
- NamedQuery(
- name = "Trace.findAll",
- query = "SELECT t FROM Trace t"
- )
- ]
-)
-class Trace(
+public class Trace extends PanacheEntityBase {
+ /**
+ * The unique identifier of the trace.
+ */
@Id
- val id: String,
+ public String id;
+ /**
+ * The name of the trace.
+ */
@Column(nullable = false, updatable = false)
- val name: String,
+ public String name;
+ /**
+ * The type of trace.
+ */
@Column(nullable = false, updatable = false)
- val type: String
-) {
+ public String type;
+
+ /**
+ * Construct a {@link Trace}.
+ *
+ * @param id The unique identifier of the trace.
+ * @param name The name of the trace.
+ * @param type The type of trace.
+ */
+ public Trace(String id, String name, String type) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ }
+
/**
- * Return a string representation of this trace.
+ * JPA constructor.
*/
- override fun toString(): String = "Trace[id=$id,name=$name,type=$type]"
+ protected Trace() {}
}
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java
new file mode 100644
index 00000000..fda4302f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java
@@ -0,0 +1,167 @@
+/*
+ * 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.model;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
+import io.quarkus.panache.common.Parameters;
+import java.time.LocalDate;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * Entity to track the number of simulation minutes used by a user.
+ */
+@Entity
+@Table(name = "user_accounting")
+@NamedQueries({
+ @NamedQuery(
+ name = "UserAccounting.consumeBudget",
+ query =
+ """
+ UPDATE UserAccounting a
+ SET a.simulationTime = a.simulationTime + :seconds
+ WHERE a.userId = :userId AND a.periodEnd = :periodEnd
+ """),
+ @NamedQuery(
+ name = "UserAccounting.resetBudget",
+ query =
+ """
+ UPDATE UserAccounting a
+ SET a.periodEnd = :periodEnd, a.simulationTime = :seconds
+ WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd
+ """)
+})
+public class UserAccounting extends PanacheEntityBase {
+ /**
+ * User to which this object belongs.
+ */
+ @Id
+ @Column(name = "user_id", nullable = false)
+ public String userId;
+
+ /**
+ * The end of the accounting period.
+ */
+ @Column(name = "period_end", nullable = false)
+ public LocalDate periodEnd;
+
+ /**
+ * The number of simulation seconds to be used per accounting period.
+ */
+ @Column(name = "simulation_time_budget", nullable = false)
+ public int simulationTimeBudget;
+
+ /**
+ * The number of simulation seconds used in this period. This number should reset once the accounting period has
+ * been reached.
+ */
+ @Column(name = "simulation_time", nullable = false)
+ public int simulationTime = 0;
+
+ /**
+ * Construct a new {@link UserAccounting} object.
+ *
+ * @param userId The identifier of the user that this object belongs to.
+ * @param periodEnd The end of the accounting period.
+ * @param simulationTimeBudget The number of simulation seconds available per accounting period.
+ */
+ public UserAccounting(String userId, LocalDate periodEnd, int simulationTimeBudget) {
+ this.userId = userId;
+ this.periodEnd = periodEnd;
+ this.simulationTimeBudget = simulationTimeBudget;
+ }
+
+ /**
+ * JPA constructor.
+ */
+ protected UserAccounting() {}
+
+ /**
+ * Return the {@link UserAccounting} object associated with the specified user id.
+ */
+ public static UserAccounting findByUser(String userId) {
+ return findById(userId);
+ }
+
+ /**
+ * Create a new {@link UserAccounting} object and persist it to the database.
+ *
+ * @param userId The identifier of the user that this object belongs to.
+ * @param periodEnd The end of the accounting period.
+ * @param simulationTimeBudget The number of simulation seconds available per accounting period.
+ * @param simulationTime The initial simulation time that has been consumed.
+ */
+ public static UserAccounting create(
+ String userId, LocalDate periodEnd, int simulationTimeBudget, int simulationTime) {
+ UserAccounting newAccounting = new UserAccounting(userId, periodEnd, simulationTimeBudget);
+ newAccounting.simulationTime = simulationTime;
+ newAccounting.persistAndFlush();
+ return newAccounting;
+ }
+
+ /**
+ * Atomically consume the budget for this {@link UserAccounting} object.
+ *
+ * @param seconds The number of seconds to consume from the user.
+ * @return <code>true</code> when the update succeeded, <code>false</code> when there was a conflict.
+ */
+ public boolean consumeBudget(int seconds) {
+ long count = update(
+ "#UserAccounting.consumeBudget",
+ Parameters.with("userId", userId).and("periodEnd", periodEnd).and("seconds", seconds));
+ return count > 0;
+ }
+
+ /**
+ * Atomically reset the budget for this {@link UserAccounting} object.
+ *
+ * @param periodEnd The new end period for the budget.
+ * @param seconds The number of seconds that have already been consumed.
+ * @return <code>true</code> when the update succeeded`, <code>false</code> when there was a conflict.
+ */
+ public boolean resetBudget(LocalDate periodEnd, int seconds) {
+ long count = update(
+ "#UserAccounting.resetBudget",
+ Parameters.with("userId", userId)
+ .and("oldPeriodEnd", this.periodEnd)
+ .and("periodEnd", periodEnd)
+ .and("seconds", seconds));
+ return count > 0;
+ }
+
+ /**
+ * Determine whether the user has any remaining simulation budget.
+ *
+ * @return <code>true</code> when the user still has budget left, <code>false</code> otherwise.
+ */
+ public boolean hasSimulationBudget() {
+ var today = LocalDate.now();
+
+ // The accounting period must be over or there must be budget remaining.
+ return !today.isBefore(periodEnd) || simulationTimeBudget > simulationTime;
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java
index 9c59dc25..129fb0c5 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 AtLarge Research
+ * 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
@@ -20,20 +20,42 @@
* SOFTWARE.
*/
-package org.opendc.web.server.model
+package org.opendc.web.server.model;
-import javax.persistence.Column
-import javax.persistence.Embeddable
-import javax.persistence.ManyToOne
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import javax.persistence.ManyToOne;
/**
- * Specification of the workload for a [Scenario].
+ * Specification of the workload for a {@link Scenario}
*/
@Embeddable
-class Workload(
+public class Workload {
+ /**
+ * The {@link Trace} that the workload runs.
+ */
@ManyToOne(optional = false)
- val trace: Trace,
+ public Trace trace;
+ /**
+ * The percentage of the trace that should be sampled.
+ */
@Column(name = "sampling_fraction", nullable = false, updatable = false)
- val samplingFraction: Double
-)
+ public double samplingFraction;
+
+ /**
+ * Construct a {@link Workload} object.
+ *
+ * @param trace The {@link Trace} to run as workload.
+ * @param samplingFraction The percentage of the workload to sample.
+ */
+ public Workload(Trace trace, double samplingFraction) {
+ this.trace = trace;
+ this.samplingFraction = samplingFraction;
+ }
+
+ /**
+ * JPA constructor.
+ */
+ protected Workload() {}
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java
index bd14950c..44d2d569 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * 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
@@ -20,29 +20,31 @@
* SOFTWARE.
*/
-package org.opendc.web.server.service
+package org.opendc.web.server.rest;
-import org.opendc.web.proto.Trace
-import org.opendc.web.server.repository.TraceRepository
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
+import org.opendc.web.server.model.Trace;
+import org.opendc.web.server.model.Workload;
/**
- * Service for managing [Trace]s.
+ * DTO-conversions for the base protocol.
*/
-@ApplicationScoped
-class TraceService @Inject constructor(private val repository: TraceRepository) {
+public final class BaseProtocol {
/**
- * Obtain all available workload traces.
+ * Private constructor to prevent instantiation of class.
*/
- fun findAll(): List<Trace> {
- return repository.findAll().map { it.toUserDto() }
+ 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);
}
/**
- * Obtain a workload trace by identifier.
+ * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO.
*/
- fun findById(id: String): Trace? {
- return repository.findOne(id)?.toUserDto()
+ 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/kotlin/org/opendc/web/server/rest/SchedulerResource.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java
index 919b25fc..0fd58182 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/SchedulerResource.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * 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
@@ -20,29 +20,33 @@
* SOFTWARE.
*/
-package org.opendc.web.server.rest
+package org.opendc.web.server.rest;
-import javax.ws.rs.GET
-import javax.ws.rs.Path
+import java.util.List;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
/**
* A resource representing the available schedulers that can be used during experiments.
*/
+@Produces("application/json")
@Path("/schedulers")
-class SchedulerResource {
+public final class SchedulerResource {
/**
* Obtain all available schedulers.
*/
@GET
- fun getAll() = listOf(
- "mem",
- "mem-inv",
- "core-mem",
- "core-mem-inv",
- "active-servers",
- "active-servers-inv",
- "provisioned-cores",
- "provisioned-cores-inv",
- "random"
- )
+ public List<String> getAll() {
+ return List.of(
+ "mem",
+ "mem-inv",
+ "core-mem",
+ "core-mem-inv",
+ "active-servers",
+ "active-servers-inv",
+ "provisioned-cores",
+ "provisioned-cores-inv",
+ "random");
+ }
}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/TraceResource.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java
index a33bd8f1..7316c93f 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/TraceResource.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * 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
@@ -20,27 +20,30 @@
* SOFTWARE.
*/
-package org.opendc.web.server.rest
+package org.opendc.web.server.rest;
-import org.opendc.web.proto.Trace
-import org.opendc.web.server.service.TraceService
-import javax.inject.Inject
-import javax.ws.rs.GET
-import javax.ws.rs.Path
-import javax.ws.rs.PathParam
-import javax.ws.rs.WebApplicationException
+import java.util.List;
+import java.util.stream.Stream;
+import javax.ws.rs.GET;
+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.model.Trace;
/**
* A resource representing the workload traces available in the OpenDC instance.
*/
+@Produces("application/json")
@Path("/traces")
-class TraceResource @Inject constructor(private val traceService: TraceService) {
+public final class TraceResource {
/**
* Obtain all available traces.
*/
@GET
- fun getAll(): List<Trace> {
- return traceService.findAll()
+ public List<org.opendc.web.proto.Trace> getAll() {
+ Stream<Trace> entities = Trace.streamAll();
+ return entities.map(TraceResource::toDto).toList();
}
/**
@@ -48,7 +51,20 @@ class TraceResource @Inject constructor(private val traceService: TraceService)
*/
@GET
@Path("{id}")
- fun get(@PathParam("id") id: String): Trace {
- return traceService.findById(id) ?: throw WebApplicationException("Trace not found", 404)
+ public org.opendc.web.proto.Trace get(@PathParam("id") String id) {
+ Trace trace = Trace.findById(id);
+
+ if (trace == null) {
+ throw new WebApplicationException("Trace not found", 404);
+ }
+
+ return toDto(trace);
+ }
+
+ /**
+ * 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/kotlin/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java
index e50917aa..3b6be42e 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 AtLarge Research
+ * 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
@@ -20,24 +20,27 @@
* SOFTWARE.
*/
-package org.opendc.web.server.rest.error
+package org.opendc.web.server.rest.error;
-import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
-import org.opendc.web.proto.ProtocolError
-import javax.ws.rs.core.MediaType
-import javax.ws.rs.core.Response
-import javax.ws.rs.ext.ExceptionMapper
-import javax.ws.rs.ext.Provider
+import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import org.opendc.web.proto.ProtocolError;
/**
* An [ExceptionMapper] for [MissingKotlinParameterException] thrown by Jackson.
*/
@Provider
-class MissingKotlinParameterExceptionMapper : ExceptionMapper<MissingKotlinParameterException> {
- override fun toResponse(exception: MissingKotlinParameterException): Response {
+public final class MissingKotlinParameterExceptionMapper implements ExceptionMapper<MissingKotlinParameterException> {
+ @Override
+ public Response toResponse(MissingKotlinParameterException exception) {
return Response.status(Response.Status.BAD_REQUEST)
- .entity(ProtocolError(Response.Status.BAD_REQUEST.statusCode, "Field '${exception.parameter.name}' is missing from body."))
- .type(MediaType.APPLICATION_JSON)
- .build()
+ .entity(new ProtocolError(
+ Response.Status.BAD_REQUEST.getStatusCode(),
+ "Field " + exception.getParameter().getName() + " is missing from body."))
+ .type(MediaType.APPLICATION_JSON)
+ .build();
}
}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.java
index aa046abf..ad1bb05e 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 AtLarge Research
+ * 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
@@ -20,26 +20,32 @@
* SOFTWARE.
*/
-package org.opendc.web.server.rest.error
+package org.opendc.web.server.rest.error;
-import org.opendc.web.proto.ProtocolError
-import javax.ws.rs.WebApplicationException
-import javax.ws.rs.core.MediaType
-import javax.ws.rs.core.Response
-import javax.ws.rs.ext.ExceptionMapper
-import javax.ws.rs.ext.Provider
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import org.opendc.web.proto.ProtocolError;
/**
- * Helper class to transform a [WebApplicationException] into an JSON error response.
+ * Helper class to transform a {@link WebApplicationException} into an JSON error response.
*/
@Provider
-class WebApplicationExceptionMapper : ExceptionMapper<WebApplicationException> {
- override fun toResponse(exception: WebApplicationException): Response {
- val code = exception.response.status
+public final class WebApplicationExceptionMapper implements ExceptionMapper<WebApplicationException> {
+ @Override
+ public Response toResponse(WebApplicationException exception) {
+ int code = exception.getResponse().getStatus();
+
+ String message = exception.getMessage();
+ if (message == null) {
+ message = "Unknown error";
+ }
return Response.status(code)
- .entity(ProtocolError(code, exception.message ?: "Unknown error"))
- .type(MediaType.APPLICATION_JSON)
- .build()
+ .entity(new ProtocolError(code, message))
+ .type(MediaType.APPLICATION_JSON)
+ .build();
}
}
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
new file mode 100644
index 00000000..dff52526
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java
@@ -0,0 +1,110 @@
+/*
+ * 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 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.JobState;
+import org.opendc.web.server.model.Job;
+import org.opendc.web.server.service.JobService;
+
+/**
+ * A resource representing the available simulation jobs.
+ */
+@Produces("application/json")
+@Path("/jobs")
+@RolesAllowed("runner")
+public final class JobResource {
+ /**
+ * The {@link JobService} for helping manage the job lifecycle.
+ */
+ private final JobService jobService;
+
+ /**
+ * Construct a {@link JobResource} instance.
+ *
+ * @param jobService The {@link JobService} for managing the job lifecycle.
+ */
+ public JobResource(JobService jobService) {
+ this.jobService = jobService;
+ }
+
+ /**
+ * Obtain all pending simulation jobs.
+ */
+ @GET
+ public List<org.opendc.web.proto.runner.Job> queryPending() {
+ return Job.findByState(JobState.PENDING).list().stream()
+ .map(RunnerProtocol::toDto)
+ .toList();
+ }
+
+ /**
+ * Get a job by identifier.
+ */
+ @GET
+ @Path("{job}")
+ public org.opendc.web.proto.runner.Job get(@PathParam("job") long id) {
+ Job job = Job.findById(id);
+
+ if (job == null) {
+ throw new WebApplicationException("Job not found", 404);
+ }
+
+ return RunnerProtocol.toDto(job);
+ }
+
+ /**
+ * Atomically update the state of a job.
+ */
+ @POST
+ @Path("{job}")
+ @Consumes("application/json")
+ @Transactional
+ public org.opendc.web.proto.runner.Job update(
+ @PathParam("job") long id, @Valid org.opendc.web.proto.runner.Job.Update update) {
+ Job job = Job.findById(id);
+ if (job == null) {
+ throw new WebApplicationException("Job not found", 404);
+ }
+
+ 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
new file mode 100644
index 00000000..d1fc980d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java
@@ -0,0 +1,161 @@
+/*
+ * 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 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;
+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.model.Portfolio;
+import org.opendc.web.server.model.ProjectAuthorization;
+
+/**
+ * A resource representing the portfolios of a project.
+ */
+@Produces("application/json")
+@Path("/projects/{project}/portfolios")
+@RolesAllowed("openid")
+public final class PortfolioResource {
+ /**
+ * The identity of the current user.
+ */
+ private final SecurityIdentity identity;
+
+ /**
+ * Construct a {@link PortfolioResource}.
+ *
+ * @param identity The {@link SecurityIdentity} of the current user.
+ */
+ public PortfolioResource(SecurityIdentity identity) {
+ this.identity = identity;
+ }
+
+ /**
+ * Get all portfolios that belong to the specified project.
+ */
+ @GET
+ public List<org.opendc.web.proto.user.Portfolio> getAll(@PathParam("project") long 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();
+ }
+
+ /**
+ * Create a portfolio for this project.
+ */
+ @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) {
+ // 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);
+ }
+
+ 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);
+ }
+
+ /**
+ * Obtain a portfolio by its identifier.
+ */
+ @GET
+ @Path("{portfolio}")
+ public org.opendc.web.proto.user.Portfolio get(
+ @PathParam("project") long projectId, @PathParam("portfolio") int 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 UserProtocol.toDto(portfolio, auth);
+ }
+
+ /**
+ * Delete a portfolio.
+ */
+ @DELETE
+ @Path("{portfolio}")
+ @Transactional
+ public org.opendc.web.proto.user.Portfolio delete(
+ @PathParam("project") long projectId, @PathParam("portfolio") int 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);
+ } 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);
+ }
+
+ 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
new file mode 100644
index 00000000..a058cd31
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java
@@ -0,0 +1,159 @@
+/*
+ * 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 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.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 accounting.
+ */
+ private final UserAccountingService accountingService;
+
+ /**
+ * The identity of the current user.
+ */
+ private final SecurityIdentity identity;
+
+ /**
+ * Construct a {@link PortfolioScenarioResource}.
+ *
+ * @param accountingService The {@link UserAccountingService} instance to use.
+ * @param identity The {@link SecurityIdentity} of the current user.
+ */
+ public PortfolioScenarioResource(UserAccountingService accountingService, SecurityIdentity identity) {
+ this.accountingService = accountingService;
+ this.identity = identity;
+ }
+
+ /**
+ * Get all scenarios that belong to the specified portfolio.
+ */
+ @GET
+ 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();
+ }
+
+ /**
+ * Create a scenario for this portfolio.
+ */
+ @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) {
+ // 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);
+ }
+
+ 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
new file mode 100644
index 00000000..da47c3ff
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java
@@ -0,0 +1,131 @@
+/*
+ * 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 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;
+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.ProjectRole;
+import org.opendc.web.server.model.Project;
+import org.opendc.web.server.model.ProjectAuthorization;
+
+/**
+ * A resource representing the created projects.
+ */
+@Produces("application/json")
+@Path("/projects")
+@RolesAllowed("openid")
+public final class ProjectResource {
+ /**
+ * The identity of the current user.
+ */
+ private final SecurityIdentity identity;
+
+ /**
+ * Construct a {@link ProjectResource}.
+ *
+ * @param identity The {@link SecurityIdentity} of the current user.
+ */
+ public ProjectResource(SecurityIdentity identity) {
+ this.identity = identity;
+ }
+
+ /**
+ * Obtain all the projects of the current user.
+ */
+ @GET
+ public List<org.opendc.web.proto.user.Project> getAll() {
+ return ProjectAuthorization.findByUser(identity.getPrincipal().getName()).list().stream()
+ .map(UserProtocol::toDto)
+ .toList();
+ }
+
+ /**
+ * Create a new project for the current user.
+ */
+ @POST
+ @Transactional
+ @Consumes("application/json")
+ public org.opendc.web.proto.user.Project create(@Valid org.opendc.web.proto.user.Project.Create request) {
+ 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);
+ }
+
+ /**
+ * Obtain a single project by its identifier.
+ */
+ @GET
+ @Path("{project}")
+ public org.opendc.web.proto.user.Project get(@PathParam("project") long id) {
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id);
+
+ if (auth == null) {
+ throw new WebApplicationException("Project not found", 404);
+ }
+
+ return UserProtocol.toDto(auth);
+ }
+
+ /**
+ * Delete a project.
+ */
+ @DELETE
+ @Path("{project}")
+ @Transactional
+ public org.opendc.web.proto.user.Project delete(@PathParam("project") long id) {
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id);
+
+ 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
new file mode 100644
index 00000000..cf933c32
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java
@@ -0,0 +1,127 @@
+/*
+ * 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 io.quarkus.security.identity.SecurityIdentity;
+import java.util.List;
+import javax.annotation.security.RolesAllowed;
+import javax.transaction.Transactional;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+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.model.ProjectAuthorization;
+import org.opendc.web.server.model.Scenario;
+
+/**
+ * A resource representing the scenarios of a portfolio.
+ */
+@Produces("application/json")
+@Path("/projects/{project}/scenarios")
+@RolesAllowed("openid")
+public final class ScenarioResource {
+ /**
+ * The identity of the current user.
+ */
+ private final SecurityIdentity identity;
+
+ /**
+ * Construct a {@link ScenarioResource}.
+ *
+ * @param identity The {@link SecurityIdentity} of the current user.
+ */
+ 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) {
+ // 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 UserProtocol.toDto(scenario, auth);
+ }
+
+ /**
+ * Delete a scenario.
+ */
+ @DELETE
+ @Path("{scenario}")
+ @Transactional
+ public org.opendc.web.proto.user.Scenario delete(
+ @PathParam("project") long projectId, @PathParam("scenario") int 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);
+ } 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);
+ }
+
+ 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
new file mode 100644
index 00000000..2b66b64b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java
@@ -0,0 +1,198 @@
+/*
+ * 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 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;
+import javax.ws.rs.PUT;
+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.model.Project;
+import org.opendc.web.server.model.ProjectAuthorization;
+import org.opendc.web.server.model.Topology;
+
+/**
+ * A resource representing the constructed datacenter topologies.
+ */
+@Produces("application/json")
+@Path("/projects/{project}/topologies")
+@RolesAllowed("openid")
+public final class TopologyResource {
+ /**
+ * The identity of the current user.
+ */
+ private final SecurityIdentity identity;
+
+ /**
+ * Construct a {@link TopologyResource}.
+ *
+ * @param identity The {@link SecurityIdentity} of the current user.
+ */
+ public TopologyResource(SecurityIdentity identity) {
+ this.identity = identity;
+ }
+
+ /**
+ * Get all topologies that belong to the specified project.
+ */
+ @GET
+ public List<org.opendc.web.proto.user.Topology> getAll(@PathParam("project") long 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();
+ }
+
+ /**
+ * Create a topology for this project.
+ */
+ @POST
+ @Consumes("application/json")
+ @Transactional
+ public org.opendc.web.proto.user.Topology create(
+ @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Topology.Create request) {
+ // User must have access to project
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId);
+
+ if (auth == null) {
+ throw new WebApplicationException("Topology not found", 404);
+ } else if (!auth.canEdit()) {
+ throw new WebApplicationException("Not permitted to edit project", 403);
+ }
+
+ 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);
+ }
+
+ /**
+ * Obtain a topology by its number.
+ */
+ @GET
+ @Path("{topology}")
+ public org.opendc.web.proto.user.Topology get(
+ @PathParam("project") long projectId, @PathParam("topology") int 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 UserProtocol.toDto(topology, auth);
+ }
+
+ /**
+ * Update the specified topology by its number.
+ */
+ @PUT
+ @Path("{topology}")
+ @Consumes("application/json")
+ @Transactional
+ public org.opendc.web.proto.user.Topology update(
+ @PathParam("project") long projectId,
+ @PathParam("topology") int number,
+ @Valid org.opendc.web.proto.user.Topology.Update request) {
+ // User must have access to project
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId);
+
+ 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);
+ }
+
+ entity.updatedAt = Instant.now();
+ entity.rooms = request.getRooms();
+
+ return UserProtocol.toDto(entity, auth);
+ }
+
+ /**
+ * Delete the specified topology.
+ */
+ @Path("{topology}")
+ @DELETE
+ @Transactional
+ public org.opendc.web.proto.user.Topology delete(
+ @PathParam("project") long projectId, @PathParam("topology") int 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);
+ } 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);
+ }
+
+ 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/rest/user/UserResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java
new file mode 100644
index 00000000..c3fb2866
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java
@@ -0,0 +1,73 @@
+/*
+ * 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 io.quarkus.security.identity.SecurityIdentity;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import org.opendc.web.proto.user.User;
+import org.opendc.web.proto.user.UserAccounting;
+import org.opendc.web.server.service.UserAccountingService;
+
+/**
+ * A resource representing the active user.
+ */
+@Produces("application/json")
+@Path("/users")
+@RolesAllowed("openid")
+public final class UserResource {
+ /**
+ * The service for managing the user accounting.
+ */
+ private final UserAccountingService accountingService;
+
+ /**
+ * The identity of the current user.
+ */
+ private final SecurityIdentity identity;
+
+ /**
+ * Construct a {@link UserResource}.
+ *
+ * @param accountingService The {@link UserAccountingService} instance to use.
+ * @param identity The {@link SecurityIdentity} of the current user.
+ */
+ public UserResource(UserAccountingService accountingService, SecurityIdentity identity) {
+ this.accountingService = accountingService;
+ this.identity = identity;
+ }
+
+ /**
+ * Get the current active user data.
+ */
+ @GET
+ @Path("me")
+ public User get() {
+ String userId = identity.getPrincipal().getName();
+ UserAccounting accounting = accountingService.getAccounting(userId);
+
+ return new User(userId, accounting);
+ }
+}
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
new file mode 100644
index 00000000..ed0eaf9c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java
@@ -0,0 +1,81 @@
+/*
+ * 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.Map;
+import javax.enterprise.context.ApplicationScoped;
+import org.opendc.web.proto.JobState;
+import org.opendc.web.server.model.Job;
+
+/**
+ * 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 {@link UserAccountingService} responsible for accounting the simulation time of users.
+ */
+ private final UserAccountingService accountingService;
+
+ /**
+ * Construct a {@link JobService} instance.
+ *
+ * @param accountingService The {@link UserAccountingService} for accounting the simulation time of users.
+ */
+ public JobService(UserAccountingService accountingService) {
+ this.accountingService = accountingService;
+ }
+
+ /**
+ * Update the job state.
+ *
+ * @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 void updateJob(Job job, JobState newState, int runtime, Map<String, ?> results) {
+ JobState state = job.state;
+
+ 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 - job.runtime);
+
+ // Check whether the user still has any simulation budget left
+ if (accountingService.consumeSimulationBudget(job.createdBy, consumedBudget) && nextState == JobState.RUNNING) {
+ nextState = JobState.FAILED; // User has consumed all their budget; cancel the job
+ }
+
+ if (!job.updateAtomically(nextState, now, runtime, results)) {
+ throw new IllegalStateException("Conflicting update");
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java
new file mode 100644
index 00000000..e5003cb4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java
@@ -0,0 +1,136 @@
+/*
+ * 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.Duration;
+import java.time.LocalDate;
+import java.time.temporal.TemporalAdjusters;
+import javax.enterprise.context.ApplicationScoped;
+import javax.persistence.EntityExistsException;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.opendc.web.server.model.UserAccounting;
+
+/**
+ * Service to track the simulation budget of users.
+ */
+@ApplicationScoped
+public final class UserAccountingService {
+ /**
+ * The default simulation budget for new users.
+ */
+ private final Duration simulationBudget;
+
+ /**
+ * Construct a {@link UserAccountingService} instance.
+ *
+ * @param simulationBudget The default simulation budget for new users.
+ */
+ public UserAccountingService(
+ @ConfigProperty(name = "opendc.accounting.simulation-budget", defaultValue = "2000m")
+ Duration simulationBudget) {
+ this.simulationBudget = simulationBudget;
+ }
+
+ /**
+ * Return the {@link org.opendc.web.proto.user.UserAccounting} object for the user with the
+ * specified <code>userId</code>. If the object does not exist in the database, a default value is constructed.
+ */
+ public org.opendc.web.proto.user.UserAccounting getAccounting(String userId) {
+ UserAccounting accounting = UserAccounting.findByUser(userId);
+ if (accounting != null) {
+ return new org.opendc.web.proto.user.UserAccounting(
+ accounting.periodEnd, accounting.simulationTime, accounting.simulationTimeBudget);
+ }
+
+ return new org.opendc.web.proto.user.UserAccounting(
+ getNextAccountingPeriod(LocalDate.now()), 0, (int) simulationBudget.toSeconds());
+ }
+
+ /**
+ * Determine whether the user with <code>userId</code> has any remaining simulation budget.
+ *
+ * @param userId The unique identifier of the user.
+ * @return <code>true</code> when the user still has budget left, <code>false</code> otherwise.
+ */
+ public boolean hasSimulationBudget(String userId) {
+ UserAccounting accounting = UserAccounting.findByUser(userId);
+ if (accounting == null) {
+ return true;
+ }
+ return accounting.hasSimulationBudget();
+ }
+
+ /**
+ * Consume <code>seconds</code> from the simulation budget of the user with <code>userId</code>.
+ *
+ * @param userId The unique identifier of the user.
+ * @param seconds The seconds to consume from the simulation budget.
+ * @return <code>true</code> if the user has consumed his full budget or <code>false</code> if there is still budget
+ * remaining.
+ */
+ public boolean consumeSimulationBudget(String userId, int seconds) {
+ LocalDate today = LocalDate.now();
+ LocalDate nextAccountingPeriod = getNextAccountingPeriod(today);
+
+ // We need to be careful to prevent conflicts in case of concurrency
+ // 1. First, we try to create the accounting object if it does not exist yet. This may fail if another instance
+ // creates the object concurrently.
+ // 2. Second, we check if the budget needs to be reset and try this atomically.
+ // 3. Finally, we atomically consume the budget from the object
+ // This is repeated three times in case there is a conflict
+ for (int i = 0; i < 3; i++) {
+ UserAccounting accounting = UserAccounting.findByUser(userId);
+
+ if (accounting == null) {
+ try {
+ UserAccounting newAccounting = UserAccounting.create(
+ userId, nextAccountingPeriod, (int) simulationBudget.toSeconds(), seconds);
+ return !newAccounting.hasSimulationBudget();
+ } catch (EntityExistsException e) {
+ // Conflict due to concurrency; retry
+ }
+ } else {
+ boolean success;
+
+ if (!today.isBefore(accounting.periodEnd)) {
+ success = accounting.resetBudget(nextAccountingPeriod, seconds);
+ } else {
+ success = accounting.consumeBudget(seconds);
+ }
+
+ if (success) {
+ return !accounting.hasSimulationBudget();
+ }
+ }
+ }
+
+ throw new IllegalStateException("Failed to allocate consume budget due to conflict");
+ }
+
+ /**
+ * Helper method to find next accounting period.
+ */
+ private static LocalDate getNextAccountingPeriod(LocalDate today) {
+ return today.with(TemporalAdjusters.firstDayOfNextMonth());
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/DevSecurityOverrideFilter.java
index 0bdf959a..de4478cb 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/DevSecurityOverrideFilter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 AtLarge Research
+ * Copyright (c) 2021 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
@@ -20,15 +20,15 @@
* SOFTWARE.
*/
-package org.opendc.web.server.util
+package org.opendc.web.server.util;
-import io.quarkus.arc.properties.IfBuildProperty
-import java.security.Principal
-import javax.ws.rs.container.ContainerRequestContext
-import javax.ws.rs.container.ContainerRequestFilter
-import javax.ws.rs.container.PreMatching
-import javax.ws.rs.core.SecurityContext
-import javax.ws.rs.ext.Provider
+import io.quarkus.arc.properties.IfBuildProperty;
+import java.security.Principal;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.PreMatching;
+import javax.ws.rs.core.SecurityContext;
+import javax.ws.rs.ext.Provider;
/**
* Helper class to disable security for the OpenDC web API when in development mode.
@@ -36,16 +36,29 @@ import javax.ws.rs.ext.Provider
@Provider
@PreMatching
@IfBuildProperty(name = "opendc.security.enabled", stringValue = "false")
-class DevSecurityOverrideFilter : ContainerRequestFilter {
- override fun filter(requestContext: ContainerRequestContext) {
- requestContext.securityContext = object : SecurityContext {
- override fun getUserPrincipal(): Principal = Principal { "anon" }
+public class DevSecurityOverrideFilter implements ContainerRequestFilter {
+ @Override
+ public void filter(ContainerRequestContext requestContext) {
+ requestContext.setSecurityContext(new SecurityContext() {
+ @Override
+ public Principal getUserPrincipal() {
+ return () -> "anon";
+ }
- override fun isSecure(): Boolean = false
+ @Override
+ public boolean isUserInRole(String role) {
+ return true;
+ }
- override fun isUserInRole(role: String): Boolean = true
+ @Override
+ public boolean isSecure() {
+ return false;
+ }
- override fun getAuthenticationScheme(): String = "basic"
- }
+ @Override
+ public String getAuthenticationScheme() {
+ return "basic";
+ }
+ });
}
}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/KotlinModuleCustomizer.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/KotlinModuleCustomizer.java
index 8634c8a4..c30edcbf 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/KotlinModuleCustomizer.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/KotlinModuleCustomizer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 AtLarge Research
+ * 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
@@ -20,19 +20,20 @@
* SOFTWARE.
*/
-package org.opendc.web.server.util
+package org.opendc.web.server.util;
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.KotlinModule
-import io.quarkus.jackson.ObjectMapperCustomizer
-import javax.inject.Singleton
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.module.kotlin.KotlinModule;
+import io.quarkus.jackson.ObjectMapperCustomizer;
+import javax.inject.Singleton;
/**
* Helper class to register the Kotlin Jackson module.
*/
@Singleton
-class KotlinModuleCustomizer : ObjectMapperCustomizer {
- override fun customize(objectMapper: ObjectMapper) {
- objectMapper.registerModule(KotlinModule.Builder().build())
+public final class KotlinModuleCustomizer implements ObjectMapperCustomizer {
+ @Override
+ public void customize(ObjectMapper objectMapper) {
+ objectMapper.registerModule(new KotlinModule.Builder().build());
}
}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/QuarkusObjectMapperSupplier.java
index 2d0da3b3..e46c74ed 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/QuarkusObjectMapperSupplier.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 AtLarge Research
+ * 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
@@ -20,21 +20,20 @@
* SOFTWARE.
*/
-package org.opendc.web.server.service
+package org.opendc.web.server.util;
-import org.opendc.web.proto.user.ProjectRole
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.hypersistence.utils.hibernate.type.util.ObjectMapperSupplier;
+import io.quarkus.runtime.annotations.RegisterForReflection;
+import javax.enterprise.inject.spi.CDI;
/**
- * Flag to indicate that the user can edit a project.
+ * A supplier for an {@link ObjectMapper} used by the Hypersistence utilities.
*/
-internal val ProjectRole.canEdit: Boolean
- get() = when (this) {
- ProjectRole.OWNER, ProjectRole.EDITOR -> true
- ProjectRole.VIEWER -> false
+@RegisterForReflection
+public class QuarkusObjectMapperSupplier implements ObjectMapperSupplier {
+ @Override
+ public ObjectMapper get() {
+ return CDI.current().select(ObjectMapper.class).get();
}
-
-/**
- * Flag to indicate that the user can delete a project.
- */
-internal val ProjectRole.canDelete: Boolean
- get() = this == ProjectRole.OWNER
+}
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
new file mode 100644
index 00000000..0331eacf
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2022 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.util.runner;
+
+import java.util.Map;
+import javax.enterprise.context.ApplicationScoped;
+import javax.transaction.Transactional;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.opendc.web.proto.JobState;
+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 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;
+ }
+
+ @Transactional
+ @Nullable
+ @Override
+ public org.opendc.web.proto.runner.Job findNext() {
+ var job = Job.findByState(JobState.PENDING).firstResult();
+ if (job == null) {
+ return null;
+ }
+
+ return RunnerProtocol.toDto(job);
+ }
+
+ @Transactional
+ @Override
+ public boolean claim(long id) {
+ return updateState(id, JobState.CLAIMED, 0, null);
+ }
+
+ @Transactional
+ @Override
+ public boolean heartbeat(long id, int runtime) {
+ return updateState(id, JobState.RUNNING, runtime, null);
+ }
+
+ @Transactional
+ @Override
+ public void fail(long id, int runtime) {
+ updateState(id, JobState.FAILED, runtime, null);
+ }
+
+ @Transactional
+ @Override
+ public void finish(long id, int runtime, @NotNull Map<String, ?> 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;
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt
deleted file mode 100644
index 1a426095..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (c) 2021 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
-
-import javax.ws.rs.core.Application
-
-/**
- * [Application] definition for the OpenDC web API.
- */
-class OpenDCApplication : Application()
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt
deleted file mode 100644
index 84a71acf..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (c) 2022 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.model
-
-import org.hibernate.annotations.Type
-import org.hibernate.annotations.TypeDef
-import org.opendc.web.proto.JobState
-import org.opendc.web.server.util.hibernate.json.JsonType
-import java.time.Instant
-import javax.persistence.Column
-import javax.persistence.Entity
-import javax.persistence.FetchType
-import javax.persistence.GeneratedValue
-import javax.persistence.GenerationType
-import javax.persistence.Id
-import javax.persistence.JoinColumn
-import javax.persistence.NamedQueries
-import javax.persistence.NamedQuery
-import javax.persistence.OneToOne
-import javax.persistence.Table
-
-/**
- * A simulation job to be run by the simulator.
- */
-@TypeDef(name = "json", typeClass = JsonType::class)
-@Entity
-@Table(name = "jobs")
-@NamedQueries(
- value = [
- NamedQuery(
- name = "Job.findAll",
- query = "SELECT j FROM Job j WHERE j.state = :state"
- ),
- NamedQuery(
- name = "Job.updateOne",
- query = """
- UPDATE Job j
- SET j.state = :newState, j.updatedAt = :updatedAt, j.runtime = :runtime, j.results = :results
- WHERE j.id = :id AND j.state = :oldState
- """
- )
- ]
-)
-class Job(
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- val id: Long,
-
- @Column(name = "created_by", nullable = false, updatable = false)
- val createdBy: String,
-
- @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER)
- @JoinColumn(name = "scenario_id", nullable = false)
- val scenario: Scenario,
-
- @Column(name = "created_at", nullable = false, updatable = false)
- val createdAt: Instant,
-
- /**
- * The number of simulation runs to perform.
- */
- @Column(nullable = false, updatable = false)
- val repeats: Int
-) {
- /**
- * The instant at which the job was updated.
- */
- @Column(name = "updated_at", nullable = false)
- var updatedAt: Instant = createdAt
-
- /**
- * The state of the job.
- */
- @Column(nullable = false)
- var state: JobState = JobState.PENDING
-
- /**
- * The runtime of the job (in seconds).
- */
- @Column(nullable = false)
- var runtime: Int = 0
-
- /**
- * Experiment results in JSON
- */
- @Type(type = "json")
- @Column(columnDefinition = "jsonb")
- var results: Map<String, Any>? = null
-
- /**
- * Return a string representation of this job.
- */
- override fun toString(): String = "Job[id=$id,scenario=${scenario.id},state=$state]"
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt
deleted file mode 100644
index 09437712..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (c) 2022 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.model
-
-import org.hibernate.annotations.Type
-import org.hibernate.annotations.TypeDef
-import org.opendc.web.proto.Targets
-import org.opendc.web.server.util.hibernate.json.JsonType
-import javax.persistence.CascadeType
-import javax.persistence.Column
-import javax.persistence.Entity
-import javax.persistence.GeneratedValue
-import javax.persistence.GenerationType
-import javax.persistence.Id
-import javax.persistence.Index
-import javax.persistence.JoinColumn
-import javax.persistence.ManyToOne
-import javax.persistence.NamedQueries
-import javax.persistence.NamedQuery
-import javax.persistence.OneToMany
-import javax.persistence.OrderBy
-import javax.persistence.Table
-import javax.persistence.UniqueConstraint
-
-/**
- * A portfolio is the composition of multiple scenarios.
- */
-@TypeDef(name = "json", typeClass = JsonType::class)
-@Entity
-@Table(
- name = "portfolios",
- uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])],
- indexes = [Index(name = "fn_portfolios_number", columnList = "project_id, number")]
-)
-@NamedQueries(
- value = [
- NamedQuery(
- name = "Portfolio.findAll",
- query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId"
- ),
- NamedQuery(
- name = "Portfolio.findOne",
- query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId AND p.number = :number"
- )
- ]
-)
-class Portfolio(
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- val id: Long,
-
- /**
- * Unique number of the portfolio for the project.
- */
- @Column(nullable = false)
- val number: Int,
-
- @Column(nullable = false)
- val name: String,
-
- @ManyToOne(optional = false)
- @JoinColumn(name = "project_id", nullable = false)
- val project: Project,
-
- /**
- * The portfolio targets (metrics, repetitions).
- */
- @Type(type = "json")
- @Column(columnDefinition = "jsonb", nullable = false, updatable = false)
- val targets: Targets
-) {
- /**
- * The scenarios in this portfolio.
- */
- @OneToMany(cascade = [CascadeType.ALL], mappedBy = "portfolio", orphanRemoval = true)
- @OrderBy("id ASC")
- val scenarios: MutableSet<Scenario> = mutableSetOf()
-
- /**
- * Return a string representation of this portfolio.
- */
- override fun toString(): String = "Job[id=$id,name=$name,project=${project.id},targets=$targets]"
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt
deleted file mode 100644
index 41d1a786..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (c) 2022 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.model
-
-import java.time.Instant
-import javax.persistence.CascadeType
-import javax.persistence.Column
-import javax.persistence.Entity
-import javax.persistence.GeneratedValue
-import javax.persistence.GenerationType
-import javax.persistence.Id
-import javax.persistence.NamedQueries
-import javax.persistence.NamedQuery
-import javax.persistence.OneToMany
-import javax.persistence.OrderBy
-import javax.persistence.Table
-
-/**
- * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users.
- */
-@Entity
-@Table(name = "projects")
-@NamedQueries(
- value = [
- NamedQuery(
- name = "Project.findAll",
- query = """
- SELECT a
- FROM ProjectAuthorization a
- WHERE a.key.userId = :userId
- """
- ),
- NamedQuery(
- name = "Project.allocatePortfolio",
- query = """
- UPDATE Project p
- SET p.portfoliosCreated = :oldState + 1, p.updatedAt = :now
- WHERE p.id = :id AND p.portfoliosCreated = :oldState
- """
- ),
- NamedQuery(
- name = "Project.allocateTopology",
- query = """
- UPDATE Project p
- SET p.topologiesCreated = :oldState + 1, p.updatedAt = :now
- WHERE p.id = :id AND p.topologiesCreated = :oldState
- """
- ),
- NamedQuery(
- name = "Project.allocateScenario",
- query = """
- UPDATE Project p
- SET p.scenariosCreated = :oldState + 1, p.updatedAt = :now
- WHERE p.id = :id AND p.scenariosCreated = :oldState
- """
- )
- ]
-)
-class Project(
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- val id: Long,
-
- @Column(nullable = false)
- var name: String,
-
- @Column(name = "created_at", nullable = false, updatable = false)
- val createdAt: Instant
-) {
- /**
- * The instant at which the project was updated.
- */
- @Column(name = "updated_at", nullable = false)
- var updatedAt: Instant = createdAt
-
- /**
- * The portfolios belonging to this project.
- */
- @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true)
- @OrderBy("id ASC")
- val portfolios: MutableSet<Portfolio> = mutableSetOf()
-
- /**
- * The number of portfolios created for this project (including deleted portfolios).
- */
- @Column(name = "portfolios_created", nullable = false)
- var portfoliosCreated: Int = 0
-
- /**
- * The topologies belonging to this project.
- */
- @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true)
- @OrderBy("id ASC")
- val topologies: MutableSet<Topology> = mutableSetOf()
-
- /**
- * The number of topologies created for this project (including deleted topologies).
- */
- @Column(name = "topologies_created", nullable = false)
- var topologiesCreated: Int = 0
-
- /**
- * The scenarios belonging to this project.
- */
- @OneToMany(mappedBy = "project", orphanRemoval = true)
- val scenarios: MutableSet<Scenario> = mutableSetOf()
-
- /**
- * The number of scenarios created for this project (including deleted scenarios).
- */
- @Column(name = "scenarios_created", nullable = false)
- var scenariosCreated: Int = 0
-
- /**
- * The users authorized to access the project.
- */
- @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true)
- val authorizations: MutableSet<ProjectAuthorization> = mutableSetOf()
-
- /**
- * Return a string representation of this project.
- */
- override fun toString(): String = "Project[id=$id,name=$name]"
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt
deleted file mode 100644
index 791725cd..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (c) 2022 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.model
-
-import org.opendc.web.proto.user.ProjectRole
-import javax.persistence.Column
-import javax.persistence.EmbeddedId
-import javax.persistence.Entity
-import javax.persistence.JoinColumn
-import javax.persistence.ManyToOne
-import javax.persistence.MapsId
-import javax.persistence.Table
-
-/**
- * An authorization for some user to participate in a project.
- */
-@Entity
-@Table(name = "project_authorizations")
-class ProjectAuthorization(
- /**
- * The user identifier of the authorization.
- */
- @EmbeddedId
- val key: ProjectAuthorizationKey,
-
- /**
- * The project that the user is authorized to participate in.
- */
- @ManyToOne(optional = false)
- @MapsId("projectId")
- @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false)
- val project: Project,
-
- /**
- * The role of the user in the project.
- */
- @Column(nullable = false)
- val role: ProjectRole
-) {
- /**
- * Return a string representation of this project authorization.
- */
- override fun toString(): String = "ProjectAuthorization[project=${key.projectId},user=${key.userId},role=$role]"
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt
deleted file mode 100644
index 449b6608..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) 2022 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.model
-
-import javax.persistence.Column
-import javax.persistence.Embeddable
-
-/**
- * Key for representing a [ProjectAuthorization] object.
- */
-@Embeddable
-data class ProjectAuthorizationKey(
- @Column(name = "user_id", nullable = false)
- val userId: String,
-
- @Column(name = "project_id", nullable = false)
- val projectId: Long
-) : java.io.Serializable
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt
deleted file mode 100644
index 62adc9e2..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (c) 2022 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.model
-
-import org.hibernate.annotations.Type
-import org.hibernate.annotations.TypeDef
-import org.opendc.web.proto.OperationalPhenomena
-import org.opendc.web.server.util.hibernate.json.JsonType
-import javax.persistence.CascadeType
-import javax.persistence.Column
-import javax.persistence.Embedded
-import javax.persistence.Entity
-import javax.persistence.GeneratedValue
-import javax.persistence.GenerationType
-import javax.persistence.Id
-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.Table
-import javax.persistence.UniqueConstraint
-
-/**
- * A single scenario to be explored by the simulator.
- */
-@TypeDef(name = "json", typeClass = JsonType::class)
-@Entity
-@Table(
- name = "scenarios",
- uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])],
- indexes = [Index(name = "fn_scenarios_number", columnList = "project_id, number")]
-)
-@NamedQueries(
- value = [
- NamedQuery(
- name = "Scenario.findAll",
- query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId"
- ),
- NamedQuery(
- name = "Scenario.findAllForPortfolio",
- query = """
- SELECT s
- FROM Scenario s
- JOIN Portfolio p ON p.id = s.portfolio.id AND p.number = :number
- WHERE s.project.id = :projectId
- """
- ),
- NamedQuery(
- name = "Scenario.findOne",
- query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId AND s.number = :number"
- )
- ]
-)
-class Scenario(
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- val id: Long,
-
- /**
- * Unique number of the scenario for the project.
- */
- @Column(nullable = false)
- val number: Int,
-
- @Column(nullable = false, updatable = false)
- val name: String,
-
- @ManyToOne(optional = false)
- @JoinColumn(name = "project_id", nullable = false)
- val project: Project,
-
- @ManyToOne(optional = false)
- @JoinColumn(name = "portfolio_id", nullable = false)
- val portfolio: Portfolio,
-
- @Embedded
- val workload: Workload,
-
- @ManyToOne(optional = false)
- val topology: Topology,
-
- @Type(type = "json")
- @Column(columnDefinition = "jsonb", nullable = false, updatable = false)
- val phenomena: OperationalPhenomena,
-
- @Column(name = "scheduler_name", nullable = false, updatable = false)
- val schedulerName: String
-) {
- /**
- * The [Job] associated with the scenario.
- */
- @OneToOne(cascade = [CascadeType.ALL])
- lateinit var job: Job
-
- /**
- * Return a string representation of this scenario.
- */
- override fun toString(): String = "Scenario[id=$id,name=$name,project=${project.id},portfolio=${portfolio.id}]"
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt
deleted file mode 100644
index 26368455..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (c) 2022 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.model
-
-import org.hibernate.annotations.Type
-import org.hibernate.annotations.TypeDef
-import org.opendc.web.proto.Room
-import org.opendc.web.server.util.hibernate.json.JsonType
-import java.time.Instant
-import javax.persistence.Column
-import javax.persistence.Entity
-import javax.persistence.GeneratedValue
-import javax.persistence.GenerationType
-import javax.persistence.Id
-import javax.persistence.Index
-import javax.persistence.JoinColumn
-import javax.persistence.ManyToOne
-import javax.persistence.NamedQueries
-import javax.persistence.NamedQuery
-import javax.persistence.Table
-import javax.persistence.UniqueConstraint
-
-/**
- * A datacenter design in OpenDC.
- */
-@TypeDef(name = "json", typeClass = JsonType::class)
-@Entity
-@Table(
- name = "topologies",
- uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])],
- indexes = [Index(name = "fn_topologies_number", columnList = "project_id, number")]
-)
-@NamedQueries(
- value = [
- NamedQuery(
- name = "Topology.findAll",
- query = "SELECT t FROM Topology t WHERE t.project.id = :projectId"
- ),
- NamedQuery(
- name = "Topology.findOne",
- query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number"
- )
- ]
-)
-class Topology(
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- val id: Long,
-
- /**
- * Unique number of the topology for the project.
- */
- @Column(nullable = false)
- val number: Int,
-
- @Column(nullable = false)
- val name: String,
-
- @ManyToOne(optional = false)
- @JoinColumn(name = "project_id", nullable = false)
- val project: Project,
-
- @Column(name = "created_at", nullable = false, updatable = false)
- val createdAt: Instant,
-
- /**
- * Datacenter design in JSON
- */
- @Type(type = "json")
- @Column(columnDefinition = "jsonb", nullable = false)
- var rooms: List<Room> = emptyList()
-) {
- /**
- * The instant at which the topology was updated.
- */
- @Column(name = "updated_at", nullable = false)
- var updatedAt: Instant = createdAt
-
- /**
- * Return a string representation of this topology.
- */
- override fun toString(): String = "Topology[id=$id,name=$name,project=${project.id}]"
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt
deleted file mode 100644
index 5b813044..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (c) 2022 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.model
-
-import java.time.LocalDate
-import javax.persistence.Column
-import javax.persistence.Entity
-import javax.persistence.Id
-import javax.persistence.NamedQueries
-import javax.persistence.NamedQuery
-import javax.persistence.Table
-
-/**
- * Entity to track the number of simulation minutes used by a user.
- */
-@Entity
-@Table(name = "user_accounting")
-@NamedQueries(
- value = [
- NamedQuery(
- name = "UserAccounting.consumeBudget",
- query = """
- UPDATE UserAccounting a
- SET a.simulationTime = a.simulationTime + :seconds
- WHERE a.userId = :userId AND a.periodEnd = :periodEnd
- """
- ),
- NamedQuery(
- name = "UserAccounting.resetBudget",
- query = """
- UPDATE UserAccounting a
- SET a.periodEnd = :periodEnd, a.simulationTime = :seconds
- WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd
- """
- )
- ]
-)
-class UserAccounting(
- @Id
- @Column(name = "user_id", nullable = false)
- val userId: String,
-
- /**
- * The end of the accounting period.
- */
- @Column(name = "period_end", nullable = false)
- var periodEnd: LocalDate,
-
- /**
- * The number of simulation seconds to be used per accounting period.
- */
- @Column(name = "simulation_time_budget", nullable = false)
- var simulationTimeBudget: Int
-) {
- /**
- * The number of simulation seconds used in this period. This number should reset once the accounting period has
- * been reached.
- */
- @Column(name = "simulation_time", nullable = false)
- var simulationTime: Int = 0
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt
deleted file mode 100644
index e9bf0af0..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (c) 2022 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.repository
-
-import org.opendc.web.proto.JobState
-import org.opendc.web.server.model.Job
-import java.time.Instant
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.persistence.EntityManager
-
-/**
- * A repository to manage [Job] entities.
- */
-@ApplicationScoped
-class JobRepository @Inject constructor(private val em: EntityManager) {
- /**
- * Find all jobs currently residing in [state].
- *
- * @param state The state in which the jobs should be.
- * @return The list of jobs in state [state].
- */
- fun findAll(state: JobState): List<Job> {
- return em.createNamedQuery("Job.findAll", Job::class.java)
- .setParameter("state", state)
- .resultList
- }
-
- /**
- * Find the [Job] with the specified [id].
- *
- * @param id The unique identifier of the job.
- * @return The trace or `null` if it does not exist.
- */
- fun findOne(id: Long): Job? {
- return em.find(Job::class.java, id)
- }
-
- /**
- * Delete the specified [job].
- */
- fun delete(job: Job) {
- em.remove(job)
- }
-
- /**
- * Save the specified [job] to the database.
- */
- fun save(job: Job) {
- em.persist(job)
- }
-
- /**
- * Atomically update the specified [job].
- *
- * @param job The job to update atomically.
- * @param newState The new state to enter into.
- * @param time The time at which the update occurs.
- * @param results The results to possible set.
- * @return `true` when the update succeeded`, `false` when there was a conflict.
- */
- fun updateOne(job: Job, newState: JobState, time: Instant, runtime: Int, results: Map<String, Any>?): Boolean {
- val count = em.createNamedQuery("Job.updateOne")
- .setParameter("id", job.id)
- .setParameter("oldState", job.state)
- .setParameter("newState", newState)
- .setParameter("updatedAt", Instant.now())
- .setParameter("runtime", runtime)
- .setParameter("results", results)
- .executeUpdate()
- em.refresh(job)
- return count > 0
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt
deleted file mode 100644
index 77130c15..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2022 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.repository
-
-import org.opendc.web.server.model.Portfolio
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.persistence.EntityManager
-
-/**
- * A repository to manage [Portfolio] entities.
- */
-@ApplicationScoped
-class PortfolioRepository @Inject constructor(private val em: EntityManager) {
- /**
- * Find all [Portfolio]s that belong to [project][projectId].
- *
- * @param projectId The unique identifier of the project.
- * @return The list of portfolios that belong to the specified project.
- */
- fun findAll(projectId: Long): List<Portfolio> {
- return em.createNamedQuery("Portfolio.findAll", Portfolio::class.java)
- .setParameter("projectId", projectId)
- .resultList
- }
-
- /**
- * Find the [Portfolio] with the specified [number] belonging to [project][projectId].
- *
- * @param projectId The unique identifier of the project.
- * @param number The number of the portfolio.
- * @return The portfolio or `null` if it does not exist.
- */
- fun findOne(projectId: Long, number: Int): Portfolio? {
- return em.createNamedQuery("Portfolio.findOne", Portfolio::class.java)
- .setParameter("projectId", projectId)
- .setParameter("number", number)
- .setMaxResults(1)
- .resultList
- .firstOrNull()
- }
-
- /**
- * Delete the specified [portfolio].
- */
- fun delete(portfolio: Portfolio) {
- em.remove(portfolio)
- }
-
- /**
- * Save the specified [portfolio] to the database.
- */
- fun save(portfolio: Portfolio) {
- em.persist(portfolio)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt
deleted file mode 100644
index 519da3de..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (c) 2022 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.repository
-
-import org.opendc.web.server.model.Project
-import org.opendc.web.server.model.ProjectAuthorization
-import org.opendc.web.server.model.ProjectAuthorizationKey
-import java.time.Instant
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.persistence.EntityManager
-
-/**
- * A repository to manage [Project] entities.
- */
-@ApplicationScoped
-class ProjectRepository @Inject constructor(private val em: EntityManager) {
- /**
- * List all projects for the user with the specified [userId].
- *
- * @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.
- */
- fun findAll(userId: String): List<ProjectAuthorization> {
- return em.createNamedQuery("Project.findAll", ProjectAuthorization::class.java)
- .setParameter("userId", userId)
- .resultList
- }
-
- /**
- * Find the project with [id] for the user with the specified [userId].
- *
- * @param userId The identifier of the user that is requesting the list of projects.
- * @param id The unique identifier of the project.
- * @return The project with the specified identifier or `null` if it does not exist or is not accessible to the
- * user with the specified identifier.
- */
- fun findOne(userId: String, id: Long): ProjectAuthorization? {
- return em.find(ProjectAuthorization::class.java, ProjectAuthorizationKey(userId, id))
- }
-
- /**
- * Delete the specified [project].
- */
- fun delete(project: Project) {
- em.remove(project)
- }
-
- /**
- * Save the specified [project] to the database.
- */
- fun save(project: Project) {
- em.persist(project)
- }
-
- /**
- * Save the specified [auth] to the database.
- */
- fun save(auth: ProjectAuthorization) {
- em.persist(auth)
- }
-
- /**
- * Allocate the next portfolio number for the specified [project].
- *
- * @param project The project to allocate the portfolio number for.
- * @param time The time at which the new portfolio is created.
- * @param tries The number of times to try to allocate the number before failing.
- */
- fun allocatePortfolio(project: Project, time: Instant, tries: Int = 4): Int {
- repeat(tries) {
- val count = em.createNamedQuery("Project.allocatePortfolio")
- .setParameter("id", project.id)
- .setParameter("oldState", project.portfoliosCreated)
- .setParameter("now", time)
- .executeUpdate()
-
- if (count > 0) {
- return project.portfoliosCreated + 1
- } else {
- em.refresh(project)
- }
- }
-
- throw IllegalStateException("Failed to allocate next portfolio")
- }
-
- /**
- * Allocate the next topology number for the specified [project].
- *
- * @param project The project to allocate the topology number for.
- * @param time The time at which the new topology is created.
- * @param tries The number of times to try to allocate the number before failing.
- */
- fun allocateTopology(project: Project, time: Instant, tries: Int = 4): Int {
- repeat(tries) {
- val count = em.createNamedQuery("Project.allocateTopology")
- .setParameter("id", project.id)
- .setParameter("oldState", project.topologiesCreated)
- .setParameter("now", time)
- .executeUpdate()
-
- if (count > 0) {
- return project.topologiesCreated + 1
- } else {
- em.refresh(project)
- }
- }
-
- throw IllegalStateException("Failed to allocate next topology")
- }
-
- /**
- * Allocate the next scenario number for the specified [project].
- *
- * @param project The project to allocate the scenario number for.
- * @param time The time at which the new scenario is created.
- * @param tries The number of times to try to allocate the number before failing.
- */
- fun allocateScenario(project: Project, time: Instant, tries: Int = 4): Int {
- repeat(tries) {
- val count = em.createNamedQuery("Project.allocateScenario")
- .setParameter("id", project.id)
- .setParameter("oldState", project.scenariosCreated)
- .setParameter("now", time)
- .executeUpdate()
-
- if (count > 0) {
- return project.scenariosCreated + 1
- } else {
- em.refresh(project)
- }
- }
-
- throw IllegalStateException("Failed to allocate next scenario")
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt
deleted file mode 100644
index 145db71d..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2022 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.repository
-
-import org.opendc.web.server.model.Scenario
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.persistence.EntityManager
-
-/**
- * A repository to manage [Scenario] entities.
- */
-@ApplicationScoped
-class ScenarioRepository @Inject constructor(private val em: EntityManager) {
- /**
- * Find all [Scenario]s that belong to [project][projectId].
- *
- * @param projectId The unique identifier of the project.
- * @return The list of scenarios that belong to the specified project.
- */
- fun findAll(projectId: Long): List<Scenario> {
- return em.createNamedQuery("Scenario.findAll", Scenario::class.java)
- .setParameter("projectId", projectId)
- .resultList
- }
-
- /**
- * Find all [Scenario]s that belong to [portfolio][number] of [project][projectId].
- *
- * @param projectId The unique identifier of the project.
- * @param number The number of the portfolio to which the scenarios should belong.
- * @return The list of scenarios that belong to the specified portfolio.
- */
- fun findAll(projectId: Long, number: Int): List<Scenario> {
- return em.createNamedQuery("Scenario.findAllForPortfolio", Scenario::class.java)
- .setParameter("projectId", projectId)
- .setParameter("number", number)
- .resultList
- }
-
- /**
- * Find the [Scenario] with the specified [number] belonging to [project][projectId].
- *
- * @param projectId The unique identifier of the project.
- * @param number The number of the scenario.
- * @return The scenario or `null` if it does not exist.
- */
- fun findOne(projectId: Long, number: Int): Scenario? {
- return em.createNamedQuery("Scenario.findOne", Scenario::class.java)
- .setParameter("projectId", projectId)
- .setParameter("number", number)
- .setMaxResults(1)
- .resultList
- .firstOrNull()
- }
-
- /**
- * Delete the specified [scenario].
- */
- fun delete(scenario: Scenario) {
- em.remove(scenario)
- }
-
- /**
- * Save the specified [scenario] to the database.
- */
- fun save(scenario: Scenario) {
- em.persist(scenario)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt
deleted file mode 100644
index e8eadd63..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (c) 2022 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.repository
-
-import org.opendc.web.server.model.Topology
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.persistence.EntityManager
-
-/**
- * A repository to manage [Topology] entities.
- */
-@ApplicationScoped
-class TopologyRepository @Inject constructor(private val em: EntityManager) {
- /**
- * 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.
- */
- fun findAll(projectId: Long): List<Topology> {
- return em.createNamedQuery("Topology.findAll", Topology::class.java)
- .setParameter("projectId", projectId)
- .resultList
- }
-
- /**
- * Find the [Topology] with the specified [number] belonging to [project][projectId].
- *
- * @param projectId The unique identifier of the project.
- * @param number The number of the topology.
- * @return The topology or `null` if it does not exist.
- */
- fun findOne(projectId: Long, number: Int): Topology? {
- return em.createNamedQuery("Topology.findOne", Topology::class.java)
- .setParameter("projectId", projectId)
- .setParameter("number", number)
- .setMaxResults(1)
- .resultList
- .firstOrNull()
- }
-
- /**
- * Find the [Topology] with the specified [id].
- *
- * @param id Unique identifier of the topology.
- * @return The topology or `null` if it does not exist.
- */
- fun findOne(id: Long): Topology? {
- return em.find(Topology::class.java, id)
- }
-
- /**
- * Delete the specified [topology].
- */
- fun delete(topology: Topology) {
- em.remove(topology)
- }
-
- /**
- * Save the specified [topology] to the database.
- */
- fun save(topology: Topology) {
- em.persist(topology)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt
deleted file mode 100644
index f328eea6..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (c) 2022 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.repository
-
-import org.opendc.web.server.model.Trace
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.persistence.EntityManager
-
-/**
- * A repository to manage [Trace] entities.
- */
-@ApplicationScoped
-class TraceRepository @Inject constructor(private val em: EntityManager) {
- /**
- * Find all workload traces in the database.
- *
- * @return The list of available workload traces.
- */
- fun findAll(): List<Trace> {
- return em.createNamedQuery("Trace.findAll", Trace::class.java).resultList
- }
-
- /**
- * Find the [Trace] with the specified [id].
- *
- * @param id The unique identifier of the trace.
- * @return The trace or `null` if it does not exist.
- */
- fun findOne(id: String): Trace? {
- return em.find(Trace::class.java, id)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt
deleted file mode 100644
index f0265d3d..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (c) 2022 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.repository
-
-import org.opendc.web.server.model.UserAccounting
-import java.time.LocalDate
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.persistence.EntityManager
-
-/**
- * A repository to manage [UserAccounting] entities.
- */
-@ApplicationScoped
-class UserAccountingRepository @Inject constructor(private val em: EntityManager) {
- /**
- * Find the [UserAccounting] object for the specified [userId].
- *
- * @param userId The unique identifier of the user.
- * @return The [UserAccounting] object or `null` if it does not exist.
- */
- fun findForUser(userId: String): UserAccounting? {
- return em.find(UserAccounting::class.java, userId)
- }
-
- /**
- * Save the specified [UserAccounting] object to the database.
- */
- fun save(accounting: UserAccounting) {
- em.persist(accounting)
- }
-
- /**
- * Atomically consume the budget for the specified [UserAccounting] object.
- *
- * @param accounting The [UserAccounting] object to update atomically.
- * @param seconds The number of seconds to consume from the user.
- * @return `true` when the update succeeded`, `false` when there was a conflict.
- */
- fun consumeBudget(accounting: UserAccounting, seconds: Int): Boolean {
- val count = em.createNamedQuery("UserAccounting.consumeBudget")
- .setParameter("userId", accounting.userId)
- .setParameter("periodEnd", accounting.periodEnd)
- .setParameter("seconds", seconds)
- .executeUpdate()
- em.refresh(accounting)
- return count > 0
- }
-
- /**
- * Atomically reset the budget for the specified [UserAccounting] object.
- *
- * @param accounting The [UserAccounting] object to update atomically.
- * @param periodEnd The new end period for the budget.
- * @param seconds The number of seconds that have already been consumed.
- * @return `true` when the update succeeded`, `false` when there was a conflict.
- */
- fun resetBudget(accounting: UserAccounting, periodEnd: LocalDate, seconds: Int): Boolean {
- val count = em.createNamedQuery("UserAccounting.resetBudget")
- .setParameter("userId", accounting.userId)
- .setParameter("oldPeriodEnd", accounting.periodEnd)
- .setParameter("periodEnd", periodEnd)
- .setParameter("seconds", seconds)
- .executeUpdate()
- em.refresh(accounting)
- return count > 0
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt
deleted file mode 100644
index d0432360..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2022 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.proto.runner.Job
-import org.opendc.web.server.service.JobService
-import javax.annotation.security.RolesAllowed
-import javax.inject.Inject
-import javax.transaction.Transactional
-import javax.validation.Valid
-import javax.ws.rs.GET
-import javax.ws.rs.POST
-import javax.ws.rs.Path
-import javax.ws.rs.PathParam
-import javax.ws.rs.WebApplicationException
-
-/**
- * A resource representing the available simulation jobs.
- */
-@Path("/jobs")
-@RolesAllowed("runner")
-class JobResource @Inject constructor(private val jobService: JobService) {
- /**
- * Obtain all pending simulation jobs.
- */
- @GET
- fun queryPending(): List<Job> {
- return jobService.queryPending()
- }
-
- /**
- * Get a job by identifier.
- */
- @GET
- @Path("{job}")
- fun get(@PathParam("job") id: Long): Job {
- return jobService.findById(id) ?: throw WebApplicationException("Job not found", 404)
- }
-
- /**
- * Atomically update the state of a job.
- */
- @POST
- @Path("{job}")
- @Transactional
- fun update(@PathParam("job") id: Long, @Valid update: Job.Update): Job {
- return try {
- jobService.updateState(id, update.state, update.runtime, update.results)
- ?: throw WebApplicationException("Job not found", 404)
- } catch (e: IllegalArgumentException) {
- throw WebApplicationException(e, 400)
- } catch (e: IllegalStateException) {
- throw WebApplicationException(e, 409)
- }
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt
deleted file mode 100644
index ebe57ae2..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (c) 2022 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 io.quarkus.security.identity.SecurityIdentity
-import org.opendc.web.proto.user.Portfolio
-import org.opendc.web.server.service.PortfolioService
-import javax.annotation.security.RolesAllowed
-import javax.inject.Inject
-import javax.transaction.Transactional
-import javax.validation.Valid
-import javax.ws.rs.DELETE
-import javax.ws.rs.GET
-import javax.ws.rs.POST
-import javax.ws.rs.Path
-import javax.ws.rs.PathParam
-import javax.ws.rs.WebApplicationException
-
-/**
- * A resource representing the portfolios of a project.
- */
-@Path("/projects/{project}/portfolios")
-@RolesAllowed("openid")
-class PortfolioResource @Inject constructor(
- private val portfolioService: PortfolioService,
- private val identity: SecurityIdentity
-) {
- /**
- * Get all portfolios that belong to the specified project.
- */
- @GET
- fun getAll(@PathParam("project") projectId: Long): List<Portfolio> {
- return portfolioService.findAll(identity.principal.name, projectId)
- }
-
- /**
- * Create a portfolio for this project.
- */
- @POST
- @Transactional
- fun create(@PathParam("project") projectId: Long, @Valid request: Portfolio.Create): Portfolio {
- return portfolioService.create(identity.principal.name, projectId, request) ?: throw WebApplicationException("Project not found", 404)
- }
-
- /**
- * Obtain a portfolio by its identifier.
- */
- @GET
- @Path("{portfolio}")
- fun get(@PathParam("project") projectId: Long, @PathParam("portfolio") number: Int): Portfolio {
- return portfolioService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Portfolio not found", 404)
- }
-
- /**
- * Delete a portfolio.
- */
- @DELETE
- @Path("{portfolio}")
- @Transactional
- fun delete(@PathParam("project") projectId: Long, @PathParam("portfolio") number: Int): Portfolio {
- return portfolioService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Portfolio not found", 404)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt
deleted file mode 100644
index 82f35127..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (c) 2022 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 io.quarkus.security.identity.SecurityIdentity
-import org.opendc.web.proto.user.Scenario
-import org.opendc.web.server.service.ScenarioService
-import javax.annotation.security.RolesAllowed
-import javax.inject.Inject
-import javax.transaction.Transactional
-import javax.validation.Valid
-import javax.ws.rs.GET
-import javax.ws.rs.POST
-import javax.ws.rs.Path
-import javax.ws.rs.PathParam
-import javax.ws.rs.WebApplicationException
-
-/**
- * A resource representing the scenarios of a portfolio.
- */
-@Path("/projects/{project}/portfolios/{portfolio}/scenarios")
-@RolesAllowed("openid")
-class PortfolioScenarioResource @Inject constructor(
- private val scenarioService: ScenarioService,
- private val identity: SecurityIdentity
-) {
- /**
- * Get all scenarios that belong to the specified portfolio.
- */
- @GET
- fun get(@PathParam("project") projectId: Long, @PathParam("portfolio") portfolioNumber: Int): List<Scenario> {
- return scenarioService.findAll(identity.principal.name, projectId, portfolioNumber)
- }
-
- /**
- * Create a scenario for this portfolio.
- */
- @POST
- @Transactional
- fun create(@PathParam("project") projectId: Long, @PathParam("portfolio") portfolioNumber: Int, @Valid request: Scenario.Create): Scenario {
- return scenarioService.create(identity.principal.name, projectId, portfolioNumber, request) ?: throw WebApplicationException("Portfolio not found", 404)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt
deleted file mode 100644
index 817f53a5..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (c) 2022 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 io.quarkus.security.identity.SecurityIdentity
-import org.opendc.web.proto.user.Project
-import org.opendc.web.server.service.ProjectService
-import javax.annotation.security.RolesAllowed
-import javax.inject.Inject
-import javax.transaction.Transactional
-import javax.validation.Valid
-import javax.ws.rs.DELETE
-import javax.ws.rs.GET
-import javax.ws.rs.POST
-import javax.ws.rs.Path
-import javax.ws.rs.PathParam
-import javax.ws.rs.WebApplicationException
-
-/**
- * A resource representing the created projects.
- */
-@Path("/projects")
-@RolesAllowed("openid")
-class ProjectResource @Inject constructor(
- private val projectService: ProjectService,
- private val identity: SecurityIdentity
-) {
- /**
- * Obtain all the projects of the current user.
- */
- @GET
- fun getAll(): List<Project> {
- return projectService.findWithUser(identity.principal.name)
- }
-
- /**
- * Create a new project for the current user.
- */
- @POST
- @Transactional
- fun create(@Valid request: Project.Create): Project {
- return projectService.createForUser(identity.principal.name, request.name)
- }
-
- /**
- * Obtain a single project by its identifier.
- */
- @GET
- @Path("{project}")
- fun get(@PathParam("project") id: Long): Project {
- return projectService.findWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404)
- }
-
- /**
- * Delete a project.
- */
- @DELETE
- @Path("{project}")
- @Transactional
- fun delete(@PathParam("project") id: Long): Project {
- try {
- return projectService.deleteWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404)
- } catch (e: IllegalArgumentException) {
- throw WebApplicationException(e.message, 403)
- }
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt
deleted file mode 100644
index 56bb4290..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (c) 2022 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 io.quarkus.security.identity.SecurityIdentity
-import org.opendc.web.proto.user.Scenario
-import org.opendc.web.server.service.ScenarioService
-import javax.annotation.security.RolesAllowed
-import javax.inject.Inject
-import javax.transaction.Transactional
-import javax.ws.rs.DELETE
-import javax.ws.rs.GET
-import javax.ws.rs.Path
-import javax.ws.rs.PathParam
-import javax.ws.rs.WebApplicationException
-
-/**
- * A resource representing the scenarios of a portfolio.
- */
-@Path("/projects/{project}/scenarios")
-@RolesAllowed("openid")
-class ScenarioResource @Inject constructor(
- private val scenarioService: ScenarioService,
- private val identity: SecurityIdentity
-) {
- /**
- * Obtain a scenario by its identifier.
- */
- @GET
- @Path("{scenario}")
- fun get(@PathParam("project") projectId: Long, @PathParam("scenario") number: Int): Scenario {
- return scenarioService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Scenario not found", 404)
- }
-
- /**
- * Delete a scenario.
- */
- @DELETE
- @Path("{scenario}")
- @Transactional
- fun delete(@PathParam("project") projectId: Long, @PathParam("scenario") number: Int): Scenario {
- return scenarioService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Scenario not found", 404)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt
deleted file mode 100644
index 8eef66c8..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (c) 2022 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 io.quarkus.security.identity.SecurityIdentity
-import org.opendc.web.proto.user.Topology
-import org.opendc.web.server.service.TopologyService
-import javax.annotation.security.RolesAllowed
-import javax.inject.Inject
-import javax.transaction.Transactional
-import javax.validation.Valid
-import javax.ws.rs.DELETE
-import javax.ws.rs.GET
-import javax.ws.rs.POST
-import javax.ws.rs.PUT
-import javax.ws.rs.Path
-import javax.ws.rs.PathParam
-import javax.ws.rs.WebApplicationException
-
-/**
- * A resource representing the constructed datacenter topologies.
- */
-@Path("/projects/{project}/topologies")
-@RolesAllowed("openid")
-class TopologyResource @Inject constructor(
- private val topologyService: TopologyService,
- private val identity: SecurityIdentity
-) {
- /**
- * Get all topologies that belong to the specified project.
- */
- @GET
- fun getAll(@PathParam("project") projectId: Long): List<Topology> {
- return topologyService.findAll(identity.principal.name, projectId)
- }
-
- /**
- * Create a topology for this project.
- */
- @POST
- @Transactional
- fun create(@PathParam("project") projectId: Long, @Valid request: Topology.Create): Topology {
- return topologyService.create(identity.principal.name, projectId, request) ?: throw WebApplicationException("Topology not found", 404)
- }
-
- /**
- * Obtain a topology by its number.
- */
- @GET
- @Path("{topology}")
- fun get(@PathParam("project") projectId: Long, @PathParam("topology") number: Int): Topology {
- return topologyService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Topology not found", 404)
- }
-
- /**
- * Update the specified topology by its number.
- */
- @PUT
- @Path("{topology}")
- @Transactional
- fun update(@PathParam("project") projectId: Long, @PathParam("topology") number: Int, @Valid request: Topology.Update): Topology {
- return topologyService.update(identity.principal.name, projectId, number, request) ?: throw WebApplicationException("Topology not found", 404)
- }
-
- /**
- * Delete the specified topology.
- */
- @Path("{topology}")
- @DELETE
- @Transactional
- fun delete(@PathParam("project") projectId: Long, @PathParam("topology") number: Int): Topology {
- return topologyService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Topology not found", 404)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt
deleted file mode 100644
index d640cc08..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (c) 2022 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 io.quarkus.security.identity.SecurityIdentity
-import org.opendc.web.proto.user.User
-import org.opendc.web.server.service.UserService
-import javax.annotation.security.RolesAllowed
-import javax.inject.Inject
-import javax.ws.rs.GET
-import javax.ws.rs.Path
-
-/**
- * A resource representing the active user.
- */
-@Path("/users")
-@RolesAllowed("openid")
-class UserResource @Inject constructor(private val userService: UserService, private val identity: SecurityIdentity) {
- /**
- * Get the current active user data.
- */
- @GET
- @Path("me")
- fun get(): User = userService.getUser(identity)
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt
deleted file mode 100644
index a0ebd4f4..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (c) 2022 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 org.opendc.web.proto.JobState
-import org.opendc.web.proto.runner.Job
-import org.opendc.web.server.repository.JobRepository
-import java.time.Instant
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-
-/**
- * Service for managing [Job]s.
- */
-@ApplicationScoped
-class JobService @Inject constructor(
- private val repository: JobRepository,
- private val accountingService: UserAccountingService
-) {
- /**
- * Query the pending simulation jobs.
- */
- fun queryPending(): List<Job> {
- return repository.findAll(JobState.PENDING).map { it.toRunnerDto() }
- }
-
- /**
- * Find a job by its identifier.
- */
- fun findById(id: Long): Job? {
- return repository.findOne(id)?.toRunnerDto()
- }
-
- /**
- * Atomically update the state of a [Job].
- *
- * @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.
- */
- fun updateState(id: Long, newState: JobState, runtime: Int, results: Map<String, Any>?): Job? {
- val entity = repository.findOne(id) ?: return null
- val state = entity.state
- if (!state.isTransitionLegal(newState)) {
- throw IllegalArgumentException("Invalid transition from $state to $newState")
- }
-
- val now = Instant.now()
- var nextState = newState
- val consumedBudget = (runtime - entity.runtime).coerceAtLeast(1)
-
- // Check whether the user still has any simulation budget left
- if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) && nextState == JobState.RUNNING) {
- nextState = JobState.FAILED // User has consumed all their budget; cancel the job
- }
-
- if (!repository.updateOne(entity, nextState, now, runtime, results)) {
- throw IllegalStateException("Conflicting update")
- }
-
- return entity.toRunnerDto()
- }
-
- /**
- * Determine whether the transition from [this] to [newState] is legal.
- */
- private fun JobState.isTransitionLegal(newState: JobState): Boolean {
- // Note that we always allow transitions from the state
- return newState == this || when (this) {
- JobState.PENDING -> newState == JobState.CLAIMED
- JobState.CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED
- JobState.RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED
- JobState.FINISHED, JobState.FAILED -> false
- }
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt
deleted file mode 100644
index c83b7a54..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (c) 2022 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 org.opendc.web.proto.user.Portfolio
-import org.opendc.web.server.repository.PortfolioRepository
-import org.opendc.web.server.repository.ProjectRepository
-import java.time.Instant
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import org.opendc.web.server.model.Portfolio as PortfolioEntity
-
-/**
- * Service for managing [Portfolio]s.
- */
-@ApplicationScoped
-class PortfolioService @Inject constructor(
- private val projectRepository: ProjectRepository,
- private val portfolioRepository: PortfolioRepository
-) {
- /**
- * List all [Portfolio]s that belong a certain project.
- */
- fun findAll(userId: String, projectId: Long): List<Portfolio> {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId) ?: return emptyList()
- val project = auth.toUserDto()
- return portfolioRepository.findAll(projectId).map { it.toUserDto(project) }
- }
-
- /**
- * Find a [Portfolio] with the specified [number] belonging to [project][projectId].
- */
- fun findOne(userId: String, projectId: Long, number: Int): Portfolio? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId) ?: return null
- return portfolioRepository.findOne(projectId, number)?.toUserDto(auth.toUserDto())
- }
-
- /**
- * Delete the portfolio with the specified [number] belonging to [project][projectId].
- */
- fun delete(userId: String, projectId: Long, number: Int): Portfolio? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId)
-
- if (auth == null) {
- return null
- } else if (!auth.role.canEdit) {
- throw IllegalStateException("Not permitted to edit project")
- }
-
- val entity = portfolioRepository.findOne(projectId, number) ?: return null
- val portfolio = entity.toUserDto(auth.toUserDto())
- portfolioRepository.delete(entity)
- return portfolio
- }
-
- /**
- * Construct a new [Portfolio] with the specified name.
- */
- fun create(userId: String, projectId: Long, request: Portfolio.Create): Portfolio? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId)
-
- if (auth == null) {
- return null
- } else if (!auth.role.canEdit) {
- throw IllegalStateException("Not permitted to edit project")
- }
-
- val now = Instant.now()
- val project = auth.project
- val number = projectRepository.allocatePortfolio(auth.project, now)
-
- val portfolio = PortfolioEntity(0, number, request.name, project, request.targets)
-
- project.portfolios.add(portfolio)
- portfolioRepository.save(portfolio)
-
- return portfolio.toUserDto(auth.toUserDto())
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt
deleted file mode 100644
index 2fc5a054..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (c) 2022 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 org.opendc.web.proto.user.ProjectRole
-import org.opendc.web.server.model.Project
-import org.opendc.web.server.model.ProjectAuthorization
-import org.opendc.web.server.model.ProjectAuthorizationKey
-import org.opendc.web.server.repository.ProjectRepository
-import java.time.Instant
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import org.opendc.web.proto.user.Project as ProjectDto
-
-/**
- * Service for managing [Project]s.
- */
-@ApplicationScoped
-class ProjectService @Inject constructor(private val repository: ProjectRepository) {
- /**
- * List all projects for the user with the specified [userId].
- */
- fun findWithUser(userId: String): List<ProjectDto> {
- return repository.findAll(userId).map { it.toUserDto() }
- }
-
- /**
- * Obtain the project with the specified [id] for the user with the specified [userId].
- */
- fun findWithUser(userId: String, id: Long): ProjectDto? {
- return repository.findOne(userId, id)?.toUserDto()
- }
-
- /**
- * Create a new [Project] for the user with the specified [userId].
- */
- fun createForUser(userId: String, name: String): ProjectDto {
- val now = Instant.now()
- val entity = Project(0, name, now)
- repository.save(entity)
-
- val authorization = ProjectAuthorization(ProjectAuthorizationKey(userId, entity.id), entity, ProjectRole.OWNER)
-
- entity.authorizations.add(authorization)
- repository.save(authorization)
-
- return authorization.toUserDto()
- }
-
- /**
- * Delete a project by its identifier.
- *
- * @param userId The user that invokes the action.
- * @param id The identifier of the project.
- */
- fun deleteWithUser(userId: String, id: Long): ProjectDto? {
- val auth = repository.findOne(userId, id) ?: return null
-
- if (!auth.role.canDelete) {
- throw IllegalArgumentException("Not allowed to delete project")
- }
-
- val now = Instant.now()
- val project = auth.toUserDto().copy(updatedAt = now)
- repository.delete(auth.project)
- return project
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt
deleted file mode 100644
index 465ac2df..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (c) 2022 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 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
-
-/**
- * Conversions into DTOs provided to OpenDC runners.
- */
-
-/**
- * Convert a [Topology] into a runner-facing DTO.
- */
-internal fun Topology.toRunnerDto(): org.opendc.web.proto.runner.Topology {
- return org.opendc.web.proto.runner.Topology(id, number, name, rooms, createdAt, updatedAt)
-}
-
-/**
- * Convert a [Portfolio] into a runner-facing DTO.
- */
-internal fun Portfolio.toRunnerDto(): org.opendc.web.proto.runner.Portfolio {
- return org.opendc.web.proto.runner.Portfolio(id, number, name, targets)
-}
-
-/**
- * Convert a [Job] into a runner-facing DTO.
- */
-internal fun Job.toRunnerDto(): org.opendc.web.proto.runner.Job {
- return org.opendc.web.proto.runner.Job(id, scenario.toRunnerDto(), state, createdAt, updatedAt, runtime, results)
-}
-
-/**
- * Convert a [Job] into a runner-facing DTO.
- */
-internal fun Scenario.toRunnerDto(): org.opendc.web.proto.runner.Scenario {
- return org.opendc.web.proto.runner.Scenario(
- id,
- number,
- portfolio.toRunnerDto(),
- name,
- workload.toDto(),
- topology.toRunnerDto(),
- phenomena,
- schedulerName
- )
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt
deleted file mode 100644
index 083f2451..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (c) 2022 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 org.opendc.web.proto.JobState
-import org.opendc.web.server.model.Job
-import org.opendc.web.server.model.Scenario
-import org.opendc.web.server.model.Workload
-import org.opendc.web.server.repository.PortfolioRepository
-import org.opendc.web.server.repository.ProjectRepository
-import org.opendc.web.server.repository.ScenarioRepository
-import org.opendc.web.server.repository.TopologyRepository
-import org.opendc.web.server.repository.TraceRepository
-import java.time.Instant
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import org.opendc.web.proto.user.Scenario as ScenarioDto
-
-/**
- * Service for managing [Scenario]s.
- */
-@ApplicationScoped
-class ScenarioService @Inject constructor(
- private val projectRepository: ProjectRepository,
- private val portfolioRepository: PortfolioRepository,
- private val topologyRepository: TopologyRepository,
- private val traceRepository: TraceRepository,
- private val scenarioRepository: ScenarioRepository,
- private val accountingService: UserAccountingService
-) {
- /**
- * List all [Scenario]s that belong a certain portfolio.
- */
- fun findAll(userId: String, projectId: Long, number: Int): List<ScenarioDto> {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId) ?: return emptyList()
- val project = auth.toUserDto()
- return scenarioRepository.findAll(projectId).map { it.toUserDto(project) }
- }
-
- /**
- * Obtain a [Scenario] by identifier.
- */
- fun findOne(userId: String, projectId: Long, number: Int): ScenarioDto? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId) ?: return null
- val project = auth.toUserDto()
- return scenarioRepository.findOne(projectId, number)?.toUserDto(project)
- }
-
- /**
- * Delete the specified scenario.
- */
- fun delete(userId: String, projectId: Long, number: Int): ScenarioDto? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId)
-
- if (auth == null) {
- return null
- } else if (!auth.role.canEdit) {
- throw IllegalStateException("Not permitted to edit project")
- }
-
- val entity = scenarioRepository.findOne(projectId, number) ?: return null
- val scenario = entity.toUserDto(auth.toUserDto())
- scenarioRepository.delete(entity)
- return scenario
- }
-
- /**
- * Construct a new [Scenario] with the specified data.
- */
- fun create(userId: String, projectId: Long, portfolioNumber: Int, request: ScenarioDto.Create): ScenarioDto? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId)
-
- if (auth == null) {
- return null
- } else if (!auth.role.canEdit) {
- throw IllegalStateException("Not permitted to edit project")
- }
-
- val portfolio = portfolioRepository.findOne(projectId, portfolioNumber) ?: return null
- val topology = requireNotNull(
- topologyRepository.findOne(
- projectId,
- request.topology.toInt()
- )
- ) { "Referred topology does not exist" }
- val trace =
- requireNotNull(traceRepository.findOne(request.workload.trace)) { "Referred trace does not exist" }
-
- val now = Instant.now()
- val project = auth.project
- val number = projectRepository.allocateScenario(auth.project, now)
-
- val scenario = Scenario(
- 0,
- number,
- request.name,
- project,
- portfolio,
- Workload(trace, request.workload.samplingFraction),
- topology,
- request.phenomena,
- request.schedulerName
- )
- val job = Job(0, userId, scenario, now, portfolio.targets.repeats)
-
- // 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)
- scenarioRepository.save(scenario)
-
- return scenario.toUserDto(auth.toUserDto())
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt
deleted file mode 100644
index 5c2a457a..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (c) 2022 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 org.opendc.web.proto.user.Topology
-import org.opendc.web.server.repository.ProjectRepository
-import org.opendc.web.server.repository.TopologyRepository
-import java.time.Instant
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import org.opendc.web.server.model.Topology as TopologyEntity
-
-/**
- * Service for managing [Topology]s.
- */
-@ApplicationScoped
-class TopologyService @Inject constructor(
- private val projectRepository: ProjectRepository,
- private val topologyRepository: TopologyRepository
-) {
- /**
- * List all [Topology]s that belong a certain project.
- */
- fun findAll(userId: String, projectId: Long): List<Topology> {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId) ?: return emptyList()
- val project = auth.toUserDto()
- return topologyRepository.findAll(projectId).map { it.toUserDto(project) }
- }
-
- /**
- * Find the [Topology] with the specified [number] belonging to [project][projectId].
- */
- fun findOne(userId: String, projectId: Long, number: Int): Topology? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId) ?: return null
- return topologyRepository.findOne(projectId, number)?.toUserDto(auth.toUserDto())
- }
-
- /**
- * Delete the [Topology] with the specified [number] belonging to [project][projectId].
- */
- fun delete(userId: String, projectId: Long, number: Int): Topology? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId)
-
- if (auth == null) {
- return null
- } else if (!auth.role.canEdit) {
- throw IllegalStateException("Not permitted to edit project")
- }
-
- val entity = topologyRepository.findOne(projectId, number) ?: return null
- val now = Instant.now()
- val topology = entity.toUserDto(auth.toUserDto()).copy(updatedAt = now)
- topologyRepository.delete(entity)
-
- return topology
- }
-
- /**
- * Update a [Topology] with the specified [number] belonging to [project][projectId].
- */
- fun update(userId: String, projectId: Long, number: Int, request: Topology.Update): Topology? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId)
-
- if (auth == null) {
- return null
- } else if (!auth.role.canEdit) {
- throw IllegalStateException("Not permitted to edit project")
- }
-
- val entity = topologyRepository.findOne(projectId, number) ?: return null
- val now = Instant.now()
-
- entity.updatedAt = now
- entity.rooms = request.rooms
-
- return entity.toUserDto(auth.toUserDto())
- }
-
- /**
- * Construct a new [Topology] with the specified name.
- */
- fun create(userId: String, projectId: Long, request: Topology.Create): Topology? {
- // User must have access to project
- val auth = projectRepository.findOne(userId, projectId)
-
- if (auth == null) {
- return null
- } else if (!auth.role.canEdit) {
- throw IllegalStateException("Not permitted to edit project")
- }
-
- val now = Instant.now()
- val project = auth.project
- val number = projectRepository.allocateTopology(auth.project, now)
-
- val topology = TopologyEntity(0, number, request.name, project, now, request.rooms)
-
- project.topologies.add(topology)
- topologyRepository.save(topology)
-
- return topology.toUserDto(auth.toUserDto())
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt
deleted file mode 100644
index 11066bfb..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (c) 2022 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 org.eclipse.microprofile.config.inject.ConfigProperty
-import org.opendc.web.server.model.UserAccounting
-import org.opendc.web.server.repository.UserAccountingRepository
-import java.time.Duration
-import java.time.LocalDate
-import java.time.temporal.TemporalAdjusters
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.persistence.EntityExistsException
-import org.opendc.web.proto.user.UserAccounting as UserAccountingDto
-
-/**
- * Service for tracking the simulation budget of users.
- *
- * @param repository The [UserAccountingRepository] used to communicate with the database.
- * @param simulationBudget The default simulation budget for new users.
- */
-@ApplicationScoped
-class UserAccountingService @Inject constructor(
- private val repository: UserAccountingRepository,
- @ConfigProperty(name = "opendc.accounting.simulation-budget", defaultValue = "2000m")
- private val simulationBudget: Duration
-) {
- /**
- * Return the [UserAccountingDto] object for the user with the specified [userId]. If the object does not exist in the
- * database, a default value is constructed.
- */
- fun getAccounting(userId: String): UserAccountingDto {
- val accounting = repository.findForUser(userId)
- return if (accounting != null) {
- UserAccountingDto(accounting.periodEnd, accounting.simulationTime, accounting.simulationTimeBudget)
- } else {
- UserAccountingDto(getNextAccountingPeriod(), 0, simulationBudget.toSeconds().toInt())
- }
- }
-
- /**
- * Determine whether the user with [userId] has any remaining simulation budget.
- *
- * @param userId The unique identifier of the user.
- * @return `true` when the user still has budget left, `false` otherwise.
- */
- fun hasSimulationBudget(userId: String): Boolean {
- val accounting = repository.findForUser(userId) ?: return true
- val today = LocalDate.now()
-
- // The accounting period must be over or there must be budget remaining.
- return !today.isBefore(accounting.periodEnd) || accounting.simulationTimeBudget > accounting.simulationTime
- }
-
- /**
- * Consume [seconds] from the simulation budget of the user with [userId].
- *
- * @param userId The unique identifier of the user.
- * @param seconds The seconds to consume from the simulation budget.
- * @param `true` if the user has consumed his full budget or `false` if there is still budget remaining.
- */
- fun consumeSimulationBudget(userId: String, seconds: Int): Boolean {
- val today = LocalDate.now()
- val nextAccountingPeriod = getNextAccountingPeriod(today)
- val repository = repository
-
- // We need to be careful to prevent conflicts in case of concurrency
- // 1. First, we try to create the accounting object if it does not exist yet. This may fail if another instance
- // creates the object concurrently.
- // 2. Second, we check if the budget needs to be reset and try this atomically.
- // 3. Finally, we atomically consume the budget from the object
- // This is repeated three times in case there is a conflict
- repeat(3) {
- val accounting = repository.findForUser(userId)
-
- if (accounting == null) {
- try {
- val newAccounting = UserAccounting(userId, nextAccountingPeriod, simulationBudget.toSeconds().toInt())
- newAccounting.simulationTime = seconds
- repository.save(newAccounting)
-
- return newAccounting.simulationTime >= newAccounting.simulationTimeBudget
- } catch (e: EntityExistsException) {
- // Conflict due to concurrency; retry
- }
- } else {
- val success = if (!today.isBefore(accounting.periodEnd)) {
- repository.resetBudget(accounting, nextAccountingPeriod, seconds)
- } else {
- repository.consumeBudget(accounting, seconds)
- }
-
- if (success) {
- return accounting.simulationTimeBudget <= accounting.simulationTime
- }
- }
- }
-
- throw IllegalStateException("Failed to allocate consume budget due to conflict")
- }
-
- /**
- * Helper method to find next accounting period.
- */
- private fun getNextAccountingPeriod(today: LocalDate = LocalDate.now()): LocalDate {
- return today.with(TemporalAdjusters.firstDayOfNextMonth())
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt
deleted file mode 100644
index e28d9c0f..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (c) 2022 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 org.opendc.web.proto.user.Project
-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
-
-/**
- * Conversions into DTOs provided to users.
- */
-
-/**
- * Convert a [Trace] entity into a [org.opendc.web.proto.Trace] DTO.
- */
-internal fun Trace.toUserDto(): org.opendc.web.proto.Trace {
- return org.opendc.web.proto.Trace(id, name, type)
-}
-
-/**
- * Convert a [ProjectAuthorization] entity into a [Project] DTO.
- */
-internal fun ProjectAuthorization.toUserDto(): Project {
- return Project(project.id, project.name, project.createdAt, project.updatedAt, role)
-}
-
-/**
- * Convert a [Topology] entity into a [org.opendc.web.proto.user.Topology] DTO.
- */
-internal fun Topology.toUserDto(project: Project): org.opendc.web.proto.user.Topology {
- return org.opendc.web.proto.user.Topology(id, number, project, name, rooms, createdAt, updatedAt)
-}
-
-/**
- * Convert a [Topology] entity into a [org.opendc.web.proto.user.Topology.Summary] DTO.
- */
-private fun Topology.toSummaryDto(): org.opendc.web.proto.user.Topology.Summary {
- return org.opendc.web.proto.user.Topology.Summary(id, number, name, createdAt, updatedAt)
-}
-
-/**
- * Convert a [Portfolio] entity into a [org.opendc.web.proto.user.Portfolio] DTO.
- */
-internal fun Portfolio.toUserDto(project: Project): org.opendc.web.proto.user.Portfolio {
- return org.opendc.web.proto.user.Portfolio(id, number, project, name, targets, scenarios.map { it.toSummaryDto() })
-}
-
-/**
- * Convert a [Portfolio] entity into a [org.opendc.web.proto.user.Portfolio.Summary] DTO.
- */
-private fun Portfolio.toSummaryDto(): org.opendc.web.proto.user.Portfolio.Summary {
- return org.opendc.web.proto.user.Portfolio.Summary(id, number, name, targets)
-}
-
-/**
- * Convert a [Scenario] entity into a [org.opendc.web.proto.user.Scenario] DTO.
- */
-internal fun Scenario.toUserDto(project: Project): org.opendc.web.proto.user.Scenario {
- return org.opendc.web.proto.user.Scenario(
- id,
- number,
- project,
- portfolio.toSummaryDto(),
- name,
- workload.toDto(),
- topology.toSummaryDto(),
- phenomena,
- schedulerName,
- job.toUserDto()
- )
-}
-
-/**
- * Convert a [Scenario] entity into a [org.opendc.web.proto.user.Scenario.Summary] DTO.
- */
-private fun Scenario.toSummaryDto(): org.opendc.web.proto.user.Scenario.Summary {
- return org.opendc.web.proto.user.Scenario.Summary(
- id,
- number,
- name,
- workload.toDto(),
- topology.toSummaryDto(),
- phenomena,
- schedulerName,
- job.toUserDto()
- )
-}
-
-/**
- * Convert a [Job] entity into a [org.opendc.web.proto.user.Job] DTO.
- */
-internal fun Job.toUserDto(): org.opendc.web.proto.user.Job {
- return org.opendc.web.proto.user.Job(id, state, createdAt, updatedAt, results)
-}
-
-/**
- * Convert a [Workload] entity into a DTO.
- */
-internal fun Workload.toDto(): org.opendc.web.proto.Workload {
- return org.opendc.web.proto.Workload(trace.toUserDto(), samplingFraction)
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt
deleted file mode 100644
index 39352267..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (c) 2021 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 io.quarkus.security.identity.SecurityIdentity
-import org.opendc.web.proto.user.User
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-
-/**
- * Service for managing [User]s.
- */
-@ApplicationScoped
-class UserService @Inject constructor(private val accounting: UserAccountingService) {
- /**
- * Obtain the [User] object for the specified [identity].
- */
- fun getUser(identity: SecurityIdentity): User {
- val userId = identity.principal.name
- val accounting = accounting.getAccounting(userId)
-
- return User(userId, accounting)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt
deleted file mode 100644
index 9e29b734..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (c) 2022 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.util.hibernate.json
-
-import org.hibernate.type.descriptor.ValueExtractor
-import org.hibernate.type.descriptor.WrapperOptions
-import org.hibernate.type.descriptor.java.JavaTypeDescriptor
-import org.hibernate.type.descriptor.sql.BasicExtractor
-import org.hibernate.type.descriptor.sql.SqlTypeDescriptor
-import java.sql.CallableStatement
-import java.sql.ResultSet
-import java.sql.Types
-
-/**
- * Abstract implementation of a [SqlTypeDescriptor] for Hibernate JSON type.
- */
-internal abstract class AbstractJsonSqlTypeDescriptor : SqlTypeDescriptor {
-
- override fun getSqlType(): Int {
- return Types.OTHER
- }
-
- override fun canBeRemapped(): Boolean {
- return true
- }
-
- override fun <X> getExtractor(typeDescriptor: JavaTypeDescriptor<X>): ValueExtractor<X> {
- return object : BasicExtractor<X>(typeDescriptor, this) {
- override fun doExtract(rs: ResultSet, name: String, options: WrapperOptions): X {
- return typeDescriptor.wrap(extractJson(rs, name), options)
- }
-
- override fun doExtract(statement: CallableStatement, index: Int, options: WrapperOptions): X {
- return typeDescriptor.wrap(extractJson(statement, index), options)
- }
-
- override fun doExtract(statement: CallableStatement, name: String, options: WrapperOptions): X {
- return typeDescriptor.wrap(extractJson(statement, name), options)
- }
- }
- }
-
- open fun extractJson(rs: ResultSet, name: String): Any? {
- return rs.getObject(name)
- }
-
- open fun extractJson(statement: CallableStatement, index: Int): Any? {
- return statement.getObject(index)
- }
-
- open fun extractJson(statement: CallableStatement, name: String): Any? {
- return statement.getObject(name)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt
deleted file mode 100644
index df6a3013..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (c) 2022 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.util.hibernate.json
-
-import com.fasterxml.jackson.databind.JsonNode
-import org.hibernate.type.descriptor.ValueBinder
-import org.hibernate.type.descriptor.WrapperOptions
-import org.hibernate.type.descriptor.java.JavaTypeDescriptor
-import org.hibernate.type.descriptor.sql.BasicBinder
-import java.sql.CallableStatement
-import java.sql.PreparedStatement
-
-/**
- * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as binary (JSONB).
- */
-internal object JsonBinarySqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
- override fun <X> getBinder(typeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
- return object : BasicBinder<X>(typeDescriptor, this) {
- override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
- st.setObject(index, typeDescriptor.unwrap(value, JsonNode::class.java, options), sqlType)
- }
-
- override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
- st.setObject(name, typeDescriptor.unwrap(value, JsonNode::class.java, options), sqlType)
- }
- }
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt
deleted file mode 100644
index 4924f586..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (c) 2022 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.util.hibernate.json
-
-import org.hibernate.type.descriptor.ValueBinder
-import org.hibernate.type.descriptor.WrapperOptions
-import org.hibernate.type.descriptor.java.JavaTypeDescriptor
-import org.hibernate.type.descriptor.sql.BasicBinder
-import java.io.UnsupportedEncodingException
-import java.sql.CallableStatement
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-import java.sql.Types
-
-/**
- * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as UTF-8 encoded bytes.
- */
-internal object JsonBytesSqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
- private val CHARSET = Charsets.UTF_8
-
- override fun getSqlType(): Int {
- return Types.BINARY
- }
-
- override fun <X> getBinder(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
- return object : BasicBinder<X>(javaTypeDescriptor, this) {
- override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
- st.setBytes(index, toJsonBytes(javaTypeDescriptor.unwrap(value, String::class.java, options)))
- }
-
- override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
- st.setBytes(name, toJsonBytes(javaTypeDescriptor.unwrap(value, String::class.java, options)))
- }
- }
- }
-
- override fun extractJson(rs: ResultSet, name: String): Any? {
- return fromJsonBytes(rs.getBytes(name))
- }
-
- override fun extractJson(statement: CallableStatement, index: Int): Any? {
- return fromJsonBytes(statement.getBytes(index))
- }
-
- override fun extractJson(statement: CallableStatement, name: String): Any? {
- return fromJsonBytes(statement.getBytes(name))
- }
-
- private fun toJsonBytes(jsonValue: String): ByteArray? {
- return try {
- jsonValue.toByteArray(CHARSET)
- } catch (e: UnsupportedEncodingException) {
- throw IllegalStateException(e)
- }
- }
-
- private fun fromJsonBytes(jsonBytes: ByteArray?): String? {
- return if (jsonBytes == null) {
- null
- } else {
- try {
- String(jsonBytes, CHARSET)
- } catch (e: UnsupportedEncodingException) {
- throw IllegalStateException(e)
- }
- }
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt
deleted file mode 100644
index bd22ffbe..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (c) 2022 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.util.hibernate.json
-
-import org.hibernate.dialect.H2Dialect
-import org.hibernate.dialect.PostgreSQL81Dialect
-import org.hibernate.internal.SessionImpl
-import org.hibernate.type.descriptor.ValueBinder
-import org.hibernate.type.descriptor.ValueExtractor
-import org.hibernate.type.descriptor.WrapperOptions
-import org.hibernate.type.descriptor.java.JavaTypeDescriptor
-import org.hibernate.type.descriptor.sql.BasicBinder
-import org.hibernate.type.descriptor.sql.BasicExtractor
-import org.hibernate.type.descriptor.sql.SqlTypeDescriptor
-import java.sql.CallableStatement
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-import java.sql.Types
-
-/**
- * A [SqlTypeDescriptor] that automatically selects the correct implementation for the database dialect.
- */
-internal object JsonSqlTypeDescriptor : SqlTypeDescriptor {
-
- override fun getSqlType(): Int = Types.OTHER
-
- override fun canBeRemapped(): Boolean = true
-
- override fun <X> getExtractor(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueExtractor<X> {
- return object : BasicExtractor<X>(javaTypeDescriptor, this) {
- private var delegate: AbstractJsonSqlTypeDescriptor? = null
-
- override fun doExtract(rs: ResultSet, name: String, options: WrapperOptions): X {
- return javaTypeDescriptor.wrap(delegate(options).extractJson(rs, name), options)
- }
-
- override fun doExtract(statement: CallableStatement, index: Int, options: WrapperOptions): X {
- return javaTypeDescriptor.wrap(delegate(options).extractJson(statement, index), options)
- }
-
- override fun doExtract(statement: CallableStatement, name: String, options: WrapperOptions): X {
- return javaTypeDescriptor.wrap(delegate(options).extractJson(statement, name), options)
- }
-
- private fun delegate(options: WrapperOptions): AbstractJsonSqlTypeDescriptor {
- var delegate = delegate
- if (delegate == null) {
- delegate = resolveSqlTypeDescriptor(options)
- this.delegate = delegate
- }
- return delegate
- }
- }
- }
-
- override fun <X> getBinder(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
- return object : BasicBinder<X>(javaTypeDescriptor, this) {
- private var delegate: ValueBinder<X>? = null
-
- override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
- delegate(options).bind(st, value, index, options)
- }
-
- override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
- delegate(options).bind(st, value, name, options)
- }
-
- private fun delegate(options: WrapperOptions): ValueBinder<X> {
- var delegate = delegate
- if (delegate == null) {
- delegate = checkNotNull(resolveSqlTypeDescriptor(options).getBinder(javaTypeDescriptor))
- this.delegate = delegate
- }
- return delegate
- }
- }
- }
-
- /**
- * Helper method to resolve the appropriate [SqlTypeDescriptor] based on the [WrapperOptions].
- */
- private fun resolveSqlTypeDescriptor(options: WrapperOptions): AbstractJsonSqlTypeDescriptor {
- val session = options as? SessionImpl
- return when (session?.jdbcServices?.dialect) {
- is PostgreSQL81Dialect -> JsonBinarySqlTypeDescriptor
- is H2Dialect -> JsonBytesSqlTypeDescriptor
- else -> JsonStringSqlTypeDescriptor
- }
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt
deleted file mode 100644
index 6e015762..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (c) 2022 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.util.hibernate.json
-
-import org.hibernate.type.descriptor.ValueBinder
-import org.hibernate.type.descriptor.WrapperOptions
-import org.hibernate.type.descriptor.java.JavaTypeDescriptor
-import org.hibernate.type.descriptor.sql.BasicBinder
-import java.sql.CallableStatement
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-import java.sql.Types
-
-/**
- * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as string (VARCHAR).
- */
-internal object JsonStringSqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
- override fun getSqlType(): Int = Types.VARCHAR
-
- override fun <X> getBinder(typeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
- return object : BasicBinder<X>(typeDescriptor, this) {
- override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
- st.setString(index, typeDescriptor.unwrap(value, String::class.java, options))
- }
-
- override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
- st.setString(name, typeDescriptor.unwrap(value, String::class.java, options))
- }
- }
- }
-
- override fun extractJson(rs: ResultSet, name: String): Any? {
- return rs.getString(name)
- }
-
- override fun extractJson(statement: CallableStatement, index: Int): Any? {
- return statement.getString(index)
- }
-
- override fun extractJson(statement: CallableStatement, name: String): Any? {
- return statement.getString(name)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt
deleted file mode 100644
index 9ee21a4c..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (c) 2022 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.util.hibernate.json
-
-import com.fasterxml.jackson.databind.ObjectMapper
-import org.hibernate.type.AbstractSingleColumnStandardBasicType
-import org.hibernate.type.BasicType
-import org.hibernate.usertype.DynamicParameterizedType
-import java.util.Properties
-import javax.enterprise.inject.spi.CDI
-
-/**
- * A [BasicType] that contains JSON.
- */
-class JsonType(objectMapper: ObjectMapper) : AbstractSingleColumnStandardBasicType<Any>(JsonSqlTypeDescriptor, JsonTypeDescriptor(objectMapper)), DynamicParameterizedType {
- /**
- * No-arg constructor for Hibernate to instantiate.
- */
- constructor() : this(CDI.current().select(ObjectMapper::class.java).get())
-
- override fun getName(): String = "json"
-
- override fun registerUnderJavaType(): Boolean = true
-
- override fun setParameterValues(parameters: Properties) {
- (javaTypeDescriptor as JsonTypeDescriptor).setParameterValues(parameters)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt
deleted file mode 100644
index 9407f940..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (c) 2022 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.util.hibernate.json
-
-import com.fasterxml.jackson.databind.ObjectMapper
-import org.hibernate.HibernateException
-import org.hibernate.annotations.common.reflection.XProperty
-import org.hibernate.annotations.common.reflection.java.JavaXMember
-import org.hibernate.engine.jdbc.BinaryStream
-import org.hibernate.engine.jdbc.internal.BinaryStreamImpl
-import org.hibernate.type.descriptor.WrapperOptions
-import org.hibernate.type.descriptor.java.AbstractTypeDescriptor
-import org.hibernate.type.descriptor.java.BlobTypeDescriptor
-import org.hibernate.type.descriptor.java.DataHelper
-import org.hibernate.type.descriptor.java.MutableMutabilityPlan
-import org.hibernate.usertype.DynamicParameterizedType
-import java.io.ByteArrayInputStream
-import java.io.IOException
-import java.io.InputStream
-import java.lang.reflect.Type
-import java.sql.Blob
-import java.sql.SQLException
-import java.util.Objects
-import java.util.Properties
-
-/**
- * An [AbstractTypeDescriptor] implementation for Hibernate JSON type.
- */
-internal class JsonTypeDescriptor(private val objectMapper: ObjectMapper) : AbstractTypeDescriptor<Any>(Any::class.java, JsonMutabilityPlan(objectMapper)), DynamicParameterizedType {
- private var type: Type? = null
-
- override fun setParameterValues(parameters: Properties) {
- val xProperty = parameters[DynamicParameterizedType.XPROPERTY] as XProperty
- type = if (xProperty is JavaXMember) {
- val x = xProperty as JavaXMember
- x.javaType
- } else {
- (parameters[DynamicParameterizedType.PARAMETER_TYPE] as DynamicParameterizedType.ParameterType).returnedClass
- }
- }
-
- override fun areEqual(one: Any?, another: Any?): Boolean {
- return when {
- one === another -> true
- one == null || another == null -> false
- one is String && another is String -> one == another
- one is Collection<*> && another is Collection<*> -> Objects.equals(one, another)
- else -> areJsonEqual(one, another)
- }
- }
-
- override fun toString(value: Any?): String {
- return objectMapper.writeValueAsString(value)
- }
-
- override fun fromString(string: String): Any? {
- return objectMapper.readValue(string, objectMapper.typeFactory.constructType(type))
- }
-
- override fun <X> unwrap(value: Any?, type: Class<X>, options: WrapperOptions): X? {
- if (value == null) {
- return null
- }
-
- @Suppress("UNCHECKED_CAST")
- return when {
- String::class.java.isAssignableFrom(type) -> toString(value)
- BinaryStream::class.java.isAssignableFrom(type) || ByteArray::class.java.isAssignableFrom(type) -> {
- val stringValue = if (value is String) value else toString(value)
- BinaryStreamImpl(DataHelper.extractBytes(ByteArrayInputStream(stringValue.toByteArray())))
- }
- Blob::class.java.isAssignableFrom(type) -> {
- val stringValue = if (value is String) value else toString(value)
- BlobTypeDescriptor.INSTANCE.fromString(stringValue)
- }
- Any::class.java.isAssignableFrom(type) -> toJsonType(value)
- else -> throw unknownUnwrap(type)
- } as X
- }
-
- override fun <X> wrap(value: X?, options: WrapperOptions): Any? {
- if (value == null) {
- return null
- }
-
- var blob: Blob? = null
- if (Blob::class.java.isAssignableFrom(value.javaClass)) {
- blob = options.lobCreator.wrap(value as Blob?)
- } else if (ByteArray::class.java.isAssignableFrom(value.javaClass)) {
- blob = options.lobCreator.createBlob(value as ByteArray?)
- } else if (InputStream::class.java.isAssignableFrom(value.javaClass)) {
- val inputStream = value as InputStream
- blob = try {
- options.lobCreator.createBlob(inputStream, inputStream.available().toLong())
- } catch (e: IOException) {
- throw unknownWrap(value.javaClass)
- }
- }
-
- val stringValue: String = try {
- if (blob != null) String(DataHelper.extractBytes(blob.binaryStream)) else value.toString()
- } catch (e: SQLException) {
- throw HibernateException("Unable to extract binary stream from Blob", e)
- }
-
- return fromString(stringValue)
- }
-
- private class JsonMutabilityPlan(private val objectMapper: ObjectMapper) : MutableMutabilityPlan<Any>() {
- override fun deepCopyNotNull(value: Any): Any {
- return objectMapper.treeToValue(objectMapper.valueToTree(value), value.javaClass)
- }
- }
-
- private fun readObject(value: String): Any {
- return objectMapper.readTree(value)
- }
-
- private fun areJsonEqual(one: Any, another: Any): Boolean {
- return readObject(objectMapper.writeValueAsString(one)) == readObject(objectMapper.writeValueAsString(another))
- }
-
- private fun toJsonType(value: Any?): Any {
- return try {
- readObject(objectMapper.writeValueAsString(value))
- } catch (e: Exception) {
- throw IllegalArgumentException(e)
- }
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt
deleted file mode 100644
index 742a510c..00000000
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (c) 2022 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.util.runner
-
-import org.opendc.web.proto.JobState
-import org.opendc.web.proto.runner.Job
-import org.opendc.web.runner.JobManager
-import org.opendc.web.server.service.JobService
-import javax.enterprise.context.ApplicationScoped
-import javax.inject.Inject
-import javax.transaction.Transactional
-
-/**
- * Implementation of [JobManager] that interfaces directly with [JobService] without overhead of the REST API.
- */
-@ApplicationScoped
-class QuarkusJobManager @Inject constructor(private val service: JobService) : JobManager {
- @Transactional
- override fun findNext(): Job? {
- return service.queryPending().firstOrNull()
- }
-
- @Transactional
- override fun claim(id: Long): Boolean {
- return try {
- service.updateState(id, JobState.CLAIMED, 0, null)
- true
- } catch (e: IllegalStateException) {
- false
- }
- }
-
- @Transactional
- override fun heartbeat(id: Long, runtime: Int): Boolean {
- val res = service.updateState(id, JobState.RUNNING, runtime, null)
- return res?.state != JobState.FAILED
- }
-
- @Transactional
- override fun fail(id: Long, runtime: Int) {
- service.updateState(id, JobState.FAILED, runtime, null)
- }
-
- @Transactional
- override fun finish(id: Long, runtime: Int, results: Map<String, Any>) {
- service.updateState(id, JobState.FINISHED, runtime, results)
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/resources/application-test.properties b/opendc-web/opendc-web-server/src/main/resources/application-test.properties
index 338a00b9..bee17221 100644
--- a/opendc-web/opendc-web-server/src/main/resources/application-test.properties
+++ b/opendc-web/opendc-web-server/src/main/resources/application-test.properties
@@ -23,7 +23,9 @@ quarkus.datasource.db-kind = h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE IF NOT EXISTS "JSONB" AS json;
quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
+quarkus.hibernate-orm.log.sql=true
quarkus.flyway.clean-at-start=true
+quarkus.flyway.locations=db/migration,db/testing
# Disable security
quarkus.oidc.enabled=false
diff --git a/opendc-web/opendc-web-server/src/main/resources/application.properties b/opendc-web/opendc-web-server/src/main/resources/application.properties
index 40933304..0f47db30 100644
--- a/opendc-web/opendc-web-server/src/main/resources/application.properties
+++ b/opendc-web/opendc-web-server/src/main/resources/application.properties
@@ -20,6 +20,7 @@
# Enable CORS
quarkus.http.cors=true
+quarkus.http.cors.origins=http://localhost:3000,https://opendc.org
# Security
quarkus.oidc.enabled=${opendc.security.enabled}
diff --git a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql b/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql
index 1a0e4046..40654b6b 100644
--- a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql
+++ b/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql
@@ -7,19 +7,21 @@ create table projects
id bigint not null,
created_at timestamp not null,
name varchar(255) not null,
- portfolios_created integer not null,
- scenarios_created integer not null,
- topologies_created integer not null,
+ portfolios_created integer not null default 0,
+ scenarios_created integer not null default 0,
+ topologies_created integer not null default 0,
updated_at timestamp not null,
primary key (id)
);
+create type project_role as enum ('OWNER', 'EDITOR', 'VIEWER');
+
-- Project authorizations authorize users specific permissions to a project.
create table project_authorizations
(
project_id bigint not null,
user_id varchar(255) not null,
- role integer not null,
+ role project_role not null,
primary key (project_id, user_id)
);
@@ -55,7 +57,6 @@ create table scenarios
phenomena jsonb not null,
scheduler_name varchar(255) not null,
sampling_fraction double precision not null,
- job_id bigint,
portfolio_id bigint not null,
project_id bigint not null,
topology_id bigint not null,
@@ -63,16 +64,19 @@ create table scenarios
primary key (id)
);
+create type job_state as enum ('PENDING', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED');
+
create table jobs
(
- id bigint not null,
- created_by varchar(255) not null,
- created_at timestamp not null,
- repeats integer not null,
- results jsonb,
- state integer not null,
- runtime integer not null,
- updated_at timestamp not null,
+ id bigint not null,
+ created_by varchar(255) not null,
+ created_at timestamp not null,
+ repeats integer not null,
+ results jsonb,
+ state job_state not null default 'PENDING',
+ runtime integer not null default 0,
+ updated_at timestamp not null,
+ scenario_id bigint not null,
primary key (id)
);
@@ -97,60 +101,60 @@ create table traces
-- Relations
alter table project_authorizations
- add constraint FK824hw0npe6gwiamwb6vohsu19
+ add constraint fk_project_authorizations
foreign key (project_id)
references projects;
-create index fn_topologies_number on topologies (project_id, number);
+create index ux_topologies_number on topologies (project_id, number);
alter table topologies
- add constraint UK2s5na63qtu2of4g7odocmwi2a unique (project_id, number);
+ add constraint uk_topologies_number unique (project_id, number);
alter table topologies
- add constraint FK1kpw87pylq7m2ct9lq0ed1u3b
+ add constraint fk_topologies_project
foreign key (project_id)
references projects;
-create index fn_portfolios_number on portfolios (project_id, number);
+create index ux_portfolios_number on portfolios (project_id, number);
alter table portfolios
- add constraint FK31ytuaxb7aboxueng9hq7owwa
+ add constraint fk_portfolios_project
foreign key (project_id)
references projects;
alter table portfolios
- add constraint UK56dtskxruwj22dvxny2hfhks1 unique (project_id, number);
-
-create index fn_scenarios_number on scenarios (project_id, number);
-
-alter table scenarios
- add constraint UKd0bk6fmtw5qiu9ty7t3g9crqd unique (project_id, number);
+ add constraint uk_portfolios_number unique (project_id, number);
-alter table scenarios
- add constraint FK9utvg0i5uu8db9pa17a1d77iy
- foreign key (job_id)
- references jobs;
+create index ux_scenarios_number on scenarios (project_id, number);
alter table scenarios
- add constraint FK181y5hv0uibhj7fpbpkdy90s5
- foreign key (portfolio_id)
- references portfolios;
+ add constraint uk_scenarios_number unique (project_id, number);
alter table scenarios
- add constraint FKbvwyh4joavs444rj270o3b8fr
+ add constraint fk_scenarios_project
foreign key (project_id)
references projects;
alter table scenarios
- add constraint FKrk6ltvaf9lp0aukp9dq3qjujj
+ add constraint fk_scenarios_topology
foreign key (topology_id)
references topologies;
alter table scenarios
- add constraint FK5m05tqeekqjkbbsaj3ehl6o8n
+ add constraint fk_scenarios_portfolio
+ foreign key (portfolio_id)
+ references portfolios;
+
+alter table scenarios
+ add constraint fk_scenarios_trace
foreign key (trace_id)
references traces;
+alter table jobs
+ add constraint fk_scenarios_job
+ foreign key (scenario_id)
+ references scenarios;
+
-- Initial data
insert into traces (id, name, type)
values ('bitbrains-small', 'Bitbrains Small', 'vm');
diff --git a/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql b/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql
new file mode 100644
index 00000000..1b702f4e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql
@@ -0,0 +1,24 @@
+-- Test entities
+
+alter sequence hibernate_sequence restart with 500;
+
+insert into projects (id, created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at)
+values (1, current_timestamp(), 'Test Project', 1, 2, 1, current_timestamp());
+insert into project_authorizations (project_id, user_id, role)
+values (1, 'owner', 'OWNER'),
+ (1, 'editor', 'EDITOR'),
+ (1, 'viewer', 'VIEWER');
+
+insert into portfolios (id, name, number, targets, project_id)
+values (1, 'Test Portfolio', 1, '{ "metrics": [] }' format json, 1);
+
+insert into topologies (id, created_at, name, number, rooms, updated_at, project_id)
+values (1, current_timestamp(), 'Test Topology', 1, '[]' format json, current_timestamp(), 1);
+
+insert into scenarios (id, name, number, phenomena, scheduler_name, sampling_fraction, portfolio_id, project_id, topology_id, trace_id)
+values (1, 'Test Scenario', 1, '{ "failures": false, "interference": false }' format json, 'mem', 1.0, 1, 1, 1, 'bitbrains-small'),
+ (2, 'Test Scenario', 2, '{ "failures": false, "interference": false }' format json, 'mem', 1.0, 1, 1, 1, 'bitbrains-small');
+
+insert into jobs (id, created_by, created_at, repeats, updated_at, scenario_id)
+values (1, 'owner', current_timestamp(), 1, current_timestamp(), 1),
+ (2, 'owner', current_timestamp(), 1, current_timestamp(), 2);
diff --git a/opendc-web/opendc-web-server/src/main/resources/hypersistence-utils.properties b/opendc-web/opendc-web-server/src/main/resources/hypersistence-utils.properties
new file mode 100644
index 00000000..451ce2d8
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/resources/hypersistence-utils.properties
@@ -0,0 +1 @@
+hypersistence.utils.jackson.object.mapper=org.opendc.web.server.util.QuarkusObjectMapperSupplier