package fi.utu.ville.exercises.helpers; import java.io.UnsupportedEncodingException; import java.lang.reflect.Type; import java.util.Hashtable; import java.util.Map; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.TypeAdapter; 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.PersistenceHandler; import fi.utu.ville.exercises.model.SubmissionInfo; import fi.utu.ville.standardutils.AbstractFile; import fi.utu.ville.standardutils.TempFilesManager; /** * <p> * Helper class for implementing {@link PersistenceHandler} by utilizing {@link Gson} to convert {@link ExerciseData} and {@link SubmissionInfo} objects to * JSON. * </p> * <p> * In addition to normally Gson-convertable fields, the {@link ExerciseData} (possibly later also {@link SubmissionInfo}) can contain {@link AbstractFile} * -fields which will be saved and loaded by reference utilizing {@link ByRefSaver} and {@link ByRefLoader}. * </p> * <p> * Utilizing this class is the recommended method of implementing {@link PersistenceHandler} when possible (when {@link ExerciseData} and {@link SubmissionInfo} * classes comply to {@link Gson}-requirements). * </p> * * <p> * For fields that are not Gson-convertable, {@link TypeAdapter} can be registered to handle the field. * </p> * * @author Riku Haavisto * * @param <E> * used {@link ExerciseData}-implementor * @param <S> * used {@link SubmissionInfo}-implementor */ public class GsonPersistenceHandler<E extends ExerciseData, S extends SubmissionInfo> implements PersistenceHandler<E, S> { /** * */ private static final long serialVersionUID = -2601025744571919374L; private final Class<E> exerDataClass; private final Class<S> submInfoClass; private final Hashtable<Type, Object> typeAdapters = new Hashtable<Type, Object>(); /** * Generates a new {@link GsonPersistenceHandler} that can be returned from {@link ExerciseTypeDescriptor #newExerciseXML()}-method. The returned class is * stateless, meaning there is no need to instantiate more than one instance per exercise-type (though that's only a minor performance improvement). * * @param exerDataClass * {@link Class}-object representing the {@link ExerciseData} -implementor * @param submInfoClass * {@link Class}-object representing the {@link SubmissionInfo} -implementor */ public GsonPersistenceHandler(Class<E> exerDataClass, Class<S> submInfoClass) { this.exerDataClass = exerDataClass; this.submInfoClass = submInfoClass; } /** * Registers a type adapter to use with Json conversion. If a type isn't converted or loaded from Json properly this method can be used to register a type * adapter for said type. * * @param type * type to register the adapter to * @param typeAdapter * typeadapter to use. This object must implement at least one of the InstanceCreator, JsonSerializer, and a JsonDeserializer interfaces. */ public void registerTypeAdapter(Type type, Object typeAdapter) { typeAdapters.put(type, typeAdapter); } @Override public byte[] saveExerData(E etd, TempFilesManager tempManager, ByRefSaver refSaver) throws ExerciseException { GsonBuilder gsonBuilder = getGsonSaver(refSaver); Gson ser = gsonBuilder.create(); String json = ser.toJson(etd); try { return json.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { // Won't happen throw new IllegalStateException("UTF-8 missing", e); } } @Override public E loadExerData(byte[] dataPres, TempFilesManager tempManager, ByRefLoader refLoader) throws ExerciseException { try { String src = new String(dataPres, "UTF-8"); GsonBuilder gsonBuilder = getGsonLoader(refLoader); Gson ser = gsonBuilder.create(); E res = ser.fromJson(src, exerDataClass); return res; } catch (UnsupportedEncodingException e) { // Won't happen throw new IllegalStateException("UTF-8 missing", e); } } @Override public byte[] saveSubmission(S subm, TempFilesManager tempManager) throws ExerciseException { Gson ser = getSubmissionGson(); String json = ser.toJson(subm); try { return json.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { // Won't happen throw new IllegalStateException("UTF-8 missing", e); } } @Override public S loadSubmission(byte[] dataPres, boolean forStatGiver, TempFilesManager tempManager) throws ExerciseException { try { String src = new String(dataPres, "UTF-8"); Gson ser = getSubmissionGson(); S res = ser.fromJson(src, submInfoClass); return res; } catch (UnsupportedEncodingException e) { // Won't happen throw new IllegalStateException("UTF-8 missing", e); } } private GsonBuilder getGsonLoader(ByRefLoader refLoader) { GsonBuilder gsonBuilder = new GsonBuilder(); ByRefDeserializer brSer = new ByRefDeserializer(refLoader); gsonBuilder.registerTypeAdapter(AbstractFile.class, brSer); registerCustomAdapters(gsonBuilder); return gsonBuilder; } private GsonBuilder getGsonSaver(ByRefSaver refSaver) { GsonBuilder gsonBuilder = new GsonBuilder(); ByRefSerializer brSer = new ByRefSerializer(refSaver); gsonBuilder.registerTypeAdapter(AbstractFile.class, brSer); registerCustomAdapters(gsonBuilder); return gsonBuilder; } private Gson getSubmissionGson() { GsonBuilder gsonBuilder = new GsonBuilder(); registerCustomAdapters(gsonBuilder); return gsonBuilder.create(); } private void registerCustomAdapters(GsonBuilder builder) { for (Map.Entry<Type, Object> adapter : typeAdapters.entrySet()) { builder.registerTypeAdapter(adapter.getKey(), adapter.getValue()); } } /** * {@link JsonSerializer}-implementor that can be used to automatically save {@link AbstractFile}s by reference in {@link Gson}-conversion utilizing the * provided {@link ByRefSaver}. * * @author Riku Haavisto * */ public static class ByRefSerializer implements JsonSerializer<AbstractFile> { private final ByRefSaver refSaver; public ByRefSerializer(ByRefSaver refSaver) { this.refSaver = refSaver; } @Override public JsonElement serialize(AbstractFile src, Type typeOfSrc, JsonSerializationContext context) { String refStr = ""; try { refStr = refSaver.saveByReference(src); } catch (ExerciseException e) { e.printStackTrace(); } return new JsonPrimitive(refStr); } } /** * {@link JsonDeserializer}-implementor that can be used to automatically load {@link AbstractFile}s saved by reference in {@link Gson}-conversion utilizing * the provided {@link ByRefLoader}. The 'load'-context of {@link ByRefLoader} must match the 'save'-context {@link ByRefSaver} used when the the * {@link Gson}-conversion was made. * * @author Riku Haavisto * */ public static class ByRefDeserializer implements JsonDeserializer<AbstractFile> { private final ByRefLoader refLoader; public ByRefDeserializer(ByRefLoader refLoader) { this.refLoader = refLoader; } @Override public AbstractFile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { AbstractFile res = null; try { res = refLoader.loadByReference(json.getAsString()); } catch (ExerciseException e) { e.printStackTrace(); } return res; } } }