package io.vivarium.persistence;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import com.google.common.base.Preconditions;
import io.vivarium.db.DatabaseUtils;
import io.vivarium.db.Relation;
import io.vivarium.util.UUID;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@EqualsAndHashCode(callSuper = true)
@ToString
public abstract class JobModel extends PersistenceModel
{
// Table name
private static final String TABLE_NAME = "jobs";
// Column names
protected static final String ID = "id";
protected static final String JOB_TYPE = "job_type";
protected static final String JOB_STATUS = "status";
protected static final String PRIORITY = "priority";
protected static final String CHECKED_OUT_BY = "checked_out_by";
protected static final String CHECKED_OUT_TIME = "checked_out_time";
protected static final String COMPLETED_TIME = "completed_time";
// Junction table for dependencies
private static final String DEPENDENCIES_TABLE_NAME = "job_dependencies";
protected static final String DEPENDENCIES_FROM_ID = "job_id";
protected static final String DEPENDENCIES_TO_ID = "requires_job_id";
// Junction tables for resources
private static final String INPUT_RESOURCES_TABLE_NAME = "job_input_resources";
private static final String OUTPUT_RESOURCES_TABLE_NAME = "job_output_resources";
protected static final String RESOURCES_FROM_ID = "job_id";
protected static final String RESOURCES_TO_ID = "resource_id";
// Job Property KV table
private static final String PROPERTY_TABLE_NAME = "job_properties";
protected static final String PROPERTY_JOB_ID = "job_id";
protected static final String PROPERTY_KEY = "property_name";
protected static final String PROPERTY_VALUE = "property_value";
// relation data
private final UUID jobID;
private final JobType type;
private final JobStatus status;
private final int priority;
private final Optional<UUID> checkedOutByWorkerID;
private final Optional<Timestamp> checkedOutTime;
private final Optional<Timestamp> completedTime;
private final Collection<UUID> inputResources;
private final Collection<UUID> outputResources;
private final Collection<UUID> jobDependencies;
public JobModel(UUID jobID, JobType type, JobStatus status, int priority, UUID checkedOutByWorkerID,
Timestamp checkedOutTime, Timestamp completedTime, Collection<UUID> inputResources,
Collection<UUID> outputResources, Collection<UUID> jobDependencies)
{
Preconditions.checkNotNull(jobID);
Preconditions.checkNotNull(type);
Preconditions.checkNotNull(status);
Preconditions.checkNotNull(inputResources);
Preconditions.checkNotNull(outputResources);
Preconditions.checkNotNull(jobDependencies);
this.jobID = jobID;
this.type = type;
this.status = status;
this.priority = priority;
this.checkedOutByWorkerID = checkedOutByWorkerID != null ? Optional.of(checkedOutByWorkerID) : Optional.empty();
this.checkedOutTime = checkedOutTime != null ? Optional.of(checkedOutTime) : Optional.empty();
this.completedTime = completedTime != null ? Optional.of(completedTime) : Optional.empty();
this.inputResources = new LinkedList<>(inputResources);
this.outputResources = new LinkedList<>(outputResources);
this.jobDependencies = new LinkedList<>(jobDependencies);
}
public JobModel(Map<String, Object> relation, Map<String, String> properties, Collection<UUID> inputResources,
Collection<UUID> outputResources, Collection<UUID> jobDependencies)
{
Preconditions.checkNotNull(properties);
Preconditions.checkNotNull(relation.get(ID));
Preconditions.checkNotNull(relation.get(JOB_TYPE));
Preconditions.checkNotNull(relation.get(JOB_STATUS));
Preconditions.checkNotNull(inputResources);
Preconditions.checkNotNull(outputResources);
Preconditions.checkNotNull(jobDependencies);
this.jobID = UUID.fromString(relation.get(ID).toString());
this.type = JobType.valueOf(relation.get(JOB_TYPE).toString());
this.status = JobStatus.valueOf(relation.get(JOB_STATUS).toString());
this.priority = Integer.parseInt(relation.get(PRIORITY).toString());
this.checkedOutByWorkerID = relation.get(CHECKED_OUT_BY) != null
? Optional.of((UUID) relation.get(CHECKED_OUT_BY)) : Optional.empty();
this.checkedOutTime = relation.get(CHECKED_OUT_TIME) != null
? Optional.of((Timestamp) relation.get(CHECKED_OUT_TIME)) : Optional.empty();
this.completedTime = relation.get(COMPLETED_TIME) != null
? Optional.of((Timestamp) relation.get(COMPLETED_TIME)) : Optional.empty();
this.inputResources = new LinkedList<>(inputResources);
this.outputResources = new LinkedList<>(outputResources);
this.jobDependencies = new LinkedList<>(jobDependencies);
}
@Override
public void persistToDatabase(Connection connection) throws SQLException
{
Map<String, Object> jobRelation = buildRelationalModel();
List<Map<String, Object>> dependencyRelations = buildDependencyRelations();
List<Map<String, Object>> inputResourceRelations = buildInputResourceRelations();
List<Map<String, Object>> outputResourceRelations = buildOutputResourceRelations();
List<Map<String, Object>> propertyRelations = buildPropertyRelations();
DatabaseUtils.upsert(connection, TABLE_NAME, jobRelation, getPrimaryKeys());
DatabaseUtils.updateJunctionTable(connection, DEPENDENCIES_TABLE_NAME, dependencyRelations,
DEPENDENCIES_FROM_ID, this.jobID);
DatabaseUtils.updateJunctionTable(connection, INPUT_RESOURCES_TABLE_NAME, inputResourceRelations,
RESOURCES_FROM_ID, this.jobID);
DatabaseUtils.updateJunctionTable(connection, OUTPUT_RESOURCES_TABLE_NAME, outputResourceRelations,
RESOURCES_FROM_ID, this.jobID);
DatabaseUtils.updateJunctionTable(connection, PROPERTY_TABLE_NAME, propertyRelations, PROPERTY_JOB_ID,
this.jobID);
connection.commit();
}
private List<String> getPrimaryKeys()
{
List<String> primaryKeys = new LinkedList<>();
primaryKeys.add(ID);
return primaryKeys;
}
protected Map<String, Object> buildRelationalModel()
{
Map<String, Object> relation = new HashMap<>();
relation.put(ID, jobID);
relation.put(JOB_TYPE, type);
relation.put(JOB_STATUS, status);
relation.put(PRIORITY, priority);
relation.put(CHECKED_OUT_BY, checkedOutByWorkerID);
relation.put(CHECKED_OUT_TIME, checkedOutTime);
relation.put(COMPLETED_TIME, completedTime);
return relation;
}
protected List<Map<String, Object>> buildDependencyRelations()
{
List<Map<String, Object>> relations = new LinkedList<>();
for (UUID jobDependencyID : jobDependencies)
{
Map<String, Object> relation = new HashMap<>();
relation.put(DEPENDENCIES_FROM_ID, this.jobID);
relation.put(DEPENDENCIES_TO_ID, jobDependencyID);
relations.add(relation);
}
return relations;
}
protected List<Map<String, Object>> buildInputResourceRelations()
{
List<Map<String, Object>> relations = new LinkedList<>();
for (UUID resourceID : inputResources)
{
Map<String, Object> relation = new HashMap<>();
relation.put(RESOURCES_FROM_ID, this.jobID);
relation.put(RESOURCES_TO_ID, resourceID);
relations.add(relation);
}
return relations;
}
protected List<Map<String, Object>> buildOutputResourceRelations()
{
List<Map<String, Object>> relations = new LinkedList<>();
for (UUID resourceID : outputResources)
{
Map<String, Object> relation = new HashMap<>();
relation.put(RESOURCES_FROM_ID, this.jobID);
relation.put(RESOURCES_TO_ID, resourceID);
relations.add(relation);
}
return relations;
}
private List<Map<String, Object>> buildPropertyRelations()
{
Map<String, String> properties = buildProperties();
List<Map<String, Object>> relations = new LinkedList<>();
for (Entry<String, String> entry : properties.entrySet())
{
Map<String, Object> relation = new HashMap<>();
relation.put(PROPERTY_JOB_ID, this.jobID);
relation.put(PROPERTY_KEY, entry.getKey());
relation.put(PROPERTY_VALUE, entry.getValue());
relations.add(relation);
}
return relations;
}
protected Map<String, String> buildProperties()
{
return new HashMap<>();
}
static List<JobModel> getFromDatabase(Connection connection, JobStatus status) throws SQLException
{
List<JobModel> results = new LinkedList<>();
List<Map<String, Object>> relations = DatabaseUtils.select(connection, TABLE_NAME,
Optional.of(Relation.equalTo(JOB_STATUS, status)));
for (Map<String, Object> relationMap : relations)
{
UUID jobID = UUID.fromString(relationMap.get(ID).toString());
Map<String, String> properties = getPropertiesFromDatabase(connection, jobID);
List<UUID> inputResources = getInputResourcesFromDatabase(connection, jobID);
List<UUID> outputResources = getOutputResourcesFromDatabase(connection, jobID);
List<UUID> dependecies = getDepdendenciesFromDatabase(connection, jobID);
JobModel job = inflateFromMap(relations.get(0), properties, inputResources, outputResources, dependecies);
results.add(job);
}
return results;
}
static Optional<JobModel> getFromDatabase(Connection connection, UUID jobID) throws SQLException
{
List<Map<String, Object>> relations = DatabaseUtils.select(connection, TABLE_NAME,
Optional.of(Relation.equalTo(ID, jobID)));
if (relations.size() == 1)
{
Map<String, String> properties = getPropertiesFromDatabase(connection, jobID);
List<UUID> inputResources = getInputResourcesFromDatabase(connection, jobID);
List<UUID> outputResources = getOutputResourcesFromDatabase(connection, jobID);
List<UUID> dependecies = getDepdendenciesFromDatabase(connection, jobID);
JobModel job = inflateFromMap(relations.get(0), properties, inputResources, outputResources, dependecies);
return Optional.of(job);
}
else if (relations.isEmpty())
{
return Optional.empty();
}
else
{
throw new IllegalStateException("Select of Resource returned multiple objects");
}
}
private static JobModel inflateFromMap(Map<String, Object> map, Map<String, String> properties,
Collection<UUID> inputResources, Collection<UUID> outputResouces, Collection<UUID> dependencies)
{
// Determine the type
JobModel job;
JobType type = JobType.valueOf(map.get(JOB_TYPE).toString());
// Construct the sub-type
if (type == JobType.RUN_SIMULATION)
{
job = new RunSimulationJobModel(map, properties, inputResources, outputResouces, dependencies);
}
else if (type == JobType.CREATE_WORLD)
{
job = new CreateWorldJobModel(map, properties, inputResources, outputResouces, dependencies);
}
else
{
throw new IllegalStateException("Unable to get job type " + type + " from database.");
}
return job;
}
private static List<UUID> getDepdendenciesFromDatabase(Connection connection, UUID jobID) throws SQLException
{
List<Map<String, Object>> relations = DatabaseUtils.select(connection, DEPENDENCIES_TABLE_NAME,
Optional.of(Relation.equalTo(DEPENDENCIES_FROM_ID, jobID)));
List<UUID> dependencies = new LinkedList<>();
for (Map<String, Object> relation : relations)
{
dependencies.add(UUID.fromString(relation.get(DEPENDENCIES_TO_ID).toString()));
}
return dependencies;
}
private static List<UUID> getInputResourcesFromDatabase(Connection connection, UUID jobID) throws SQLException
{
List<Map<String, Object>> relations = DatabaseUtils.select(connection, INPUT_RESOURCES_TABLE_NAME,
Optional.of(Relation.equalTo(RESOURCES_FROM_ID, jobID)));
List<UUID> resources = new LinkedList<>();
for (Map<String, Object> relation : relations)
{
resources.add(UUID.fromString(relation.get(RESOURCES_TO_ID).toString()));
}
return resources;
}
private static List<UUID> getOutputResourcesFromDatabase(Connection connection, UUID jobID) throws SQLException
{
List<Map<String, Object>> relations = DatabaseUtils.select(connection, OUTPUT_RESOURCES_TABLE_NAME,
Optional.of(Relation.equalTo(RESOURCES_FROM_ID, jobID)));
List<UUID> resources = new LinkedList<>();
for (Map<String, Object> relation : relations)
{
resources.add(UUID.fromString(relation.get(RESOURCES_TO_ID).toString()));
}
return resources;
}
private static Map<String, String> getPropertiesFromDatabase(Connection connection, UUID jobID) throws SQLException
{
List<Map<String, Object>> relations = DatabaseUtils.select(connection, PROPERTY_TABLE_NAME,
Optional.of(Relation.equalTo(PROPERTY_JOB_ID, jobID)));
Map<String, String> properties = new HashMap<>();
for (Map<String, Object> relation : relations)
{
properties.put((String) relation.get(PROPERTY_KEY), (String) relation.get(PROPERTY_VALUE));
}
return properties;
}
public static void updateJobStatuses(Connection connection) throws SQLException
{
connection.createStatement().execute(JobSQLStrings.updateStatusString);
connection.commit();
}
public UUID getJobID()
{
return jobID;
}
public JobType getType()
{
return type;
}
public JobStatus getStatus()
{
return status;
}
public int getPriority()
{
return priority;
}
public Optional<UUID> getCheckedOutByWorkerID()
{
return checkedOutByWorkerID;
}
public Optional<Timestamp> getCheckedOutTime()
{
return checkedOutTime;
}
public Optional<Timestamp> getCompletedTime()
{
return completedTime;
}
public Collection<UUID> getInputResources()
{
return inputResources;
}
public Collection<UUID> getOutputResources()
{
return outputResources;
}
public Collection<UUID> getJobDependencies()
{
return jobDependencies;
}
}