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