| package com.jetbrains.python.edu; |
| |
| import com.google.gson.*; |
| import com.google.gson.stream.JsonReader; |
| import com.intellij.facet.ui.FacetEditorValidator; |
| import com.intellij.facet.ui.FacetValidatorsManager; |
| import com.intellij.facet.ui.ValidationResult; |
| import com.intellij.lang.javascript.boilerplate.GithubDownloadUtil; |
| import com.intellij.openapi.application.PathManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.progress.ProcessCanceledException; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.vfs.VirtualFileManager; |
| import com.intellij.platform.DirectoryProjectGenerator; |
| import com.intellij.platform.templates.github.GeneratorException; |
| import com.intellij.platform.templates.github.ZipUtil; |
| import com.jetbrains.python.edu.course.Course; |
| import com.jetbrains.python.edu.course.CourseInfo; |
| import com.jetbrains.python.edu.ui.StudyNewProjectPanel; |
| import com.jetbrains.python.newProject.PythonProjectGenerator; |
| import icons.StudyIcons; |
| import org.jetbrains.annotations.Nls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.io.*; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| |
| public class StudyDirectoryProjectGenerator extends PythonProjectGenerator implements DirectoryProjectGenerator { |
| private static final Logger LOG = Logger.getInstance(StudyDirectoryProjectGenerator.class.getName()); |
| private static final String REPO_URL = "https://github.com/JetBrains/pycharm-courses/archive/master.zip"; |
| private static final String USER_NAME = "PyCharm"; |
| private static final String COURSE_META_FILE = "course.json"; |
| private static final String COURSE_NAME_ATTRIBUTE = "name"; |
| private static final Pattern CACHE_PATTERN = Pattern.compile("(name=(.*)) (path=(.*course.json)) (author=(.*)) (description=(.*))"); |
| private static final String REPOSITORY_NAME = "pycharm-courses"; |
| public static final String AUTHOR_ATTRIBUTE = "author"; |
| private final File myCoursesDir = new File(PathManager.getConfigPath(), "courses"); |
| private static final String CACHE_NAME = "courseNames.txt"; |
| private Map<CourseInfo, File> myCourses = new HashMap<CourseInfo, File>(); |
| private File mySelectedCourseFile; |
| private Project myProject; |
| public ValidationResult myValidationResult = new ValidationResult("selected course is not valid"); |
| |
| @Nls |
| @NotNull |
| @Override |
| public String getName() { |
| return "Educational"; |
| } |
| |
| |
| public void setCourses(Map<CourseInfo, File> courses) { |
| myCourses = courses; |
| } |
| |
| /** |
| * Finds selected course in courses by name. |
| * |
| * @param courseName name of selected course |
| */ |
| public void setSelectedCourse(@NotNull CourseInfo courseName) { |
| File courseFile = myCourses.get(courseName); |
| if (courseFile == null) { |
| LOG.error("invalid course in list"); |
| } |
| mySelectedCourseFile = courseFile; |
| } |
| |
| /** |
| * Adds course to courses specified in params |
| * |
| * @param courseDir must be directory containing course file |
| * @return added course name or null if course is invalid |
| */ |
| @Nullable |
| private static CourseInfo addCourse(Map<CourseInfo, File> courses, File courseDir) { |
| if (courseDir.isDirectory()) { |
| File[] courseFiles = courseDir.listFiles(new FilenameFilter() { |
| @Override |
| public boolean accept(File dir, String name) { |
| return name.equals(COURSE_META_FILE); |
| } |
| }); |
| if (courseFiles.length != 1) { |
| LOG.info("User tried to add course with more than one or without course files"); |
| return null; |
| } |
| File courseFile = courseFiles[0]; |
| CourseInfo courseInfo = getCourseInfo(courseFile); |
| if (courseInfo != null) { |
| courses.put(courseInfo, courseFile); |
| } |
| return courseInfo; |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Adds course from zip archive to courses |
| * |
| * @return added course name or null if course is invalid |
| */ |
| @Nullable |
| public CourseInfo addLocalCourse(String zipFilePath) { |
| File file = new File(zipFilePath); |
| try { |
| String fileName = file.getName(); |
| String unzippedName = fileName.substring(0, fileName.indexOf(".")); |
| File courseDir = new File(myCoursesDir, unzippedName); |
| ZipUtil.unzip(null, courseDir, file, null, null, true); |
| CourseInfo courseName = addCourse(myCourses, courseDir); |
| flushCache(); |
| return courseName; |
| } |
| catch (IOException e) { |
| LOG.error("Failed to unzip course archive"); |
| LOG.error(e); |
| } |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public Object showGenerationSettings(VirtualFile baseDir) throws ProcessCanceledException { |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public Icon getLogo() { |
| return StudyIcons.EducationalProjectType; |
| } |
| |
| |
| @Override |
| public void generateProject(@NotNull final Project project, @NotNull final VirtualFile baseDir, |
| @Nullable Object settings, @NotNull Module module) { |
| |
| myProject = project; |
| Reader reader = null; |
| try { |
| reader = new InputStreamReader(new FileInputStream(mySelectedCourseFile)); |
| Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); |
| Course course = gson.fromJson(reader, Course.class); |
| course.init(false); |
| course.create(baseDir, new File(mySelectedCourseFile.getParent())); |
| course.setResourcePath(mySelectedCourseFile.getAbsolutePath()); |
| VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); |
| StudyTaskManager.getInstance(project).setCourse(course); |
| } |
| catch (FileNotFoundException e) { |
| LOG.error(e); |
| } |
| finally { |
| StudyUtils.closeSilently(reader); |
| } |
| } |
| |
| /** |
| * Downloads courses from {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#REPO_URL} |
| * and unzips them into {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCoursesDir} |
| */ |
| |
| public void downloadAndUnzip(boolean needProgressBar) { |
| File outputFile = new File(PathManager.getConfigPath(), "courses.zip"); |
| try { |
| if (!needProgressBar) { |
| GithubDownloadUtil.downloadAtomically(null, REPO_URL, |
| outputFile, USER_NAME, REPOSITORY_NAME); |
| } |
| else { |
| GithubDownloadUtil.downloadContentToFileWithProgressSynchronously(myProject, REPO_URL, "downloading courses", outputFile, USER_NAME, |
| REPOSITORY_NAME, false); |
| } |
| if (outputFile.exists()) { |
| ZipUtil.unzip(null, myCoursesDir, outputFile, null, null, true); |
| if (!outputFile.delete()) { |
| LOG.error("Failed to delete", outputFile.getName()); |
| } |
| File[] files = myCoursesDir.listFiles(); |
| if (files != null) { |
| for (File file : files) { |
| String fileName = file.getName(); |
| if (StudyUtils.isZip(fileName)) { |
| ZipUtil.unzip(null, new File(myCoursesDir, fileName.substring(0, fileName.indexOf("."))), file, null, null, true); |
| if (!file.delete()) { |
| LOG.error("Failed to delete", fileName); |
| } |
| } |
| } |
| } |
| } else { |
| LOG.debug("failed to download course"); |
| } |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| catch (GeneratorException e) { |
| LOG.error(e); |
| } |
| } |
| |
| public Map<CourseInfo, File> getLoadedCourses() { |
| return myCourses; |
| } |
| |
| /** |
| * Parses courses located in {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCoursesDir} |
| * to {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCourses} |
| * |
| * @return map with course names and course files location |
| */ |
| public Map<CourseInfo, File> loadCourses() { |
| Map<CourseInfo, File> courses = new HashMap<CourseInfo, File>(); |
| if (myCoursesDir.exists()) { |
| File[] courseDirs = myCoursesDir.listFiles(new FileFilter() { |
| @Override |
| public boolean accept(File pathname) { |
| return pathname.isDirectory(); |
| } |
| }); |
| for (File courseDir : courseDirs) { |
| addCourse(courses, courseDir); |
| } |
| } |
| return courses; |
| } |
| |
| /** |
| * Parses course json meta file and finds course name |
| * |
| * @return information about course or null if course file is invalid |
| */ |
| @Nullable |
| private static CourseInfo getCourseInfo(File courseFile) { |
| CourseInfo courseInfo = null; |
| BufferedReader reader = null; |
| try { |
| if (courseFile.getName().equals(COURSE_META_FILE)) { |
| reader = new BufferedReader(new InputStreamReader(new FileInputStream(courseFile))); |
| JsonReader r = new JsonReader(reader); |
| JsonParser parser = new JsonParser(); |
| JsonElement el = parser.parse(r); |
| String courseName = el.getAsJsonObject().get(COURSE_NAME_ATTRIBUTE).getAsString(); |
| String courseAuthor = el.getAsJsonObject().get(AUTHOR_ATTRIBUTE).getAsString(); |
| String courseDescription = el.getAsJsonObject().get("description").getAsString(); |
| courseInfo = new CourseInfo(courseName, courseAuthor, courseDescription); |
| } |
| } |
| catch (Exception e) { |
| //error will be shown in UI |
| } |
| finally { |
| StudyUtils.closeSilently(reader); |
| } |
| return courseInfo; |
| } |
| |
| @NotNull |
| @Override |
| public ValidationResult validate(@NotNull String s) { |
| return myValidationResult; |
| } |
| |
| public void setValidationResult(ValidationResult validationResult) { |
| myValidationResult = validationResult; |
| } |
| |
| /** |
| * @return courses from memory or from cash file or parses course directory |
| */ |
| public Map<CourseInfo, File> getCourses() { |
| if (!myCourses.isEmpty()) { |
| return myCourses; |
| } |
| if (myCoursesDir.exists()) { |
| File cacheFile = new File(myCoursesDir, CACHE_NAME); |
| if (cacheFile.exists()) { |
| myCourses = getCoursesFromCache(cacheFile); |
| if (!myCourses.isEmpty()) { |
| return myCourses; |
| } |
| } |
| myCourses = loadCourses(); |
| if (!myCourses.isEmpty()) { |
| return myCourses; |
| } |
| } |
| downloadAndUnzip(false); |
| myCourses = loadCourses(); |
| flushCache(); |
| return myCourses; |
| } |
| |
| /** |
| * Writes courses to cash file {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#CACHE_NAME} |
| */ |
| @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") |
| public void flushCache() { |
| File cashFile = new File(myCoursesDir, CACHE_NAME); |
| PrintWriter writer = null; |
| try { |
| if (!cashFile.exists()) { |
| final boolean created = cashFile.createNewFile(); |
| if (!created) { |
| LOG.error("Cannot flush courses cache. Can't create " + CACHE_NAME + " file"); |
| return; |
| } |
| } |
| writer = new PrintWriter(cashFile); |
| for (Map.Entry<CourseInfo, File> course : myCourses.entrySet()) { |
| CourseInfo courseInfo = course.getKey(); |
| String line = String |
| .format("name=%s path=%s author=%s description=%s", courseInfo.getName(), course.getValue(), courseInfo.getAuthor(), |
| courseInfo.getDescription()); |
| writer.println(line); |
| } |
| } |
| catch (FileNotFoundException e) { |
| LOG.error(e); |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| finally { |
| StudyUtils.closeSilently(writer); |
| } |
| } |
| |
| /** |
| * Loads courses from {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#CACHE_NAME} file |
| * |
| * @return map of course names and course files |
| */ |
| @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") |
| private static Map<CourseInfo, File> getCoursesFromCache(File cashFile) { |
| Map<CourseInfo, File> coursesFromCash = new HashMap<CourseInfo, File>(); |
| BufferedReader reader = null; |
| try { |
| reader = new BufferedReader(new InputStreamReader(new FileInputStream(cashFile))); |
| String line; |
| |
| while ((line = reader.readLine()) != null) { |
| Matcher matcher = CACHE_PATTERN.matcher(line); |
| if (matcher.matches()) { |
| String courseName = matcher.group(2); |
| File file = new File(matcher.group(4)); |
| String author = matcher.group(6); |
| String description = matcher.group(8); |
| CourseInfo courseInfo = new CourseInfo(courseName, author, description); |
| if (file.exists()) { |
| coursesFromCash.put(courseInfo, file); |
| } |
| } |
| } |
| } |
| catch (FileNotFoundException e) { |
| LOG.error(e); |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| finally { |
| StudyUtils.closeSilently(reader); |
| } |
| return coursesFromCash; |
| } |
| |
| @Nullable |
| @Override |
| public JPanel extendBasePanel() throws ProcessCanceledException { |
| StudyNewProjectPanel settingsPanel = new StudyNewProjectPanel(this); |
| settingsPanel.registerValidators(new FacetValidatorsManager() { |
| public void registerValidator(FacetEditorValidator validator, JComponent... componentsToWatch) { |
| throw new UnsupportedOperationException(); |
| } |
| public void validate() { |
| fireStateChanged(); |
| } |
| }); |
| return settingsPanel.getContentPanel(); |
| } |
| } |