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