EDU-619 Replace line-offset placeholder representation with absolute offset
[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.Nullable;
20
21 import java.io.File;
22 import java.lang.reflect.Type;
23 import java.util.Arrays;
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 OFFSET = "offset";
34   public static final String TEXT = "text";
35   public static final String LESSONS = "lessons";
36   public static final String COURSE = "course";
37   public static final String COURSE_TITLED = "Course";
38   public static final String STATUS = "status";
39   public static final String AUTHOR = "author";
40   public static final String AUTHORS = "authors";
41   public static final String MY_INITIAL_START = "myInitialStart";
42
43   private StudySerializationUtils() {
44   }
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) {
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) {
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, Arrays.asList(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       Map<Element, String> sourceMap = getChildMap(taskManagerElement, mapName);
143       Map<String, String> destMap = new HashMap<>();
144       for (Map.Entry<Element, String> entry : sourceMap.entrySet()) {
145         String status = entry.getValue();
146         if (status.equals(StudyStatus.Unchecked.toString())) {
147           continue;
148         }
149         destMap.put(outputter.outputString(entry.getKey()), status);
150       }
151       return destMap;
152     }
153
154     public static Element convertToThirdVersion(Element state, Project project) {
155       Element taskManagerElement = state.getChild(MAIN_ELEMENT);
156       XMLOutputter outputter = new XMLOutputter();
157
158       Map<String, String> placeholderTextToStatus = fillStatusMap(taskManagerElement, STUDY_STATUS_MAP, outputter);
159       Map<String, String> taskFileToStatusMap = fillStatusMap(taskManagerElement, TASK_STATUS_MAP, outputter);
160
161       Element courseElement = getChildWithName(taskManagerElement, COURSE).getChild(COURSE_TITLED);
162       for (Element lesson : getChildList(courseElement, LESSONS)) {
163         int lessonIndex = getAsInt(lesson, INDEX);
164         for (Element task : getChildList(lesson, TASK_LIST)) {
165           String taskStatus = null;
166           int taskIndex = getAsInt(task, INDEX);
167           Map<String, Element> taskFiles = getChildMap(task, TASK_FILES);
168           for (Map.Entry<String, Element> entry : taskFiles.entrySet()) {
169             Element taskFileElement = entry.getValue();
170             String taskFileText = outputter.outputString(taskFileElement);
171             String taskFileStatus = taskFileToStatusMap.get(taskFileText);
172             if (taskFileStatus != null && (taskStatus == null || taskFileStatus.equals(StudyStatus.Failed.toString()))) {
173               taskStatus = taskFileStatus;
174             }
175             Document document = StudyUtils.getDocument(project.getBasePath(), lessonIndex, taskIndex, entry.getKey());
176             if (document == null) {
177               continue;
178             }
179             for (Element placeholder : getChildList(taskFileElement, ANSWER_PLACEHOLDERS)) {
180               taskStatus = addStatus(outputter, placeholderTextToStatus, taskStatus, placeholder);
181               addOffset(document, placeholder);
182               addInitialState(document, placeholder);
183             }
184           }
185           if (taskStatus != null) {
186             addChildWithName(task, STATUS, taskStatus);
187           }
188         }
189       }
190       return state;
191     }
192
193     public static String addStatus(XMLOutputter outputter,
194                                    Map<String, String> placeholderTextToStatus,
195                                    String taskStatus,
196                                    Element placeholder) {
197       String placeholderText = outputter.outputString(placeholder);
198       String status = placeholderTextToStatus.get(placeholderText);
199       if (status != null) {
200         addChildWithName(placeholder, STATUS, status);
201         if (taskStatus == null || status.equals(StudyStatus.Failed.toString())) {
202           taskStatus = status;
203         }
204       }
205       return taskStatus;
206     }
207
208     public static void addInitialState(Document document, Element placeholder) {
209       Element initialState = getChildWithName(placeholder, INITIAL_STATE).getChild(MY_INITIAL_STATE);
210       int initialLine = getAsInt(initialState, MY_LINE);
211       int initialStart = getAsInt(initialState, MY_START);
212       int initialOffset = document.getLineStartOffset(initialLine) + initialStart;
213       addChildWithName(initialState, OFFSET, initialOffset);
214       renameElement(getChildWithName(initialState, MY_LENGTH), LENGTH);
215     }
216
217     public static void addOffset(Document document, Element placeholder) {
218       int line = getAsInt(placeholder, LINE);
219       int start = getAsInt(placeholder, START);
220       int offset = document.getLineStartOffset(line) + start;
221       addChildWithName(placeholder, OFFSET, offset);
222     }
223
224     public static int getAsInt(Element element, String name) {
225       return Integer.valueOf(getChildWithName(element, name).getAttributeValue(VALUE));
226     }
227
228     public static void incrementIndex(Element element) {
229       Element index = getChildWithName(element, INDEX);
230       int indexValue = Integer.parseInt(index.getAttributeValue(VALUE));
231       changeValue(index, indexValue + 1);
232     }
233
234     public static void renameElement(Element element, String newName) {
235       element.setAttribute(NAME, newName);
236     }
237
238     public static void changeValue(Element element, Object newValue) {
239       element.setAttribute(VALUE, newValue.toString());
240     }
241
242     public static Element addChildWithName(Element parent, String name, Element value) {
243       Element child = new Element(OPTION);
244       child.setAttribute(NAME, name);
245       child.addContent(value);
246       parent.addContent(child);
247       return value;
248     }
249
250     public static Element addChildWithName(Element parent, String name, Object value) {
251       Element child = new Element(OPTION);
252       child.setAttribute(NAME, name);
253       child.setAttribute(VALUE, value.toString());
254       parent.addContent(child);
255       return child;
256     }
257
258     public static Element addChildList(Element parent, String name, List<Element> elements) {
259       Element listElement = new Element(LIST);
260       for (Element element : elements) {
261         listElement.addContent(element);
262       }
263       return addChildWithName(parent, name, listElement);
264     }
265
266     public static List<Element> getChildList(Element parent, String name) {
267       Element listParent = getChildWithName(parent, name);
268       if (listParent != null) {
269         Element list = listParent.getChild(LIST);
270         if (list != null) {
271           return list.getChildren();
272         }
273       }
274       return Collections.emptyList();
275     }
276
277     @Nullable
278     public static Element getChildWithName(Element parent, String name) {
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       return null;
289     }
290
291     public static <K, V> Map<K, V> getChildMap(Element element, String name) {
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 }