Resolve conflicts
[idea/community.git] / python / educational-core / student / src / com / jetbrains / edu / learning / courseGeneration / StudyProjectGenerator.java
1 package com.jetbrains.edu.learning.courseGeneration;
2
3 import com.google.gson.*;
4 import com.google.gson.stream.JsonReader;
5 import com.intellij.facet.ui.ValidationResult;
6 import com.intellij.ide.projectView.ProjectView;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.application.PathManager;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.fileEditor.FileEditorManager;
11 import com.intellij.openapi.project.DumbModePermission;
12 import com.intellij.openapi.project.DumbService;
13 import com.intellij.openapi.project.Project;
14 import com.intellij.openapi.ui.Messages;
15 import com.intellij.openapi.util.io.FileUtil;
16 import com.intellij.openapi.vfs.LocalFileSystem;
17 import com.intellij.openapi.vfs.VirtualFile;
18 import com.intellij.openapi.vfs.VirtualFileManager;
19 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
20 import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl;
21 import com.intellij.platform.templates.github.ZipUtil;
22 import com.intellij.psi.PsiFile;
23 import com.intellij.psi.PsiManager;
24 import com.intellij.util.containers.ContainerUtil;
25 import com.jetbrains.edu.learning.StudyProjectComponent;
26 import com.jetbrains.edu.learning.StudyTaskManager;
27 import com.jetbrains.edu.learning.StudyUtils;
28 import com.jetbrains.edu.learning.core.EduNames;
29 import com.jetbrains.edu.learning.core.EduUtils;
30 import com.jetbrains.edu.learning.courseFormat.Course;
31 import com.jetbrains.edu.learning.courseFormat.Lesson;
32 import com.jetbrains.edu.learning.courseFormat.Task;
33 import com.jetbrains.edu.learning.courseFormat.TaskFile;
34 import com.jetbrains.edu.learning.stepic.CourseInfo;
35 import com.jetbrains.edu.learning.stepic.EduStepicConnector;
36 import com.jetbrains.edu.learning.stepic.StepicUser;
37 import org.apache.commons.codec.binary.Base64;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.io.*;
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.List;
45 import java.util.Map;
46
47 public class StudyProjectGenerator {
48   public static final String AUTHOR_ATTRIBUTE = "authors";
49   public static final String LANGUAGE_ATTRIBUTE = "language";
50   public static final String ADAPTIVE_COURSE_PREFIX = "__AdaptivePyCharmPython__";
51   public static final File OUR_COURSES_DIR = new File(PathManager.getConfigPath(), "courses");
52   private static final Logger LOG = Logger.getInstance(StudyProjectGenerator.class.getName());
53   private static final String COURSE_NAME_ATTRIBUTE = "name";
54   private static final String COURSE_DESCRIPTION = "description";
55   private static final String CACHE_NAME = "courseNames.txt";
56   private final List<SettingsListener> myListeners = ContainerUtil.newArrayList();
57   public StepicUser myUser;
58   private List<CourseInfo> myCourses = new ArrayList<>();
59   protected CourseInfo mySelectedCourseInfo;
60
61   public void setCourses(List<CourseInfo> courses) {
62     myCourses = courses;
63   }
64
65   public void setSelectedCourse(@NotNull final CourseInfo courseName) {
66     mySelectedCourseInfo = courseName;
67   }
68
69   public void generateProject(@NotNull final Project project, @NotNull final VirtualFile baseDir) {
70     StudyTaskManager.getInstance(project).setUser(myUser);
71     final Course course = getCourse(project);
72     if (course == null) {
73       LOG.warn("Course is null");
74       return;
75     }
76     final File courseDirectory = StudyUtils.getCourseDirectory(project, course);
77     StudyTaskManager.getInstance(project).setCourse(course);
78     ApplicationManager.getApplication().invokeLater(
79       () -> DumbService.allowStartingDumbModeInside(DumbModePermission.MAY_START_BACKGROUND,
80                                                     () -> ApplicationManager.getApplication().runWriteAction(() -> {
81                                                       StudyGenerator.createCourse(course, baseDir, courseDirectory, project);
82                                                       course.setCourseDirectory(courseDirectory.getAbsolutePath());
83                                                       VirtualFileManager.getInstance().refreshWithoutFileWatcher(true);
84                                                       StudyProjectComponent.getInstance(project).registerStudyToolWindow(course);
85                                                       openFirstTask(course, project);
86                                                     })));
87   }
88
89   @Nullable
90   protected Course getCourse(@NotNull final Project project) {
91       final File courseFile = new File(new File(OUR_COURSES_DIR, mySelectedCourseInfo.getName()), EduNames.COURSE_META_FILE);
92       if (courseFile.exists()) {
93         return readCourseFromCache(courseFile, false);
94       }
95       else {
96         final File adaptiveCourseFile = new File(new File(OUR_COURSES_DIR, ADAPTIVE_COURSE_PREFIX +
97                                                                    mySelectedCourseInfo.getName() + "_" +
98                                                                    myUser.getEmail()), EduNames.COURSE_META_FILE);
99         if (adaptiveCourseFile.exists()) {
100           return readCourseFromCache(adaptiveCourseFile, true);
101         }
102         
103       }
104     final Course course = EduStepicConnector.getCourse(project, mySelectedCourseInfo);
105     if (course != null) {
106       flushCourse(project, course);
107       course.initCourse(false);
108     }
109     return course;
110   }
111
112   @Nullable
113   private static Course readCourseFromCache(@NotNull File courseFile, boolean isAdaptive) {
114     Reader reader = null;
115     try {
116       reader = new InputStreamReader(new FileInputStream(courseFile), "UTF-8");
117       Gson gson = new GsonBuilder().create();
118       final Course course = gson.fromJson(reader, Course.class);
119       course.initCourse(isAdaptive);
120       return course;
121     }
122     catch (UnsupportedEncodingException e) {
123       LOG.warn(e.getMessage());
124     }
125     catch (FileNotFoundException e) {
126       LOG.warn(e.getMessage());
127     }
128     finally {
129       StudyUtils.closeSilently(reader);
130     }
131     return null;
132   }
133
134   public static void openFirstTask(@NotNull final Course course, @NotNull final Project project) {
135     LocalFileSystem.getInstance().refresh(false);
136     final Lesson firstLesson = StudyUtils.getFirst(course.getLessons());
137     final Task firstTask = StudyUtils.getFirst(firstLesson.getTaskList());
138     final VirtualFile taskDir = firstTask.getTaskDir(project);
139     if (taskDir == null) return;
140     final Map<String, TaskFile> taskFiles = firstTask.getTaskFiles();
141     VirtualFile activeVirtualFile = null;
142     for (Map.Entry<String, TaskFile> entry : taskFiles.entrySet()) {
143       final String name = entry.getKey();
144       final TaskFile taskFile = entry.getValue();
145       final VirtualFile virtualFile = ((VirtualDirectoryImpl)taskDir).refreshAndFindChild(name);
146       if (virtualFile != null) {
147         FileEditorManager.getInstance(project).openFile(virtualFile, true);
148         if (!taskFile.getAnswerPlaceholders().isEmpty()) {
149           activeVirtualFile = virtualFile;
150         }
151       }
152     }
153     if (activeVirtualFile != null) {
154       final PsiFile file = PsiManager.getInstance(project).findFile(activeVirtualFile);
155       ProjectView.getInstance(project).select(file, activeVirtualFile, true);
156       FileEditorManager.getInstance(project).openFile(activeVirtualFile, true);
157     } else {
158       String first = StudyUtils.getFirst(taskFiles.keySet());
159       if (first != null) {
160         NewVirtualFile firstFile = ((VirtualDirectoryImpl)taskDir).refreshAndFindChild(first);
161         if (firstFile != null) {
162           FileEditorManager.getInstance(project).openFile(firstFile, true);
163         }
164       }
165     }
166   }
167
168   public static void flushCourse(@NotNull final Project project, @NotNull final Course course) {
169     final File courseDirectory = StudyUtils.getCourseDirectory(project, course);
170     FileUtil.createDirectory(courseDirectory);
171     flushCourseJson(course, courseDirectory);
172
173     int lessonIndex = 1;
174     for (Lesson lesson : course.getLessons()) {
175       if (lesson.getName().equals(EduNames.PYCHARM_ADDITIONAL)) {
176         flushAdditionalFiles(courseDirectory, lesson);
177       }
178       else {
179         final File lessonDirectory = new File(courseDirectory, EduNames.LESSON + String.valueOf(lessonIndex));
180         flushLesson(lessonDirectory, lesson);
181         lessonIndex += 1;
182       }
183     }
184   }
185
186   private static void flushAdditionalFiles(File courseDirectory, Lesson lesson) {
187     final List<Task> taskList = lesson.getTaskList();
188     if (taskList.size() != 1) return;
189     final Task task = taskList.get(0);
190     for (Map.Entry<String, String> entry : task.getTestsText().entrySet()) {
191       final String name = entry.getKey();
192       final String text = entry.getValue();
193       final File file = new File(courseDirectory, name);
194       FileUtil.createIfDoesntExist(file);
195       try {
196         if (EduUtils.isImage(name)) {
197           FileUtil.writeToFile(file, Base64.decodeBase64(text));
198         }
199         else {
200           FileUtil.writeToFile(file, text);
201         }
202       }
203       catch (IOException e) {
204         LOG.error("ERROR copying file " + name);
205       }
206     }
207   }
208
209   public static void flushLesson(@NotNull final File lessonDirectory, @NotNull final Lesson lesson) {
210     FileUtil.createDirectory(lessonDirectory);
211     int taskIndex = 1;
212     for (Task task : lesson.taskList) {
213       final File taskDirectory = new File(lessonDirectory, EduNames.TASK + String.valueOf(taskIndex));
214       flushTask(task, taskDirectory);
215       taskIndex += 1;
216     }
217   }
218
219   public static void flushTask(@NotNull final Task task, @NotNull final File taskDirectory) {
220     FileUtil.createDirectory(taskDirectory);
221     for (Map.Entry<String, TaskFile> taskFileEntry : task.taskFiles.entrySet()) {
222       final String name = taskFileEntry.getKey();
223       final TaskFile taskFile = taskFileEntry.getValue();
224       final File file = new File(taskDirectory, name);
225       FileUtil.createIfDoesntExist(file);
226
227       try {
228         if (EduUtils.isImage(taskFile.name)) {
229           FileUtil.writeToFile(file, Base64.decodeBase64(taskFile.text));
230         }
231         else {
232           FileUtil.writeToFile(file, taskFile.text);
233         }
234
235       }
236       catch (IOException e) {
237         LOG.error("ERROR copying file " + name);
238       }
239     }
240     final Map<String, String> testsText = task.getTestsText();
241     for (Map.Entry<String, String> entry : testsText.entrySet()) {
242       final File testsFile = new File(taskDirectory, entry.getKey());
243       if (testsFile.exists()) {
244         FileUtil.delete(testsFile);
245       }
246       FileUtil.createIfDoesntExist(testsFile);
247       try {
248           FileUtil.writeToFile(testsFile, entry.getValue());
249       }
250       catch (IOException e) {
251         LOG.error("ERROR copying tests file");
252       }
253     }
254     final File taskText = new File(taskDirectory, "task.html");
255     FileUtil.createIfDoesntExist(taskText);
256     try {
257       FileUtil.writeToFile(taskText, task.getText());
258     }
259     catch (IOException e) {
260       LOG.error("ERROR copying tests file");
261     }
262   }
263
264   public static void flushCourseJson(@NotNull final Course course, @NotNull final File courseDirectory) {
265     final Gson gson = new GsonBuilder().setPrettyPrinting().create();
266     final String json = gson.toJson(course);
267     final File courseJson = new File(courseDirectory, EduNames.COURSE_META_FILE);
268     final FileOutputStream fileOutputStream;
269     try {
270       fileOutputStream = new FileOutputStream(courseJson);
271       OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8");
272       try {
273         outputStreamWriter.write(json);
274       }
275       catch (IOException e) {
276         Messages.showErrorDialog(e.getMessage(), "Failed to Generate Json");
277         LOG.info(e);
278       }
279       finally {
280         try {
281           outputStreamWriter.close();
282         }
283         catch (IOException e) {
284           LOG.info(e);
285         }
286       }
287     }
288     catch (FileNotFoundException | UnsupportedEncodingException e) {
289       LOG.info(e);
290     }
291   }
292
293   /**
294    * Writes courses to cache file {@link StudyProjectGenerator#CACHE_NAME}
295    */
296   @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
297   public static void flushCache(List<CourseInfo> courses) {
298     File cacheFile = new File(OUR_COURSES_DIR, CACHE_NAME);
299     PrintWriter writer = null;
300     try {
301       if (!createCacheFile(cacheFile)) return;
302       Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
303
304       writer = new PrintWriter(cacheFile);
305       for (CourseInfo courseInfo : courses) {
306         final String json = gson.toJson(courseInfo);
307         writer.println(json);
308       }
309     }
310     catch (IOException e) {
311       LOG.error(e);
312     }
313     finally {
314       StudyUtils.closeSilently(writer);
315     }
316   }
317
318   private static boolean createCacheFile(File cacheFile) throws IOException {
319     if (!OUR_COURSES_DIR.exists()) {
320       final boolean created = OUR_COURSES_DIR.mkdirs();
321       if (!created) {
322         LOG.error("Cannot flush courses cache. Can't create courses directory");
323         return false;
324       }
325     }
326     if (!cacheFile.exists()) {
327       final boolean created = cacheFile.createNewFile();
328       if (!created) {
329         LOG.error("Cannot flush courses cache. Can't create " + CACHE_NAME + " file");
330         return false;
331       }
332     }
333     return true;
334   }
335
336   public List<CourseInfo> getCourses(boolean force) {
337     if (OUR_COURSES_DIR.exists()) {
338       myCourses = getCoursesFromCache();
339     }
340     if (force || myCourses.isEmpty()) {
341       myCourses = EduStepicConnector.getCourses();
342       flushCache(myCourses);
343     }
344     if (myCourses.isEmpty()) {
345       myCourses = getBundledIntro();
346     }
347     return myCourses;
348   }
349
350   public void addSettingsStateListener(@NotNull SettingsListener listener) {
351     myListeners.add(listener);
352   }
353
354   public interface SettingsListener {
355     void stateChanged(ValidationResult result);
356   }
357
358   public void fireStateChanged(ValidationResult result) {
359     for (SettingsListener listener : myListeners) {
360       listener.stateChanged(result);
361     }
362   }
363
364   public static List<CourseInfo> getBundledIntro() {
365     final File introCourse = new File(OUR_COURSES_DIR, "Introduction to Python");
366     if (introCourse.exists()) {
367       final CourseInfo courseInfo = getCourseInfo(introCourse);
368
369       return Collections.singletonList(courseInfo);
370     }
371     return Collections.emptyList();
372   }
373
374   public static List<CourseInfo> getCoursesFromCache() {
375     List<CourseInfo> courses = new ArrayList<>();
376     final File cacheFile = new File(OUR_COURSES_DIR, CACHE_NAME);
377     if (!cacheFile.exists()) {
378       return courses;
379     }
380     try {
381       final FileInputStream inputStream = new FileInputStream(cacheFile);
382       try {
383         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
384         try {
385           String line;
386           while ((line = reader.readLine()) != null) {
387             Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
388             final CourseInfo courseInfo = gson.fromJson(line, CourseInfo.class);
389             if (!courseInfo.isAdaptive()) {
390               courses.add(courseInfo);
391             }
392           }
393         }
394         catch (IOException | JsonSyntaxException e) {
395           LOG.error(e.getMessage());
396         }
397         finally {
398           StudyUtils.closeSilently(reader);
399         }
400       } finally {
401         StudyUtils.closeSilently(inputStream);
402       }
403     }
404     catch (FileNotFoundException e) {
405       LOG.error(e.getMessage());
406     }
407     return courses;
408   }
409   /**
410    * Adds course from zip archive to courses
411    *
412    * @return added course name or null if course is invalid
413    */
414   @Nullable
415   public CourseInfo addLocalCourse(String zipFilePath) {
416     File file = new File(zipFilePath);
417     try {
418       String fileName = file.getName();
419       String unzippedName = fileName.substring(0, fileName.indexOf("."));
420       File courseDir = new File(OUR_COURSES_DIR, unzippedName);
421       ZipUtil.unzip(null, courseDir, file, null, null, true);
422       CourseInfo courseName = addCourse(myCourses, courseDir);
423       flushCache(myCourses);
424       if (courseName != null && !courseName.getName().equals(unzippedName)) {
425         //noinspection ResultOfMethodCallIgnored
426         courseDir.renameTo(new File(OUR_COURSES_DIR, courseName.getName()));
427         //noinspection ResultOfMethodCallIgnored
428         courseDir.delete();
429       }
430       return courseName;
431     }
432     catch (IOException e) {
433       LOG.error("Failed to unzip course archive");
434       LOG.error(e);
435     }
436     return null;
437   }
438
439   /**
440    * Adds course to courses specified in params
441    *
442    *
443    * @param courses
444    * @param courseDir must be directory containing course file
445    * @return added course name or null if course is invalid
446    */
447   @Nullable
448   private static CourseInfo addCourse(List<CourseInfo> courses, File courseDir) {
449     if (courseDir.isDirectory()) {
450       File[] courseFiles = courseDir.listFiles((dir, name) -> {
451         return name.equals(EduNames.COURSE_META_FILE);
452       });
453       if (courseFiles.length != 1) {
454         LOG.info("User tried to add course with more than one or without course files");
455         return null;
456       }
457       File courseFile = courseFiles[0];
458       CourseInfo courseInfo = getCourseInfo(courseFile);
459       if (courseInfo != null) {
460         courses.add(courseInfo);
461       }
462       return courseInfo;
463     }
464     return null;
465   }
466   /**
467    * Parses course json meta file and finds course name
468    *
469    * @return information about course or null if course file is invalid
470    */
471   @Nullable
472   private static CourseInfo getCourseInfo(File courseFile) {
473     if (courseFile.isDirectory()) {
474       File[] courseFiles = courseFile.listFiles((dir, name) -> {
475         return name.equals(EduNames.COURSE_META_FILE);
476       });
477       if (courseFiles.length != 1) {
478         LOG.info("More than one or without course files");
479         return null;
480       }
481       courseFile = courseFiles[0];
482     }
483     CourseInfo courseInfo = null;
484     BufferedReader reader = null;
485     try {
486       if (courseFile.getName().equals(EduNames.COURSE_META_FILE)) {
487         reader = new BufferedReader(new InputStreamReader(new FileInputStream(courseFile), "UTF-8"));
488         JsonReader r = new JsonReader(reader);
489         JsonParser parser = new JsonParser();
490         JsonElement el = parser.parse(r);
491         String courseName = el.getAsJsonObject().get(COURSE_NAME_ATTRIBUTE).getAsString();
492         String courseDescription = el.getAsJsonObject().get(COURSE_DESCRIPTION).getAsString();
493         JsonArray courseAuthors = el.getAsJsonObject().get(AUTHOR_ATTRIBUTE).getAsJsonArray();
494         String language = el.getAsJsonObject().get(LANGUAGE_ATTRIBUTE).getAsString();
495         courseInfo = new CourseInfo();
496         courseInfo.setName(courseName);
497         courseInfo.setDescription(courseDescription);
498         courseInfo.setType("pycharm " + language);
499         final ArrayList<StepicUser> authors = new ArrayList<>();
500         for (JsonElement author : courseAuthors) {
501           final JsonObject authorAsJsonObject = author.getAsJsonObject();
502           final StepicUser stepicUser = new StepicUser();
503           stepicUser.setFirstName(authorAsJsonObject.get("first_name").getAsString());
504           stepicUser.setLastName(authorAsJsonObject.get("last_name").getAsString());
505           authors.add(stepicUser);
506         }
507         courseInfo.setAuthors(authors);
508       }
509     }
510     catch (Exception e) {
511       //error will be shown in UI
512     }
513     finally {
514       StudyUtils.closeSilently(reader);
515     }
516     return courseInfo;
517   }
518 }