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