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