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