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