package fi.utu.ville.exercises.stub;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import fi.utu.ville.exercises.model.Editor;
import fi.utu.ville.exercises.model.ExerciseData;
import fi.utu.ville.exercises.model.ExerciseException;
import fi.utu.ville.exercises.model.ExerciseTypeDescriptor;
import fi.utu.ville.exercises.model.GeneralExerciseInfo;
import fi.utu.ville.exercises.model.PersistenceHandler;
import fi.utu.ville.exercises.model.StatisticalSubmissionInfo;
import fi.utu.ville.exercises.model.SubmissionInfo;
import fi.utu.ville.standardutils.TempFilesManager;
/**
* A class handling file operations to store data needed by the exercise-stub's persistence layer. Stores and loads {@link ExerciseData}- and
* {@link SubmissionInfo} -instances. All files are stored inside the folder returned by {@link VilleExerStubUI #getStubResourceBaseDir()}.
*
* @author Riku Haavisto
*
*/
final class StubDataFilesHandler {
private static final Logger logger = Logger
.getLogger(StubDataFilesHandler.class.getName());
private static final String submDataFileExt = ".subm";
private static final String submDirName = "submissions";
private static final String xmlFileName = "exer-instance.xml";
private static final String descFileName = "description.txt";
private static final String matFolderName = "materials";
private StubDataFilesHandler() {
// only static methods
}
/**
* Returns the base-directory for storing files belonging to certain exercise-type ({@link ExerciseTypeDescriptor}) and certain exercise-instance identified
* by its name.
*
* @param type
* {@link ExerciseTypeDescriptor} representing given exercise-type
* @param exerName
* name (or id) of certain exercise-instance
* @return folder to be used for storing data belonging to certain exercise-type and -instance
*/
private static String getExerInstanceBaseDir(
ExerciseTypeDescriptor<?, ?> type, String exerName) {
return StubSessionData.getInstance().getTypeBaseDir(type)
+ File.separatorChar + exerName;
}
/**
* Returns a folder for storing submission-data files for certain exercise-instance of certain exercise-type.
*
* @param type
* {@link ExerciseTypeDescriptor} representing given exercise-type
* @param exerName
* name (or id) of certain exercise-instance
* @return folder to be used for storing submission-data belonging to certain exercise-type and -instance
*/
private static String getSubmBaseDir(ExerciseTypeDescriptor<?, ?> type,
String exerName) {
return StubSessionData.getInstance().getTypeBaseDir(type)
+ File.separatorChar + exerName + File.separatorChar
+ submDirName;
}
/**
* Returns path to be used for storing the serialized {@link ExerciseData} (not necessarily XML) corresponding to certain exercise-instance of certain
* exercise-type.
*
* @param type
* {@link ExerciseTypeDescriptor} representing given exercise-type
* @param exerName
* name (or id) of certain exercise-instance
* @return path to store data representing certain exercise-instance of certain exercise-type
*/
private static String parseCorrXmlPath(ExerciseTypeDescriptor<?, ?> type,
String exerName) {
return StubSessionData.getInstance().getTypeBaseDir(type)
+ File.separator + exerName + File.separator + xmlFileName;
}
/**
* Returns a folder for storing material-files for certain exercise-instance of certain exercise-type (the "material-scope")
*
* @param type
* {@link ExerciseTypeDescriptor} representing given exercise-type
* @param exerName
* name (or id) of certain exercise-instance
* @return folder to be used for storing material-files belonging to certain exercise-type and -instance
*/
private static String parseCorrMatScope(ExerciseTypeDescriptor<?, ?> type,
String exerName) {
return StubSessionData.getInstance().getTypeBaseDir(type)
+ File.separator + exerName + File.separator + matFolderName;
}
/**
* Returns path to be used for storing description corresponding to certain exercise-instance of certain exercise-type.
*
* @param type
* {@link ExerciseTypeDescriptor} representing given exercise-type
* @param exerName
* name (or id) of certain exercise-instance
* @return path to store description for certain exercise-instance of certain exercise-type
*/
private static String getDescPath(ExerciseTypeDescriptor<?, ?> type,
String exerName) {
return getExerInstanceBaseDir(type, exerName) + File.separator
+ descFileName;
}
/**
* Initializes the folder hierarchy for certain exercise-instance of certain exercise-type if the hierarchy does not yet exist.
*
* @param type
* {@link ExerciseTypeDescriptor} corresponding to used exercise-type
* @param exerName
* name (or id) of certain exercise-instance
*/
private static void createIfNeededExerInstanceFolderStructure(
ExerciseTypeDescriptor<?, ?> type, String exerName) {
if ("".equals(exerName)) {
throw new IllegalArgumentException(
"ExerName cannot be an empty string!");
}
String instBase = getExerInstanceBaseDir(type, exerName);
File instBaseF = new File(instBase);
if (!instBaseF.exists()) {
if (!instBaseF.mkdir()) {
// TODO some error msg
}
}
String instSubmBase = getSubmBaseDir(type, exerName);
File instSubmF = new File(instSubmBase);
if (!instSubmF.exists()) {
if (!instSubmF.mkdir()) {
// TODO some error msg
}
}
}
/**
* Loads names of all the exercise-instances that are present in currently used stub-resources-folder for certain exercise-type.
*
* @param type
* {@link ExerciseTypeDescriptor} for exercise-type for which to find existing exercise-instances
* @return names of all currently existing exercise-instances for certain exercise-type
*/
public static List<String> getPresentExerInstances(
ExerciseTypeDescriptor<?, ?> type) {
ArrayList<String> res = new ArrayList<String>();
File typeBase = new File(StubSessionData.getInstance().getTypeBaseDir(
type));
// TODO: some checks?
for (File file : typeBase.listFiles()) {
if (file.isDirectory()) {
res.add(file.getName());
}
}
return res;
}
/**
* Deletes an exercise-instance and all the data belonging to it.
*
* @param type
* {@link ExerciseTypeDescriptor} of the exercise-type of the exercise to delete
* @param exerName
* name of the exercise-instance to delete
*/
public static void deleteExerInstance(ExerciseTypeDescriptor<?, ?> type,
String exerName) {
String instanceBasePath = getExerInstanceBaseDir(type, exerName);
File instBaseFolder = new File(instanceBasePath);
if (instBaseFolder.exists()) {
StubUtil.deleteDirectory(instBaseFolder, true);
}
}
/**
* Loads the bytes storing the {@link SubmissionInfo} that was last made to certain exercise-instance of certain exercise-type. This method is mainly useful
* for loading the actual bytes stored, so that the user can inspect them through stub-UI to see whether there is something odd in them.
*
* @param type
* {@link ExerciseTypeDescriptor} of exercise-type to load
* @param exerName
* name (or id) of the exercise-instance
* @return bytes stored from latest {@link SubmissionInfo} made to given exercise-instance
*/
public static byte[] loadLatestBareSubmInfo(
ExerciseTypeDescriptor<?, ?> type, String exerName) {
byte[] res = null;
File fromFolder = new File(getSubmBaseDir(type, exerName));
int largestUsedOrderNum = getLargestUsedOrderNum(fromFolder);
if (largestUsedOrderNum == 0) {
return null;
}
String fileToLoadPath = fromFolder.getAbsolutePath() + File.separator
+ largestUsedOrderNum + submDataFileExt;
byte[] submDataPres = null;
try {
File submDataFile = new File(fileToLoadPath);
if (submDataFile.exists()) {
submDataPres = FileUtils.readFileToByteArray(submDataFile);
res = StatisticalSubmInfoSerializer.INSTANCE
.loadOnlySubmDataForInspecting(submDataPres);
}
} catch (FileNotFoundException e) {
logger.log(Level.SEVERE,
"Could not find the submission-data file for some reason",
e);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return res;
}
/**
* Loads the latest {@link SubmissionInfo} made to certain exercise instance and casts it to correct class (type parameter S).
*
* @param submDataClass
* actual class {@link Class} to use for {@link SubmissionInfo}
* @param type
* {@link ExerciseTypeDescriptor} of the used exercise-type
* @param exerName
* name (or id) of the used exercise-instance
* @param tempManager
* {@link TempFilesManager} for managing temporary files
* @return {@link SubmissionInfo} of correct type S that was last made to given exercise-instance
*/
public static <S extends SubmissionInfo> S loadLatestSubmInfo(
Class<S> submDataClass, ExerciseTypeDescriptor<?, S> type,
String exerName, TempFilesManager tempManager) {
S res = null;
StatisticalSubmissionInfo<S> genStatInfo = null;
File fromFolder = new File(getSubmBaseDir(type, exerName));
int largestUsedOrderNum = getLargestUsedOrderNum(fromFolder);
if (largestUsedOrderNum == 0) {
return null;
}
PersistenceHandler<?, S> dataLoader = type.newExerciseXML();
String fileToLoadPath = fromFolder.getAbsolutePath() + File.separator
+ largestUsedOrderNum + submDataFileExt;
byte[] submDataPres = null;
try {
File submDataFile = new File(fileToLoadPath);
if (submDataFile.exists()) {
submDataPres = FileUtils.readFileToByteArray(submDataFile);
// TODO FIXME
genStatInfo = StatisticalSubmInfoSerializer.INSTANCE.load(
submDataPres, false, dataLoader, tempManager);
res = genStatInfo.getSubmissionData();
}
} catch (FileNotFoundException e) {
logger.log(Level.SEVERE,
"Could not find the submission-data file for some reason",
e);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return res;
}
/**
* Writes the given {@link StatisticalSubmissionInfo} object in serialized form to the disck to correct path.
*
* @param type
* {@link ExerciseTypeDescriptor} for the exercise-type used
* @param exerName
* name (or id) of the exercise-instance to use
* @param submInfo
* {@link StatisticalSubmissionInfo} to write to disk
* @param tempManager
* {@link TempFilesManager} used
*/
public static <S extends SubmissionInfo> void writeSubmToDisk(
ExerciseTypeDescriptor<?, S> type, String exerName,
StatisticalSubmissionInfo<S> submInfo, TempFilesManager tempManager) {
File exerSubmFolder = new File(getSubmBaseDir(type, exerName));
String submDataSavePath = exerSubmFolder.getAbsolutePath()
+ File.separator + getNextFreeOrderNum(exerSubmFolder)
+ submDataFileExt;
File outFile = new File(submDataSavePath);
try {
FileUtils.writeByteArrayToFile(
outFile,
StatisticalSubmInfoSerializer.INSTANCE.save(submInfo,
type.newExerciseXML(), tempManager));
} catch (FileNotFoundException e) {
logger.log(Level.SEVERE, "Missing the submission-data folder", e);
} catch (IOException e) {
logger.log(Level.SEVERE, "Problems writing to stream", e);
}
}
/**
* Finds the largest number that is currently used as a file name in certain folder.
*
* @param fromFolder
* folder in which files are stored using ascending numbers as names
* @return largest number currently in use as a file name in given folder
*/
private static int getLargestUsedOrderNum(File fromFolder) {
int largestUsed = 0;
if (!(fromFolder.exists() && fromFolder.isDirectory())) {
throw new IllegalStateException("Submission-folder ( " + fromFolder
+ " ) does not exist.");
}
try {
for (String aSubm : fromFolder.list()) {
int orderNum = Integer.parseInt(aSubm.split("\\.")[0]);
if (orderNum > largestUsed) {
largestUsed = orderNum;
}
}
} catch (NumberFormatException e) {
throw new IllegalStateException(
"Inconsistency (wrong naming pattern) in the submission-folder: "
+ fromFolder,
e);
}
return largestUsed;
}
/**
* Finds the next number that can safely be used as a file name in a folder where files are named with ascending numbers.
*
* @param fromFolder
* from which folder to look for next free order number
* @return next free number in given folder
*/
private static int getNextFreeOrderNum(File fromFolder) {
return getLargestUsedOrderNum(fromFolder) + 1;
}
/**
* Loads all submissions made to certain exercise-instance.
*
* @param type
* {@link ExerciseTypeDescriptor) representing the used exercise-type
* @param exerName
* name (or id) of the used exercise-instance
* @param submDataType
* {@link Class} of the used {@link SubmissionInfo}
* @param tempManager
* {@link TempFilesManager} for managing temporary files
* @return all the {@link StatisticalSubmissionInfo}-objects representing submissions made to certain exercise-instance
*/
public static <S extends SubmissionInfo> List<StatisticalSubmissionInfo<S>> loadAllSubmissions(
ExerciseTypeDescriptor<?, S> type, String exerName,
Class<S> submDataType, TempFilesManager tempManager) {
ArrayList<StatisticalSubmissionInfo<S>> res = new ArrayList<StatisticalSubmissionInfo<S>>();
PersistenceHandler<?, S> loadHandler = type.newExerciseXML();
File fromFolder = new File(getSubmBaseDir(type, exerName));
if (!(fromFolder.exists() && fromFolder.isDirectory())) {
throw new IllegalStateException("Submission-folder ( " + fromFolder
+ " ) does not exist.");
}
try {
logger.info("Loading submission-data-objects from folder "
+ fromFolder);
for (String aSubm : fromFolder.list()) {
byte[] submDataPres = null;
try {
File submDataFile = new File(fromFolder.getAbsolutePath()
+ File.separator + aSubm);
if (submDataFile.exists()) {
submDataPres = FileUtils
.readFileToByteArray(submDataFile);
StatisticalSubmissionInfo<?> genSubmInfo = StatisticalSubmInfoSerializer.INSTANCE
.load(submDataPres, true, loadHandler,
tempManager);
res.add(castSubminfo(submDataType, genSubmInfo));
}
} catch (FileNotFoundException e) {
logger.log(
Level.SEVERE,
"Could not find the submission-data file for some reason",
e);
}
}
} catch (FileNotFoundException e) {
logger.log(Level.SEVERE, "Could not find the submission-data-file",
e);
} catch (IOException e) {
logger.log(Level.SEVERE, "Error reading submission-data stream", e);
}
return res;
}
/**
* Casts a {@link StatisticalSubmissionInfo} to correct type parameter S of the correct {@link SubmissionInfo}-implementor.
*
* @param submDataClass
* {@link Class} of correct {@link SubmissionInfo}
* @param genInfo
* a generic {@link StatisticalSubmissionInfo} to be casted
* @return {@link StatisticalSubmissionInfo} casted to use correct type-parameter S
*/
private static <S extends SubmissionInfo> StatisticalSubmissionInfo<S> castSubminfo(
Class<S> submDataClass, StatisticalSubmissionInfo<?> genInfo) {
return new StatisticalSubmissionInfo<S>(genInfo.getTimeOnTask(),
genInfo.getEvalution(), genInfo.getDoneTime(),
submDataClass.cast(genInfo.getSubmissionData()));
}
/**
* Saves a new exercise-instance to disk
*
* @param type
* {@link ExerciseTypeDescriptor} for used exercise-type
* @param exerInfo
* {@link GeneralExerciseInfo} containing name and description for the exercise to be saved
* @param dataToSave
* {@link ExerciseData} object containing the actual exercise data to be serialized and saved
* @param tempManager
* {@link TempFilesManager} for handling temporary files
* @throws ExerciseException
* if something goes wrong when saving the exercise-instance
*/
public static <E extends ExerciseData> void saveExerStream(
ExerciseTypeDescriptor<E, ?> type, GeneralExerciseInfo exerInfo,
E dataToSave, TempFilesManager tempManager)
throws ExerciseException {
String exerName = exerInfo.getName();
// it is possible that we are making new exercise
createIfNeededExerInstanceFolderStructure(type, exerName);
String filePath = parseCorrXmlPath(type, exerName);
String matScope = parseCorrMatScope(type, exerName);
StubMatPersistenceHandler matSaver = new StubMatPersistenceHandler(
matScope, true);
try {
byte[] toSave = type.newExerciseXML().saveExerData(dataToSave,
tempManager, matSaver);
FileUtils.writeByteArrayToFile(new File(filePath), toSave);
FileUtils.writeStringToFile(new File(getDescPath(type, exerName)),
exerInfo.getDescription(), "UTF-8");
} catch (FileNotFoundException e) {
logger.log(
Level.SEVERE,
"Could not find the file to save the submission-data stream to",
e);
} catch (IOException e) {
logger.log(Level.SEVERE, "Error wrinting submission-data stream", e);
} finally {
matSaver.cleanupObsoleteFiles();
}
}
/**
* Loads the description from disk to certain exercise-instance.
*
* @param type
* {@link ExerciseTypeDescriptor} for exercise-type used
* @param exerName
* name (or id) of the exercise-instance
* @return description of the given exercise-instance
*/
public static String getExerDescription(ExerciseTypeDescriptor<?, ?> type,
String exerName) {
String res = "";
String pathToData = getDescPath(type, exerName);
Scanner scanner = null;
try {
scanner = new Scanner(new File(pathToData), "UTF-8");
while (scanner.hasNextLine()) {
res += scanner.nextLine();
}
} catch (FileNotFoundException e) {
logger.log(Level.WARNING, "Could not load exercise-description", e);
} finally {
if (scanner != null) {
scanner.close();
}
}
return res;
}
/**
* Loads a serialized data of an exercise-instance and parses it to correct {@link ExerciseData}-object
*
* @param exerTypeDataClass
* {@link Class}-file representing the correct {@link ExerciseData}-implementor
* @param persistenceHandler
* {@link PersistenceHandler} to be used for parsing the serialized data to {@link ExerciseData}-object
* @param type
* {@link ExerciseTypeDescriptor} for exercise-type to use
* @param exerName
* name (or id) of the exercise-instance to use
* @param tempManager
* {@link TempFilesManager} to handle temporary files
* @return parsed {@link ExerciseData}-object, or null if the {@link Editor} is to be loaded for editing a new exercise
*/
public static <E extends ExerciseData, S extends SubmissionInfo> E loadExerDataForEditor(
Class<E> exerTypeDataClass,
PersistenceHandler<E, S> persistenceHandler,
ExerciseTypeDescriptor<?, ?> type, String exerName,
TempFilesManager tempManager) {
E res = null;
// new exercise
if ("".equals(exerName)) {
return res;
}
String pathToData = parseCorrXmlPath(type, exerName);
// it is intended behavior to return null for editor if no xml-
// file exists at the moment (whereas to the executor that would be
// an unrecoverable error)
if ((new File(pathToData)).exists()) {
try {
res = loadExerTypeData(exerTypeDataClass, persistenceHandler,
type, exerName, tempManager);
} catch (ExerciseException e) {
// Should this be reported to user ; at least in a notification
// that creating new because the old exercise could not be
// loaded..?
logger.log(
Level.WARNING,
"Could not load old exercise; possibly did not exist..?; "
+ "returning null for editor to create a totally new exercise",
e);
}
}
return res;
}
/**
* Loads a serialized data of an exercise-instance and parses it to correct {@link ExerciseData}-object. Opposed to the
* {@link #loadExerDataForEditor(Class, PersistenceHandler, ExerciseTypeDescriptor, String, TempFilesManager) loadExerDataForEditor()} this method is meant
* to be used only when the exercise-data should exist.
*
* @param exerTypeDataClass
* {@link Class}-file representing the correct {@link ExerciseData}-implementor
* @param persistenceHandler
* {@link PersistenceHandler} to be used for parsing the serialized data to {@link ExerciseData}-object
* @param type
* {@link ExerciseTypeDescriptor} for exercise-type to use
* @param exerName
* name (or id) of the exercise-instance to use
* @param tempManager
* {@link TempFilesManager} to handle temporary files
* @return parsed {@link ExerciseData}-object
*/
public static <E extends ExerciseData, S extends SubmissionInfo> E loadExerTypeData(
Class<E> exerTypeDataClass,
PersistenceHandler<E, S> persistenceHandler,
ExerciseTypeDescriptor<?, ?> type, String exerName,
TempFilesManager tempManager) throws ExerciseException {
E res = null;
String pathToData = parseCorrXmlPath(type, exerName);
byte[] dataPres = null;
StubMatPersistenceHandler matLoader = new StubMatPersistenceHandler(
parseCorrMatScope(type, exerName), false);
try {
dataPres = FileUtils.readFileToByteArray(new File(pathToData));
res = persistenceHandler.loadExerData(dataPres, tempManager,
matLoader);
} catch (FileNotFoundException e) {
throw new ExerciseException(
ExerciseException.ErrorType.EXERCISE_XML_NOT_FOUND,
"Stub xml not found from path: " + pathToData, e);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return res;
}
/**
* Loads the bytes stored for certain exercise-instace. This method is mainly useful for loading the bytes so that user of the stub can inspect them
* straight from the stub-UI.
*
* @param type
* {@link ExerciseTypeDescriptor} of the used exercise-type
* @param exerName
* name (or id) of the exercise-instance
* @return bytes storing the data of certain exercise-instance
*/
public static byte[] loadExerTypeData(ExerciseTypeDescriptor<?, ?> type,
String exerName) {
String pathToData = parseCorrXmlPath(type, exerName);
byte[] dataPres = null;
try {
dataPres = FileUtils.readFileToByteArray(new File(pathToData));
} catch (FileNotFoundException e) {
logger.log(Level.SEVERE, "Stub xml not found from path: "
+ pathToData, e);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return dataPres;
}
}