1 package com.jetbrains.edu.learning.courseGeneration;
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;
46 import static com.jetbrains.edu.learning.StudyUtils.execCancelable;
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;
63 public void setCourses(List<CourseInfo> courses) {
67 public boolean isLoggedIn() {
68 return myUser != null && !StringUtil.isEmptyOrSpaces(myUser.getPassword()) && !StringUtil.isEmptyOrSpaces(myUser.getEmail());
71 public void setEnrolledCoursesIds(@NotNull final List<Integer> coursesIds) {
72 myEnrolledCoursesIds = coursesIds;
76 public List<Integer> getEnrolledCoursesIds() {
77 return myEnrolledCoursesIds;
80 public void setSelectedCourse(@NotNull final CourseInfo courseName) {
81 mySelectedCourseInfo = courseName;
84 public void generateProject(@NotNull final Project project, @NotNull final VirtualFile baseDir) {
86 StudyTaskManager.getInstance(project).setUser(myUser);
88 final Course course = getCourse(project);
90 LOG.warn("Course is null");
91 Messages.showWarningDialog("Some problems occurred while creating the course", "Error in Course Creation");
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);
107 protected Course getCourse(@NotNull final Project project) {
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);
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);
121 return ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
122 ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true);
123 return execCancelable(() -> {
125 final Course course = EduStepicConnector.getCourse(project, mySelectedCourseInfo);
126 if (course != null) {
127 flushCourse(project, course);
128 course.initCourse(false);
132 }, "Creating Course", true, project);
136 private static Course readCourseFromCache(@NotNull File courseFile, boolean isAdaptive) {
137 Reader reader = null;
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);
145 catch (UnsupportedEncodingException e) {
146 LOG.warn(e.getMessage());
148 catch (FileNotFoundException e) {
149 LOG.warn(e.getMessage());
152 StudyUtils.closeSilently(reader);
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;
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);
182 String first = StudyUtils.getFirst(taskFiles.keySet());
184 NewVirtualFile firstFile = ((VirtualDirectoryImpl)taskDir).refreshAndFindChild(first);
185 if (firstFile != null) {
186 FileEditorManager.getInstance(project).openFile(firstFile, true);
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);
198 for (Lesson lesson : course.getLessons()) {
199 if (lesson.getName().equals(EduNames.PYCHARM_ADDITIONAL)) {
200 flushAdditionalFiles(courseDirectory, lesson);
203 final File lessonDirectory = new File(courseDirectory, EduNames.LESSON + String.valueOf(lessonIndex));
204 flushLesson(lessonDirectory, lesson);
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);
220 if (EduUtils.isImage(name)) {
221 FileUtil.writeToFile(file, Base64.decodeBase64(text));
224 FileUtil.writeToFile(file, text);
227 catch (IOException e) {
228 LOG.error("ERROR copying file " + name);
233 public static void flushLesson(@NotNull final File lessonDirectory, @NotNull final Lesson lesson) {
234 FileUtil.createDirectory(lessonDirectory);
236 for (Task task : lesson.taskList) {
237 final File taskDirectory = new File(lessonDirectory, EduNames.TASK + String.valueOf(taskIndex));
238 flushTask(task, taskDirectory);
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);
252 if (EduUtils.isImage(taskFile.name)) {
253 FileUtil.writeToFile(file, Base64.decodeBase64(taskFile.text));
256 FileUtil.writeToFile(file, taskFile.text);
259 catch (IOException e) {
260 LOG.error("ERROR copying file " + name);
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);
269 FileUtil.createIfDoesntExist(testsFile);
271 FileUtil.writeToFile(testsFile, entry.getValue());
273 catch (IOException e) {
274 LOG.error("ERROR copying tests file");
277 final File taskText = new File(taskDirectory, "task.html");
278 FileUtil.createIfDoesntExist(taskText);
280 FileUtil.writeToFile(taskText, task.getText());
282 catch (IOException e) {
283 LOG.error("ERROR copying tests file");
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;
294 fileOutputStream = new FileOutputStream(courseJson);
295 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8");
297 outputStreamWriter.write(json);
299 catch (IOException e) {
300 Messages.showErrorDialog(e.getMessage(), "Failed to Generate Json");
305 outputStreamWriter.close();
307 catch (IOException e) {
312 catch (FileNotFoundException | UnsupportedEncodingException e) {
318 * Writes courses to cache file {@link StudyProjectGenerator#CACHE_NAME}
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;
325 if (!createCacheFile(cacheFile)) return;
326 Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
328 final Set<CourseInfo> courseInfos = new HashSet<>();
329 courseInfos.addAll(courses);
330 courseInfos.addAll(getCoursesFromCache());
332 writer = new PrintWriter(cacheFile);
333 for (CourseInfo courseInfo : courseInfos) {
334 final String json = gson.toJson(courseInfo);
335 writer.println(json);
338 catch (IOException e) {
342 StudyUtils.closeSilently(writer);
346 private static boolean createCacheFile(File cacheFile) throws IOException {
347 if (!OUR_COURSES_DIR.exists()) {
348 final boolean created = OUR_COURSES_DIR.mkdirs();
350 LOG.error("Cannot flush courses cache. Can't create courses directory");
354 if (!cacheFile.exists()) {
355 final boolean created = cacheFile.createNewFile();
357 LOG.error("Cannot flush courses cache. Can't create " + CACHE_NAME + " file");
364 // Supposed to be called under progress
365 public List<CourseInfo> getCourses(boolean force) {
366 if (OUR_COURSES_DIR.exists()) {
367 myCourses = getCoursesFromCache();
369 if (force || myCourses.isEmpty()) {
370 myCourses = execCancelable(EduStepicConnector::getCourses);
371 flushCache(myCourses);
373 if (myCourses.isEmpty()) {
374 myCourses = getBundledIntro();
380 public List<CourseInfo> getCoursesUnderProgress(boolean force, @NotNull final String progressTitle, @NotNull final Project project) {
382 return ProgressManager.getInstance()
383 .runProcessWithProgressSynchronously(() -> {
384 ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true);
385 return getCourses(force);
386 }, progressTitle, true, project);
388 catch (RuntimeException e) {
389 return Collections.singletonList(CourseInfo.INVALID_COURSE);
393 public void addSettingsStateListener(@NotNull SettingsListener listener) {
394 myListeners.add(listener);
397 public interface SettingsListener {
398 void stateChanged(ValidationResult result);
401 public void fireStateChanged(ValidationResult result) {
402 for (SettingsListener listener : myListeners) {
403 listener.stateChanged(result);
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);
412 return Collections.singletonList(courseInfo);
414 return Collections.emptyList();
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()) {
424 final FileInputStream inputStream = new FileInputStream(cacheFile);
426 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
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);
435 catch (IOException | JsonSyntaxException e) {
436 LOG.error(e.getMessage());
439 StudyUtils.closeSilently(reader);
443 StudyUtils.closeSilently(inputStream);
446 catch (FileNotFoundException e) {
447 LOG.error(e.getMessage());
453 * Adds course from zip archive to courses
455 * @return added course name or null if course is invalid
458 public CourseInfo addLocalCourse(String zipFilePath) {
459 File file = new File(zipFilePath);
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
475 catch (IOException e) {
476 LOG.error("Failed to unzip course archive");
483 * Adds course to courses specified in params
486 * @param courseDir must be directory containing course file
487 * @return added course name or null if course is invalid
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");
497 File courseFile = courseFiles[0];
498 CourseInfo courseInfo = getCourseInfo(courseFile);
499 if (courseInfo != null) {
500 courses.add(courseInfo);
508 * Parses course json meta file and finds course name
510 * @return information about course or null if course file is invalid
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");
520 courseFile = courseFiles[0];
522 CourseInfo courseInfo = null;
523 BufferedReader reader = null;
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);
546 courseInfo.setAuthors(authors);
549 catch (Exception e) {
550 //error will be shown in UI
553 StudyUtils.closeSilently(reader);