d2e7d54dd2156bc5784c79aa31e7ab82bb0c4f18
[idea/community.git] / python / educational-core / student / src / com / jetbrains / edu / learning / StudySerializationUtils.java
1 package com.jetbrains.edu.learning;
2
3 import com.google.gson.*;
4 import com.google.gson.reflect.TypeToken;
5 import com.intellij.openapi.editor.Document;
6 import com.intellij.openapi.editor.EditorFactory;
7 import com.intellij.openapi.fileEditor.FileDocumentManager;
8 import com.intellij.openapi.project.Project;
9 import com.intellij.openapi.util.io.FileUtil;
10 import com.intellij.openapi.vfs.LocalFileSystem;
11 import com.intellij.openapi.vfs.VirtualFile;
12 import com.intellij.util.containers.ContainerUtil;
13 import com.intellij.util.containers.hash.HashMap;
14 import com.jetbrains.edu.learning.core.EduNames;
15 import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder;
16 import com.jetbrains.edu.learning.courseFormat.Course;
17 import com.jetbrains.edu.learning.courseFormat.StudyStatus;
18 import com.jetbrains.edu.learning.courseFormat.TaskFile;
19 import org.jdom.Attribute;
20 import org.jdom.Element;
21 import org.jdom.output.XMLOutputter;
22
23 import java.io.File;
24 import java.lang.reflect.Type;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.Map;
29
30 public class StudySerializationUtils {
31
32   public static final String PLACEHOLDERS = "placeholders";
33   public static final String LINE = "line";
34   public static final String START = "start";
35   public static final String LENGTH = "length";
36   public static final String POSSIBLE_ANSWER = "possible_answer";
37   public static final String HINT = "hint";
38   public static final String ADDITIONAL_HINTS = "additional_hints";
39   public static final String OFFSET = "offset";
40   public static final String TEXT = "text";
41   public static final String LESSONS = "lessons";
42   public static final String COURSE = "course";
43   public static final String COURSE_TITLED = "Course";
44   public static final String STATUS = "status";
45   public static final String AUTHOR = "author";
46   public static final String AUTHORS = "authors";
47   public static final String MY_INITIAL_START = "myInitialStart";
48
49   private StudySerializationUtils() {
50   }
51
52   public static class StudyUnrecognizedFormatException extends Exception {
53   }
54
55   public static class Xml {
56     public final static String COURSE_ELEMENT = "courseElement";
57     public final static String MAIN_ELEMENT = "StudyTaskManager";
58     public static final String MAP = "map";
59     public static final String KEY = "key";
60     public static final String VALUE = "value";
61     public static final String NAME = "name";
62     public static final String LIST = "list";
63     public static final String OPTION = "option";
64     public static final String INDEX = "index";
65     public static final String STUDY_STATUS_MAP = "myStudyStatusMap";
66     public static final String TASK_STATUS_MAP = "myTaskStatusMap";
67     public static final String LENGTH = "length";
68     public static final String ANSWER_PLACEHOLDERS = "answerPlaceholders";
69     public static final String TASK_LIST = "taskList";
70     public static final String TASK_FILES = "taskFiles";
71     public static final String INITIAL_STATE = "initialState";
72     public static final String MY_INITIAL_STATE = "MyInitialState";
73     public static final String MY_LINE = "myLine";
74     public static final String MY_START = "myStart";
75     public static final String MY_LENGTH = "myLength";
76     public static final String HINT = "hint";
77     public static final String AUTHOR_TITLED = "Author";
78     public static final String FIRST_NAME = "first_name";
79     public static final String SECOND_NAME = "second_name";
80     public static final String MY_INITIAL_LINE = "myInitialLine";
81     public static final String MY_INITIAL_LENGTH = "myInitialLength";
82     public static final String ANSWER_PLACEHOLDER = "AnswerPlaceholder";
83     public static final String TASK_WINDOWS = "taskWindows";
84     public static final String RESOURCE_PATH = "resourcePath";
85     public static final String COURSE_DIRECTORY = "courseDirectory";
86     public static final String SUBTASK_INFO = "AnswerPlaceholderSubtaskInfo";
87     public static final String SUBTASK_INFOS = "subtaskInfos";
88     public static final String ADDITIONAL_HINTS = "additionalHints";
89     public static final String POSSIBLE_ANSWER = "possibleAnswer";
90     public static final String SELECTED = "selected";
91     public static final String TASK_TEXT = "taskText";
92     public static final String PLACEHOLDER_TEXT = "placeholderText";
93
94     private Xml() {
95     }
96
97     public static int getVersion(Element element) throws StudyUnrecognizedFormatException {
98       if (element.getChild(COURSE_ELEMENT) != null) {
99         return 1;
100       }
101
102       final Element taskManager = element.getChild(MAIN_ELEMENT);
103
104       Element versionElement = getChildWithName(taskManager, "VERSION");
105       if (versionElement == null) {
106         return -1;
107       }
108
109       return Integer.valueOf(versionElement.getAttributeValue(VALUE));
110     }
111
112     public static Element convertToSecondVersion(Element element) throws StudyUnrecognizedFormatException {
113       final Element oldCourseElement = element.getChild(COURSE_ELEMENT);
114       Element state = new Element(MAIN_ELEMENT);
115
116       Element course = addChildWithName(state, COURSE, oldCourseElement.clone());
117       course.setName(COURSE_TITLED);
118
119       Element author = getChildWithName(course, AUTHOR);
120       String authorString = author.getAttributeValue(VALUE);
121       course.removeContent(author);
122
123       String[] names = authorString.split(" ", 2);
124       Element authorElement = new Element(AUTHOR_TITLED);
125       addChildWithName(authorElement, FIRST_NAME, names[0]);
126       addChildWithName(authorElement, SECOND_NAME, names.length == 1 ? "" : names[1]);
127
128       addChildList(course, AUTHORS, Collections.singletonList(authorElement));
129
130       Element courseDirectoryElement = getChildWithName(course, RESOURCE_PATH);
131       renameElement(courseDirectoryElement, COURSE_DIRECTORY);
132
133       for (Element lesson : getChildList(course, LESSONS)) {
134         incrementIndex(lesson);
135         for (Element task : getChildList(lesson, TASK_LIST)) {
136           incrementIndex(task);
137           Map<String, Element> taskFiles = getChildMap(task, TASK_FILES);
138           for (Element taskFile : taskFiles.values()) {
139             renameElement(getChildWithName(taskFile, TASK_WINDOWS), ANSWER_PLACEHOLDERS);
140             for (Element placeholder : getChildList(taskFile, ANSWER_PLACEHOLDERS)) {
141               placeholder.setName(ANSWER_PLACEHOLDER);
142
143               Element initialState = new Element(MY_INITIAL_STATE);
144               addChildWithName(placeholder, INITIAL_STATE, initialState);
145               addChildWithName(initialState, MY_LINE, getChildWithName(placeholder, MY_INITIAL_LINE).getAttributeValue(VALUE));
146               addChildWithName(initialState, MY_START, getChildWithName(placeholder, MY_INITIAL_START).getAttributeValue(VALUE));
147               addChildWithName(initialState, MY_LENGTH, getChildWithName(placeholder, MY_INITIAL_LENGTH).getAttributeValue(VALUE));
148             }
149           }
150         }
151       }
152       element.removeContent();
153       element.addContent(state);
154       return element;
155     }
156
157     public static Map<String, String> fillStatusMap(Element taskManagerElement, String mapName, XMLOutputter outputter)
158       throws StudyUnrecognizedFormatException {
159       Map<Element, String> sourceMap = getChildMap(taskManagerElement, mapName);
160       Map<String, String> destMap = new HashMap<>();
161       for (Map.Entry<Element, String> entry : sourceMap.entrySet()) {
162         String status = entry.getValue();
163         if (status.equals(StudyStatus.Unchecked.toString())) {
164           continue;
165         }
166         destMap.put(outputter.outputString(entry.getKey()), status);
167       }
168       return destMap;
169     }
170
171     public static Element convertToThirdVersion(Element state, Project project) throws StudyUnrecognizedFormatException {
172       Element taskManagerElement = state.getChild(MAIN_ELEMENT);
173       XMLOutputter outputter = new XMLOutputter();
174
175       Map<String, String> placeholderTextToStatus = fillStatusMap(taskManagerElement, STUDY_STATUS_MAP, outputter);
176       Map<String, String> taskFileToStatusMap = fillStatusMap(taskManagerElement, TASK_STATUS_MAP, outputter);
177
178       Element courseElement = getChildWithName(taskManagerElement, COURSE).getChild(COURSE_TITLED);
179       for (Element lesson : getChildList(courseElement, LESSONS)) {
180         int lessonIndex = getAsInt(lesson, INDEX);
181         for (Element task : getChildList(lesson, TASK_LIST)) {
182           String taskStatus = null;
183           int taskIndex = getAsInt(task, INDEX);
184           Map<String, Element> taskFiles = getChildMap(task, TASK_FILES);
185           for (Map.Entry<String, Element> entry : taskFiles.entrySet()) {
186             Element taskFileElement = entry.getValue();
187             String taskFileText = outputter.outputString(taskFileElement);
188             String taskFileStatus = taskFileToStatusMap.get(taskFileText);
189             if (taskFileStatus != null && (taskStatus == null || taskFileStatus.equals(StudyStatus.Failed.toString()))) {
190               taskStatus = taskFileStatus;
191             }
192             Document document = StudyUtils.getDocument(project.getBasePath(), lessonIndex, taskIndex, entry.getKey());
193             if (document == null) {
194               continue;
195             }
196             for (Element placeholder : getChildList(taskFileElement, ANSWER_PLACEHOLDERS)) {
197               taskStatus = addStatus(outputter, placeholderTextToStatus, taskStatus, placeholder);
198               addOffset(document, placeholder);
199               addInitialState(document, placeholder);
200             }
201           }
202           if (taskStatus != null) {
203             addChildWithName(task, STATUS, taskStatus);
204           }
205         }
206       }
207       return state;
208     }
209
210     public static Element convertToForthVersion(Element state) throws StudyUnrecognizedFormatException {
211       Element taskManagerElement = state.getChild(MAIN_ELEMENT);
212       Element courseElement = getChildWithName(taskManagerElement, COURSE).getChild(COURSE_TITLED);
213       for (Element lesson : getChildList(courseElement, LESSONS)) {
214         for (Element task : getChildList(lesson, TASK_LIST)) {
215           Map<String, Element> taskFiles = getChildMap(task, TASK_FILES);
216           for (Map.Entry<String, Element> entry : taskFiles.entrySet()) {
217             Element taskFileElement = entry.getValue();
218             for (Element placeholder : getChildList(taskFileElement, ANSWER_PLACEHOLDERS)) {
219               Element valueElement = new Element(SUBTASK_INFO);
220               addChildMap(placeholder, SUBTASK_INFOS, Collections.singletonMap(String.valueOf(0), valueElement));
221               for (String childName : ContainerUtil
222                 .list(HINT, ADDITIONAL_HINTS, POSSIBLE_ANSWER, SELECTED, STATUS, TASK_TEXT)) {
223                 Element child = getChildWithName(placeholder, childName);
224                 valueElement.addContent(child.clone());
225               }
226               renameElement(getChildWithName(valueElement, TASK_TEXT), PLACEHOLDER_TEXT);
227               List<Element> additionalHints = ContainerUtil.map(getChildList(valueElement, ADDITIONAL_HINTS), Element::clone);
228               Element hint = getChildWithName(valueElement, HINT);
229               Element firstHint = new Element(OPTION).setAttribute(VALUE, hint.getAttributeValue(VALUE));
230               List<Element> newHints = new ArrayList<>();
231               newHints.add(firstHint);
232               newHints.addAll(additionalHints);
233               addChildList(valueElement, "hints", newHints);
234             }
235           }
236         }
237       }
238
239       return state;
240     }
241
242     public static String addStatus(XMLOutputter outputter,
243                                    Map<String, String> placeholderTextToStatus,
244                                    String taskStatus,
245                                    Element placeholder) {
246       String placeholderText = outputter.outputString(placeholder);
247       String status = placeholderTextToStatus.get(placeholderText);
248       if (status != null) {
249         addChildWithName(placeholder, STATUS, status);
250         if (taskStatus == null || status.equals(StudyStatus.Failed.toString())) {
251           taskStatus = status;
252         }
253       }
254       return taskStatus;
255     }
256
257     public static void addInitialState(Document document, Element placeholder) throws StudyUnrecognizedFormatException {
258       Element initialState = getChildWithName(placeholder, INITIAL_STATE).getChild(MY_INITIAL_STATE);
259       int initialLine = getAsInt(initialState, MY_LINE);
260       int initialStart = getAsInt(initialState, MY_START);
261       int initialOffset = document.getLineStartOffset(initialLine) + initialStart;
262       addChildWithName(initialState, OFFSET, initialOffset);
263       renameElement(getChildWithName(initialState, MY_LENGTH), LENGTH);
264     }
265
266     public static void addOffset(Document document, Element placeholder) throws StudyUnrecognizedFormatException {
267       int line = getAsInt(placeholder, LINE);
268       int start = getAsInt(placeholder, START);
269       int offset = document.getLineStartOffset(line) + start;
270       addChildWithName(placeholder, OFFSET, offset);
271     }
272
273     public static int getAsInt(Element element, String name) throws StudyUnrecognizedFormatException {
274       return Integer.valueOf(getChildWithName(element, name).getAttributeValue(VALUE));
275     }
276
277     public static void incrementIndex(Element element) throws StudyUnrecognizedFormatException {
278       Element index = getChildWithName(element, INDEX);
279       int indexValue = Integer.parseInt(index.getAttributeValue(VALUE));
280       changeValue(index, indexValue + 1);
281     }
282
283     public static void renameElement(Element element, String newName) {
284       element.setAttribute(NAME, newName);
285     }
286
287     public static void changeValue(Element element, Object newValue) {
288       element.setAttribute(VALUE, newValue.toString());
289     }
290
291     public static Element addChildWithName(Element parent, String name, Element value) {
292       Element child = new Element(OPTION);
293       child.setAttribute(NAME, name);
294       child.addContent(value);
295       parent.addContent(child);
296       return value;
297     }
298
299     public static Element addChildWithName(Element parent, String name, Object value) {
300       Element child = new Element(OPTION);
301       child.setAttribute(NAME, name);
302       child.setAttribute(VALUE, value.toString());
303       parent.addContent(child);
304       return child;
305     }
306
307     public static Element addChildList(Element parent, String name, List<Element> elements) {
308       Element listElement = new Element(LIST);
309       for (Element element : elements) {
310         listElement.addContent(element);
311       }
312       return addChildWithName(parent, name, listElement);
313     }
314
315     public static Element addChildMap(Element parent, String name, Map<String, Element> value) {
316       Element mapElement = new Element(MAP);
317       for (Map.Entry<String, Element> entry : value.entrySet()) {
318         Element entryElement = new Element("entry");
319         mapElement.addContent(entryElement);
320         String key = entry.getKey();
321         entryElement.setAttribute("key", key);
322         Element valueElement = new Element("value");
323         valueElement.addContent(entry.getValue());
324         entryElement.addContent(valueElement);
325       }
326       return addChildWithName(parent, name, mapElement);
327     }
328
329     public static List<Element> getChildList(Element parent, String name) throws StudyUnrecognizedFormatException {
330       return getChildList(parent, name, false);
331     }
332
333     public static List<Element> getChildList(Element parent, String name, boolean optional) throws StudyUnrecognizedFormatException {
334       Element listParent = getChildWithName(parent, name, optional);
335       if (listParent != null) {
336         Element list = listParent.getChild(LIST);
337         if (list != null) {
338           return list.getChildren();
339         }
340       }
341       return Collections.emptyList();
342     }
343
344     public static Element getChildWithName(Element parent, String name) throws StudyUnrecognizedFormatException {
345       return getChildWithName(parent, name, false);
346     }
347
348     public static Element getChildWithName(Element parent, String name, boolean optional) throws StudyUnrecognizedFormatException {
349       for (Element child : parent.getChildren()) {
350         Attribute attribute = child.getAttribute(NAME);
351         if (attribute == null) {
352           continue;
353         }
354         if (name.equals(attribute.getValue())) {
355           return child;
356         }
357       }
358       if (optional) {
359         return null;
360       }
361       throw new StudyUnrecognizedFormatException();
362     }
363
364     public static <K, V> Map<K, V> getChildMap(Element element, String name) throws StudyUnrecognizedFormatException {
365       return getChildMap(element, name, false);
366     }
367
368     public static <K, V> Map<K, V> getChildMap(Element element, String name, boolean optional) throws StudyUnrecognizedFormatException {
369       Element mapParent = getChildWithName(element, name, optional);
370       if (mapParent != null) {
371         Element map = mapParent.getChild(MAP);
372         if (map != null) {
373           HashMap result = new HashMap();
374           for (Element entry : map.getChildren()) {
375             Object key = entry.getAttribute(KEY) == null ? entry.getChild(KEY).getChildren().get(0) : entry.getAttributeValue(KEY);
376             Object value = entry.getAttribute(VALUE) == null ? entry.getChild(VALUE).getChildren().get(0) : entry.getAttributeValue(VALUE);
377             result.put(key, value);
378           }
379           return result;
380         }
381       }
382       return Collections.emptyMap();
383     }
384   }
385
386   public static class Json {
387
388     public static final String TASK_LIST = "task_list";
389     public static final String TASK_FILES = "task_files";
390
391     private Json() {
392     }
393
394     public static class CourseTypeAdapter implements JsonDeserializer<Course> {
395
396       private final File myCourseFile;
397
398       public CourseTypeAdapter(File courseFile) {
399         myCourseFile = courseFile;
400       }
401
402       @Override
403       public Course deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
404         JsonObject courseObject = json.getAsJsonObject();
405         JsonArray lessons = courseObject.getAsJsonArray(LESSONS);
406         for (int lessonIndex = 1; lessonIndex <= lessons.size(); lessonIndex++) {
407           JsonObject lessonObject = lessons.get(lessonIndex - 1).getAsJsonObject();
408           JsonArray tasks = lessonObject.getAsJsonArray(TASK_LIST);
409           for (int taskIndex = 1; taskIndex <= tasks.size(); taskIndex++) {
410             JsonObject taskObject = tasks.get(taskIndex - 1).getAsJsonObject();
411             for (Map.Entry<String, JsonElement> taskFile : taskObject.getAsJsonObject(TASK_FILES).entrySet()) {
412               String name = taskFile.getKey();
413               String filePath = FileUtil.join(myCourseFile.getParent(), EduNames.LESSON + lessonIndex, EduNames.TASK + taskIndex, name);
414               VirtualFile resourceFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(new File(filePath));
415               if (resourceFile == null) {
416                 continue;
417               }
418               Document document = FileDocumentManager.getInstance().getDocument(resourceFile);
419               if (document == null) {
420                 continue;
421               }
422               JsonObject taskFileObject = taskFile.getValue().getAsJsonObject();
423               JsonArray placeholders = taskFileObject.getAsJsonArray(PLACEHOLDERS);
424               for (JsonElement placeholder : placeholders) {
425                 JsonObject placeholderObject = placeholder.getAsJsonObject();
426                 if (placeholderObject.getAsJsonPrimitive(OFFSET) != null) {
427                   break;
428                 }
429                 int line = placeholderObject.getAsJsonPrimitive(LINE).getAsInt();
430                 int start = placeholderObject.getAsJsonPrimitive(START).getAsInt();
431                 int offset = document.getLineStartOffset(line) + start;
432                 placeholderObject.addProperty(OFFSET, offset);
433               }
434             }
435           }
436         }
437         return new GsonBuilder().create().fromJson(json, Course.class);
438       }
439     }
440
441     public static class StepicTaskFileAdapter implements JsonDeserializer<TaskFile> {
442
443       @Override
444       public TaskFile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
445         final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
446         JsonObject taskFileObject = json.getAsJsonObject();
447         JsonArray placeholders = taskFileObject.getAsJsonArray(PLACEHOLDERS);
448         for (JsonElement placeholder : placeholders) {
449           JsonObject placeholderObject = placeholder.getAsJsonObject();
450           int line = placeholderObject.getAsJsonPrimitive(LINE).getAsInt();
451           int start = placeholderObject.getAsJsonPrimitive(START).getAsInt();
452           if (line == -1) {
453             placeholderObject.addProperty(OFFSET, start);
454           }
455           else {
456             Document document = EditorFactory.getInstance().createDocument(taskFileObject.getAsJsonPrimitive(TEXT).getAsString());
457             placeholderObject.addProperty(OFFSET, document.getLineStartOffset(line) + start);
458           }
459           final String hintString = placeholderObject.getAsJsonPrimitive(HINT).getAsString();
460           final JsonArray hintsArray = new JsonArray();
461
462           try {
463             final Type listType = new TypeToken<List<String>>() {}.getType();
464             final List<String> hints = gson.fromJson(hintString, listType);
465             if (hints != null && !hints.isEmpty()) {
466               for (int i = 0; i < hints.size(); i++) {
467                 if (i == 0) {
468                   placeholderObject.addProperty(HINT, hints.get(0));
469                   continue;
470                 }
471                 hintsArray.add(hints.get(i));
472               }
473               placeholderObject.add(ADDITIONAL_HINTS, hintsArray);
474             }
475             else {
476               placeholderObject.addProperty(HINT, "");
477             }
478           }
479           catch (JsonParseException e) {
480             hintsArray.add(hintString);
481           }
482         }
483
484         return gson.fromJson(json, TaskFile.class);
485       }
486     }
487
488     public static class StepicAnswerPlaceholderAdapter implements JsonSerializer<AnswerPlaceholder> {
489       @Override
490       public JsonElement serialize(AnswerPlaceholder src, Type typeOfSrc, JsonSerializationContext context) {
491         final List<String> hints = src.getHints();
492
493         final int length = src.getLength();
494         final int start = src.getOffset();
495         final String possibleAnswer = src.getPossibleAnswer();
496         int line = -1;
497
498         final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
499         final JsonObject answerPlaceholder = new JsonObject();
500         answerPlaceholder.addProperty(LINE, line);
501         answerPlaceholder.addProperty(START, start);
502         answerPlaceholder.addProperty(LENGTH, length);
503         answerPlaceholder.addProperty(POSSIBLE_ANSWER, possibleAnswer);
504
505         final String jsonHints = gson.toJson(hints);
506         answerPlaceholder.addProperty(HINT, jsonHints);
507
508         return answerPlaceholder;
509       }
510     }
511   }
512 }