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