Process unexpected status code in adaptive courses: try to login and post credentials...
[idea/community.git] / python / educational-core / student / src / com / jetbrains / edu / learning / stepic / EduStepicConnector.java
1 package com.jetbrains.edu.learning.stepic;
2
3 import com.google.gson.FieldNamingPolicy;
4 import com.google.gson.Gson;
5 import com.google.gson.GsonBuilder;
6 import com.google.gson.JsonObject;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.application.ModalityState;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.progress.ProgressIndicator;
11 import com.intellij.openapi.progress.ProgressManager;
12 import com.intellij.openapi.project.Project;
13 import com.intellij.openapi.ui.DialogWrapper;
14 import com.intellij.openapi.util.io.FileUtil;
15 import com.intellij.openapi.util.text.StringUtil;
16 import com.intellij.openapi.vfs.VfsUtil;
17 import com.intellij.openapi.vfs.VirtualFile;
18 import com.intellij.openapi.vfs.VirtualFileFilter;
19 import com.intellij.util.net.HttpConfigurable;
20 import com.intellij.util.net.ssl.CertificateManager;
21 import com.jetbrains.edu.learning.StudySerializationUtils;
22 import com.jetbrains.edu.learning.StudyTaskManager;
23 import com.jetbrains.edu.learning.core.EduNames;
24 import com.jetbrains.edu.learning.core.EduUtils;
25 import com.jetbrains.edu.learning.courseFormat.*;
26 import org.apache.commons.codec.binary.Base64;
27 import org.apache.http.*;
28 import org.apache.http.client.entity.UrlEncodedFormEntity;
29 import org.apache.http.client.methods.*;
30 import org.apache.http.client.utils.URIBuilder;
31 import org.apache.http.cookie.Cookie;
32 import org.apache.http.entity.ContentType;
33 import org.apache.http.entity.StringEntity;
34 import org.apache.http.impl.DefaultConnectionReuseStrategy;
35 import org.apache.http.impl.client.BasicCookieStore;
36 import org.apache.http.impl.client.CloseableHttpClient;
37 import org.apache.http.impl.client.HttpClientBuilder;
38 import org.apache.http.impl.client.HttpClients;
39 import org.apache.http.message.BasicHeader;
40 import org.apache.http.message.BasicNameValuePair;
41 import org.apache.http.util.EntityUtils;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44
45 import javax.net.ssl.SSLContext;
46 import javax.net.ssl.TrustManager;
47 import javax.net.ssl.X509TrustManager;
48 import java.io.IOException;
49 import java.net.InetSocketAddress;
50 import java.net.Proxy;
51 import java.net.URI;
52 import java.net.URISyntaxException;
53 import java.security.KeyManagementException;
54 import java.security.NoSuchAlgorithmException;
55 import java.security.SecureRandom;
56 import java.security.cert.X509Certificate;
57 import java.util.*;
58
59 import static com.jetbrains.edu.learning.stepic.EduStepicNames.CONTENT_TYPE_APPL_JSON;
60
61 public class EduStepicConnector {
62   private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName());
63   private static String ourCSRFToken = "";
64   private static CloseableHttpClient ourClient;
65
66   //this prefix indicates that course can be opened by educational plugin
67   public static final String PYCHARM_PREFIX = "pycharm";
68   private static BasicCookieStore ourCookieStore;
69
70   private EduStepicConnector() {
71   }
72
73   public static StepicUser login(@NotNull final String username, @NotNull final String password) {
74     initializeClient();
75     if (postCredentials(username, password)) {
76       final StepicWrappers.AuthorWrapper stepicUserWrapper = getCurrentUser();
77       if (stepicUserWrapper != null && stepicUserWrapper.users.size() == 1) {
78         return stepicUserWrapper.users.get(0);
79       }
80     }
81     return null;
82   }
83   
84   @NotNull
85   public static List<Integer> getEnrolledCoursesIds() {
86     try {
87       final URI enrolledCoursesUri = new URIBuilder(EduStepicNames.COURSES).addParameter("enrolled", "true").build();
88       final List<CourseInfo> courses = getFromStepic(enrolledCoursesUri.toString(), StepicWrappers.CoursesContainer.class).courses;
89       final ArrayList<Integer> ids = new ArrayList<>();
90       for (CourseInfo course : courses) {
91         ids.add(course.getId());
92       }
93       return ids;
94     }
95     catch (IOException e) {
96       LOG.warn(e.getMessage());
97     }
98     catch (URISyntaxException e) {
99       LOG.warn(e.getMessage());
100     }
101     return Collections.emptyList();
102   }
103
104   @Nullable
105   public static StepicWrappers.AuthorWrapper getCurrentUser() {
106     try {
107       return getFromStepic(EduStepicNames.CURRENT_USER, StepicWrappers.AuthorWrapper.class);
108     }
109     catch (IOException e) {
110       LOG.warn("Couldn't get author info");
111     }
112     return null;
113   }
114
115   public static boolean createUser(@NotNull final String user, @NotNull final String password) {
116     final HttpPost userRequest = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.USERS);
117     initializeClient();
118     setHeaders(userRequest, CONTENT_TYPE_APPL_JSON);
119     String requestBody = new Gson().toJson(new StepicWrappers.UserWrapper(user, password));
120     userRequest.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
121
122     try {
123       final CloseableHttpResponse response = ourClient.execute(userRequest);
124       final HttpEntity responseEntity = response.getEntity();
125       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
126       final StatusLine statusLine = response.getStatusLine();
127       if (statusLine.getStatusCode() != HttpStatus.SC_CREATED) {
128         LOG.error("Failed to create user " + responseString);
129         return false;
130       }
131     }
132     catch (IOException e) {
133       LOG.error(e.getMessage());
134     }
135     return true;
136   }
137
138   public static void initializeClient() {
139     if (ourClient == null) {
140       final HttpGet request = new HttpGet(EduStepicNames.STEPIC_URL);
141       setHeaders(request, CONTENT_TYPE_APPL_JSON);
142
143       HttpClientBuilder builder =
144         HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext()).setMaxConnPerRoute(100000).
145           setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE);
146
147       final HttpConfigurable proxyConfigurable = HttpConfigurable.getInstance();
148       final List<Proxy> proxies = proxyConfigurable.getOnlyBySettingsSelector().select(URI.create(EduStepicNames.STEPIC_URL));
149       final InetSocketAddress address = proxies.size() > 0 ? (InetSocketAddress)proxies.get(0).address() : null;
150       if (address != null) {
151         builder.setProxy(new HttpHost(address.getHostName(), address.getPort()));
152       }
153       ourCookieStore = new BasicCookieStore();
154
155       try {
156         // Create a trust manager that does not validate certificate for this connection
157         TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
158           public X509Certificate[] getAcceptedIssuers() {
159             return null;
160           }
161
162           public void checkClientTrusted(X509Certificate[] certs, String authType) {
163           }
164
165           public void checkServerTrusted(X509Certificate[] certs, String authType) {
166           }
167         }};
168         SSLContext sslContext = SSLContext.getInstance("TLS");
169         sslContext.init(null, trustAllCerts, new SecureRandom());
170         ourClient = builder.setDefaultCookieStore(ourCookieStore).setSslcontext(sslContext).build();
171
172         ourClient.execute(request);
173         saveCSRFToken();
174       }
175       catch (IOException e) {
176         LOG.error(e.getMessage());
177       }
178       catch (NoSuchAlgorithmException e) {
179         LOG.error(e.getMessage());
180       }
181       catch (KeyManagementException e) {
182         LOG.error(e.getMessage());
183       }
184     }
185   }
186
187   private static void saveCSRFToken() {
188     if (ourCookieStore == null) return;
189     final List<Cookie> cookies = ourCookieStore.getCookies();
190     for (Cookie cookie : cookies) {
191       if (cookie.getName().equals("csrftoken")) {
192         ourCSRFToken = cookie.getValue();
193       }
194     }
195   }
196
197   private static boolean postCredentials(String user, String password) {
198     String url = EduStepicNames.STEPIC_URL + EduStepicNames.LOGIN;
199     final HttpPost request = new HttpPost(url);
200     List <NameValuePair> nvps = new ArrayList <NameValuePair>();
201     nvps.add(new BasicNameValuePair("csrfmiddlewaretoken", ourCSRFToken));
202     nvps.add(new BasicNameValuePair("login", user));
203     nvps.add(new BasicNameValuePair("next", "/"));
204     nvps.add(new BasicNameValuePair("password", password));
205     nvps.add(new BasicNameValuePair("remember", "on"));
206
207     request.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8));
208
209     setHeaders(request, "application/x-www-form-urlencoded");
210
211     try {
212       final CloseableHttpResponse response = ourClient.execute(request);
213       saveCSRFToken();
214       final StatusLine line = response.getStatusLine();
215       if (line.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) {
216         final HttpEntity responseEntity = response.getEntity();
217         final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
218         LOG.warn("Failed to login: " + line.getStatusCode() + line.getReasonPhrase());
219         LOG.debug("Failed to login " + responseString);
220         ourClient = null;
221         return false;
222       }
223     }
224     catch (IOException e) {
225       LOG.warn(e.getMessage());
226       ourClient = null;
227       return false;
228     }
229     return true;
230   }
231
232   static <T> T getFromStepic(String link, final Class<T> container) throws IOException {
233     if (!link.startsWith("/")) link = "/" + link;
234     final HttpGet request = new HttpGet(EduStepicNames.STEPIC_API_URL + link);
235     if (ourClient == null) {
236       initializeClient();
237     }
238     setHeaders(request, CONTENT_TYPE_APPL_JSON);
239
240     final CloseableHttpResponse response = ourClient.execute(request);
241     final StatusLine statusLine = response.getStatusLine();
242     final HttpEntity responseEntity = response.getEntity();
243     final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
244     if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
245       throw new IOException("Stepic returned non 200 status code " + responseString);
246     }
247     Gson gson = new GsonBuilder().registerTypeAdapter(TaskFile.class, new StudySerializationUtils.Json.StepicTaskFileAdapter()).
248       registerTypeAdapter(AnswerPlaceholder.class, new StudySerializationUtils.Json.StepicAnswerPlaceholderAdapter()).
249       setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").
250       setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
251     return gson.fromJson(responseString, container);
252   }
253
254   @NotNull
255   public static CloseableHttpClient getHttpClient(@NotNull final Project project) {
256     if (ourClient == null) {
257       login(project);
258     }
259     return ourClient;
260   }
261
262   public static boolean enrollToCourse(final int courseId) {
263     HttpPost post = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ENROLLMENTS);
264     try {
265       final StepicWrappers.EnrollmentWrapper enrollment = new StepicWrappers.EnrollmentWrapper(String.valueOf(courseId));
266       post.setEntity(new StringEntity(new GsonBuilder().create().toJson(enrollment)));
267       setHeaders(post, CONTENT_TYPE_APPL_JSON);
268       if (ourClient == null) {
269         initializeClient();
270       }
271       CloseableHttpResponse response = ourClient.execute(post);
272       StatusLine line = response.getStatusLine();
273       return line.getStatusCode() == HttpStatus.SC_CREATED;
274     }
275     catch (IOException e) {
276       LOG.warn(e.getMessage());
277     }
278     return false;
279   }
280
281   @NotNull
282   public static List<CourseInfo> getCourses() {
283     try {
284       List<CourseInfo> result = new ArrayList<CourseInfo>();
285       int pageNumber = 1;
286       while (addCoursesFromStepic(result, pageNumber)) {
287         pageNumber += 1;
288       }
289       return result;
290     }
291     catch (IOException e) {
292       LOG.error("Cannot load course list " + e.getMessage());
293     }
294     return Collections.singletonList(CourseInfo.INVALID_COURSE);
295   }
296
297   public static Date getCourseUpdateDate(final int courseId) {
298     final String url = EduStepicNames.COURSES + "/" + courseId;
299     try {
300       final List<CourseInfo> courses = getFromStepic(url, StepicWrappers.CoursesContainer.class).courses;
301       if (!courses.isEmpty()) {
302         return courses.get(0).getUpdateDate();
303       }
304     }
305     catch (IOException e) {
306       LOG.warn("Could not retrieve course with id=" + courseId);
307     }
308
309     return null;
310   }
311
312   public static Date getLessonUpdateDate(final int lessonId) {
313     final String url = EduStepicNames.LESSONS + "/" + lessonId;
314     try {
315       List<Lesson> lessons = getFromStepic(url, StepicWrappers.LessonContainer.class).lessons;
316       if (!lessons.isEmpty()) {
317         return lessons.get(0).getUpdateDate();
318       }
319     }
320     catch (IOException e) {
321       LOG.warn("Could not retrieve course with id=" + lessonId);
322     }
323
324     return null;
325   }
326
327   public static Date getTaskUpdateDate(final int taskId) {
328     final String url = EduStepicNames.STEPS + "/" + String.valueOf(taskId);
329     try {
330       List<StepicWrappers.StepSource> steps = getFromStepic(url, StepicWrappers.StepContainer.class).steps;
331       if (!steps.isEmpty()) {
332         return steps.get(0).update_date;
333       }
334     }
335     catch (IOException e) {
336       LOG.warn("Could not retrieve course with id=" + taskId);
337     }
338
339     return null;
340   }
341
342   private static boolean addCoursesFromStepic(List<CourseInfo> result, int pageNumber) throws IOException {
343     final String url = pageNumber == 0 ? EduStepicNames.COURSES : EduStepicNames.COURSES_FROM_PAGE + String.valueOf(pageNumber);
344     final StepicWrappers.CoursesContainer coursesContainer = getFromStepic(url, StepicWrappers.CoursesContainer.class);
345     final List<CourseInfo> courseInfos = coursesContainer.courses;
346     for (CourseInfo info : courseInfos) {
347       final String courseType = info.getType();
348       if (!info.isAdaptive() && StringUtil.isEmptyOrSpaces(courseType)) continue;
349       final List<String> typeLanguage = StringUtil.split(courseType, " ");
350       // TODO: should adaptive course be of PyCharmType ?
351       if (info.isAdaptive() || (typeLanguage.size() == 2 && PYCHARM_PREFIX.equals(typeLanguage.get(0)))) {
352         for (Integer instructor : info.instructors) {
353           final StepicUser author = getFromStepic(EduStepicNames.USERS + "/" + String.valueOf(instructor), StepicWrappers.AuthorWrapper.class).users.get(0);
354           info.addAuthor(author);
355         }
356         
357         if (info.isAdaptive()) {
358           info.setDescription("This is a Stepic Adaptive course.\n\n" + info.getDescription());
359         }
360
361         String name = info.getName().replaceAll("[^a-zA-Z0-9\\s]", "");
362         info.setName(name.trim());
363         
364         result.add(info);
365       }
366     }
367     return coursesContainer.meta.containsKey("has_next") && coursesContainer.meta.get("has_next") == Boolean.TRUE;
368   }
369
370   public static Course getCourse(@NotNull final Project project, @NotNull final CourseInfo info) {
371     final Course course = new Course();
372     course.setAuthors(info.getAuthors());
373     course.setDescription(info.getDescription());
374     course.setAdaptive(info.isAdaptive());
375     course.setId(info.id);
376     course.setUpdateDate(info.getUpdateDate());
377     
378     if (!course.isAdaptive()) {
379       String courseType = info.getType();
380       course.setName(info.getName());
381       course.setLanguage(courseType.substring(PYCHARM_PREFIX.length() + 1));
382       try {
383         for (Integer section : info.sections) {
384           course.addLessons(getLessons(section));
385         }
386         return course;
387       }
388       catch (IOException e) {
389         LOG.error("IOException " + e.getMessage());
390       }
391     }
392     else {
393       final Lesson lesson = new Lesson();
394       course.setName(info.getName());
395       //TODO: more specific name?
396       lesson.setName("Adaptive");
397       course.addLesson(lesson);
398       final Task recommendation = EduAdaptiveStepicConnector.getNextRecommendation(project, course);
399       if (recommendation != null) {
400         lesson.addTask(recommendation);
401         return course;
402       }
403       else {
404         return null;
405       }
406     }
407     return null;
408   }
409
410   public static List<Lesson> getLessons(int sectionId) throws IOException {
411     final StepicWrappers.SectionContainer
412       sectionContainer = getFromStepic(EduStepicNames.SECTIONS + String.valueOf(sectionId), StepicWrappers.SectionContainer.class);
413     List<Integer> unitIds = sectionContainer.sections.get(0).units;
414     final List<Lesson> lessons = new ArrayList<Lesson>();
415     for (Integer unitId : unitIds) {
416       StepicWrappers.UnitContainer
417         unit = getFromStepic(EduStepicNames.UNITS + "/" + String.valueOf(unitId), StepicWrappers.UnitContainer.class);
418       int lessonID = unit.units.get(0).lesson;
419       StepicWrappers.LessonContainer
420         lessonContainer = getFromStepic(EduStepicNames.LESSONS + String.valueOf(lessonID), StepicWrappers.LessonContainer.class);
421       Lesson lesson = lessonContainer.lessons.get(0);
422       lesson.taskList = new ArrayList<Task>();
423       for (Integer s : lesson.steps) {
424         createTask(lesson, s);
425       }
426       if (!lesson.taskList.isEmpty())
427         lessons.add(lesson);
428     }
429
430     return lessons;
431   }
432
433   private static void createTask(Lesson lesson, Integer stepicId) throws IOException {
434     final StepicWrappers.StepSource step = getStep(stepicId);
435     final StepicWrappers.Step block = step.block;
436     if (!block.name.equals(PYCHARM_PREFIX)) return;
437     final Task task = new Task();
438     task.setStepicId(stepicId);
439     task.setUpdateDate(step.update_date);
440     task.setName(block.options != null ? block.options.title : PYCHARM_PREFIX);
441     task.setText(block.text);
442     for (StepicWrappers.TestFileWrapper wrapper : block.options.test) {
443       task.addTestsTexts(wrapper.name, wrapper.text);
444     }
445
446     task.taskFiles = new HashMap<String, TaskFile>();      // TODO: it looks like we don't need taskFiles as map anymore
447     if (block.options.files != null) {
448       for (TaskFile taskFile : block.options.files) {
449         task.taskFiles.put(taskFile.name, taskFile);
450       }
451     }
452     lesson.taskList.add(task);
453   }
454
455   public static StepicWrappers.StepSource getStep(Integer step) throws IOException {
456     return getFromStepic(EduStepicNames.STEPS + "/" + String.valueOf(step), StepicWrappers.StepContainer.class).steps.get(0);
457   }
458
459
460   public static boolean showLoginDialog() {
461     final boolean[] logged = {false};
462     ApplicationManager.getApplication().invokeAndWait(() -> {
463       final LoginDialog dialog = new LoginDialog();
464       dialog.show();
465       logged[0] = dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE;
466     }, ModalityState.defaultModalityState());
467     return logged[0];
468   }
469
470   public static void postAttempt(@NotNull final Task task, boolean passed, @Nullable String login, @Nullable String password) {
471     if (task.getStepicId() <= 0) {
472       return;
473     }
474     if (ourClient == null) {
475       if (StringUtil.isEmptyOrSpaces(login) || StringUtil.isEmptyOrSpaces(password)) {
476         return;
477       }
478       else {
479         if (login(login, password) == null) return;
480       }
481     }
482
483     final HttpPost attemptRequest = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ATTEMPTS);
484     setHeaders(attemptRequest, CONTENT_TYPE_APPL_JSON);
485     String attemptRequestBody = new Gson().toJson(new StepicWrappers.AttemptWrapper(task.getStepicId()));
486     attemptRequest.setEntity(new StringEntity(attemptRequestBody, ContentType.APPLICATION_JSON));
487
488     try {
489       final CloseableHttpResponse attemptResponse = ourClient.execute(attemptRequest);
490       final HttpEntity responseEntity = attemptResponse.getEntity();
491       final String attemptResponseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
492       final StatusLine statusLine = attemptResponse.getStatusLine();
493       if (statusLine.getStatusCode() != HttpStatus.SC_CREATED) {
494         LOG.error("Failed to make attempt " + attemptResponseString);
495       }
496       final StepicWrappers.AttemptWrapper.Attempt attempt = new Gson().fromJson(attemptResponseString, StepicWrappers.AttemptContainer.class).attempts.get(0);
497
498       final Map<String, TaskFile> taskFiles = task.getTaskFiles();
499       final ArrayList<StepicWrappers.SolutionFile> files = new ArrayList<StepicWrappers.SolutionFile>();
500       for (TaskFile fileEntry : taskFiles.values()) {
501         files.add(new StepicWrappers.SolutionFile(fileEntry.name, fileEntry.text));
502       }
503       postSubmission(passed, attempt, files);
504     }
505     catch (IOException e) {
506       LOG.error(e.getMessage());
507     }
508   }
509
510   private static void postSubmission(boolean passed, StepicWrappers.AttemptWrapper.Attempt attempt, ArrayList<StepicWrappers.SolutionFile> files) throws IOException {
511     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.SUBMISSIONS);
512     setHeaders(request, CONTENT_TYPE_APPL_JSON);
513
514     String requestBody = new Gson().toJson(new StepicWrappers.SubmissionWrapper(attempt.id, passed ? "1" : "0", files));
515     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
516     final CloseableHttpResponse response = ourClient.execute(request);
517     final HttpEntity responseEntity = response.getEntity();
518     final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
519     final StatusLine line = response.getStatusLine();
520     if (line.getStatusCode() != HttpStatus.SC_CREATED) {
521       LOG.error("Failed to make submission " + responseString);
522     }
523   }
524
525   public static void postCourseWithProgress(final Project project, @NotNull final Course course) {
526     postCourseWithProgress(project, course, false);
527   }
528
529   public static void postCourseWithProgress(final Project project, @NotNull final Course course, final boolean relogin) {
530     ProgressManager.getInstance().run(new com.intellij.openapi.progress.Task.Modal(project, "Uploading Course", true) {
531       @Override
532       public void run(@NotNull final ProgressIndicator indicator) {
533         postCourse(project, course, relogin, indicator);
534       }
535     });
536   }
537
538   private static void postCourse(final Project project, @NotNull Course course, boolean relogin, @NotNull final ProgressIndicator indicator) {
539     indicator.setText("Uploading course to " + EduStepicNames.STEPIC_URL);
540     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/courses");
541     if (ourClient == null || !relogin) {
542       if (!login(project)) return;
543     }
544     final StepicWrappers.AuthorWrapper authors = getCurrentUser();
545     if (authors != null) {
546       final List<StepicUser> courseAuthors = course.getAuthors();
547       for (int i = 0; i < courseAuthors.size(); i++) {
548         final StepicUser user = authors.users.get(i);
549         if (courseAuthors.size() > i) {
550           final StepicUser courseAuthor = courseAuthors.get(i);
551           user.setFirstName(courseAuthor.getFirstName());
552           user.setLastName(courseAuthor.getLastName());
553         }
554       }
555
556       course.setAuthors(authors.users);
557     }
558
559     setHeaders(request, CONTENT_TYPE_APPL_JSON);
560     String requestBody = new Gson().toJson(new StepicWrappers.CourseWrapper(course));
561     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
562
563     try {
564       final CloseableHttpResponse response = ourClient.execute(request);
565       final HttpEntity responseEntity = response.getEntity();
566       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
567       final StatusLine line = response.getStatusLine();
568       if (line.getStatusCode() != HttpStatus.SC_CREATED) {
569         if (!relogin) {
570           login(project);
571           postCourse(project, course, true, indicator);
572         }
573         LOG.error("Failed to push " + responseString);
574         return;
575       }
576       final CourseInfo postedCourse = new Gson().fromJson(responseString, StepicWrappers.CoursesContainer.class).courses.get(0);
577       course.setId(postedCourse.id);
578       final int sectionId = postModule(postedCourse.id, 1, String.valueOf(postedCourse.getName()));
579       int position = 1;
580       for (Lesson lesson : course.getLessons()) {
581         indicator.checkCanceled();
582         final int lessonId = postLesson(project, lesson, indicator);
583         postUnit(lessonId, position, sectionId);
584         position += 1;
585       }
586       ApplicationManager.getApplication().runReadAction(() -> postAdditionalFiles(project, postedCourse.id, indicator));
587     }
588     catch (IOException e) {
589       LOG.error(e.getMessage());
590     }
591   }
592
593   static boolean login(@NotNull final Project project) {
594     final StepicUser user = StudyTaskManager.getInstance(project).getUser();
595     final String login =  user.getEmail();
596     if (StringUtil.isEmptyOrSpaces(login)) {
597       return showLoginDialog();
598     }
599     else {
600       if (login(login, user.getPassword()) == null) {
601         return showLoginDialog();
602       }
603     }
604     return true;
605   }
606
607   private static void postAdditionalFiles(@NotNull final Project project, int id, ProgressIndicator indicator) {
608     final VirtualFile baseDir = project.getBaseDir();
609     final List<VirtualFile> files = VfsUtil.getChildren(baseDir, new VirtualFileFilter() {
610       @Override
611       public boolean accept(VirtualFile file) {
612         final String name = file.getName();
613         return !name.contains(EduNames.LESSON) && !name.equals(EduNames.COURSE_META_FILE) && !name.equals(EduNames.HINTS) &&
614           !"pyc".equals(file.getExtension()) && !file.isDirectory() && !name.equals(EduNames.TEST_HELPER) && !name.startsWith("");
615       }
616     });
617
618     if (!files.isEmpty()) {
619       final int sectionId = postModule(id, 2, EduNames.PYCHARM_ADDITIONAL);
620       final Lesson lesson = new Lesson();
621       lesson.setName(EduNames.PYCHARM_ADDITIONAL);
622       final Task task = new Task();
623       task.setLesson(lesson);
624       task.setName(EduNames.PYCHARM_ADDITIONAL);
625       task.setIndex(1);
626       task.setText(EduNames.PYCHARM_ADDITIONAL);
627       for (VirtualFile file : files) {
628         try {
629           if (file != null) {
630             if (EduUtils.isImage(file.getName())) {
631               task.addTestsTexts(file.getName(), Base64.encodeBase64URLSafeString(FileUtil.loadBytes(file.getInputStream())));
632             }
633             else {
634               task.addTestsTexts(file.getName(), FileUtil.loadTextAndClose(file.getInputStream()));
635             }
636           }
637         }
638         catch (IOException e) {
639           LOG.error("Can't find file " + file.getPath());
640         }
641       }
642       lesson.addTask(task);
643       lesson.setIndex(1);
644       final int lessonId = postLesson(project, lesson, indicator);
645       postUnit(lessonId, 1, sectionId);
646     }
647   }
648
649   public static void postUnit(int lessonId, int position, int sectionId) {
650     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.UNITS);
651     setHeaders(request, CONTENT_TYPE_APPL_JSON);
652     final StepicWrappers.UnitWrapper unitWrapper = new StepicWrappers.UnitWrapper();
653     unitWrapper.unit = new StepicWrappers.Unit();
654     unitWrapper.unit.lesson = lessonId;
655     unitWrapper.unit.position = position;
656     unitWrapper.unit.section = sectionId;
657
658     String requestBody = new Gson().toJson(unitWrapper);
659     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
660
661     try {
662       final CloseableHttpResponse response = ourClient.execute(request);
663       final HttpEntity responseEntity = response.getEntity();
664       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
665       final StatusLine line = response.getStatusLine();
666       if (line.getStatusCode() != HttpStatus.SC_CREATED) {
667         LOG.error("Failed to push " + responseString);
668       }
669     }
670     catch (IOException e) {
671       LOG.error(e.getMessage());
672     }
673   }
674
675   private static int postModule(int courseId, int position, @NotNull final String title) {
676     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/sections");
677     setHeaders(request, CONTENT_TYPE_APPL_JSON);
678     final StepicWrappers.Section section = new StepicWrappers.Section();
679     section.course = courseId;
680     section.title = title;
681     section.position = position;
682     final StepicWrappers.SectionWrapper sectionContainer = new StepicWrappers.SectionWrapper();
683     sectionContainer.section = section;
684     String requestBody = new Gson().toJson(sectionContainer);
685     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
686
687     try {
688       final CloseableHttpResponse response = ourClient.execute(request);
689       final HttpEntity responseEntity = response.getEntity();
690       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
691       final StatusLine line = response.getStatusLine();
692       if (line.getStatusCode() != HttpStatus.SC_CREATED) {
693         LOG.error("Failed to push " + responseString);
694         return -1;
695       }
696       final StepicWrappers.Section
697         postedSection = new Gson().fromJson(responseString, StepicWrappers.SectionContainer.class).sections.get(0);
698       return postedSection.id;
699     }
700     catch (IOException e) {
701       LOG.error(e.getMessage());
702     }
703     return -1;
704   }
705
706   public static int updateTask(@NotNull final Project project, @NotNull final Task task) {
707     final Lesson lesson = task.getLesson();
708     final int lessonId = lesson.getId();
709
710     if (ourClient == null) {
711       if (!login(project)) {
712         LOG.error("Failed to update task");
713         return -1;
714       }
715     }
716
717     final HttpPut request = new HttpPut(EduStepicNames.STEPIC_API_URL + "/step-sources/" + String.valueOf(task.getStepicId()));
718     setHeaders(request, CONTENT_TYPE_APPL_JSON);
719     final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().
720       registerTypeAdapter(AnswerPlaceholder.class, new StudySerializationUtils.Json.StepicAnswerPlaceholderAdapter()).create();
721     ApplicationManager.getApplication().invokeLater(() -> {
722       final String requestBody = gson.toJson(new StepicWrappers.StepSourceWrapper(project, task, lessonId));
723       request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
724
725       try {
726         final CloseableHttpResponse response = ourClient.execute(request);
727         final StatusLine line = response.getStatusLine();
728         if (line.getStatusCode() == HttpStatus.SC_FORBIDDEN) {
729           if (login(project)) {
730             updateTask(project, task);
731             return;
732           }
733         }
734         if (line.getStatusCode() != HttpStatus.SC_OK) {
735           final HttpEntity responseEntity = response.getEntity();
736           final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
737           LOG.error("Failed to push " + responseString);
738         }
739       }
740       catch (IOException e) {
741         LOG.error(e.getMessage());
742       }
743     });
744     return -1;
745   }
746
747   public static int updateLesson(@NotNull final Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) {
748     final HttpPut request = new HttpPut(EduStepicNames.STEPIC_API_URL + EduStepicNames.LESSONS + String.valueOf(lesson.getId()));
749     if (ourClient == null) {
750       if (!login(project)) {
751         LOG.error("Failed to push lesson");
752         return -1;
753       }
754     }
755
756     setHeaders(request, CONTENT_TYPE_APPL_JSON);
757     String requestBody = new Gson().toJson(new StepicWrappers.LessonWrapper(lesson));
758     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
759
760     try {
761       final CloseableHttpResponse response = ourClient.execute(request);
762       final HttpEntity responseEntity = response.getEntity();
763       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
764       final StatusLine line = response.getStatusLine();
765       if (line.getStatusCode() == HttpStatus.SC_FORBIDDEN) {
766         if (login(project)) {
767           return updateLesson(project, lesson, indicator);
768         }
769       }
770       if (line.getStatusCode() != HttpStatus.SC_OK) {
771         LOG.error("Failed to push " + responseString);
772         return -1;
773       }
774       final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0);
775       for (Integer step : postedLesson.steps) {
776         deleteTask(step);
777       }
778
779       for (Task task : lesson.getTaskList()) {
780         indicator.checkCanceled();
781         postTask(project, task, lesson.getId());
782       }
783       return lesson.getId();
784     }
785     catch (IOException e) {
786       LOG.error(e.getMessage());
787     }
788     return -1;
789   }
790
791   public static int postLesson(@NotNull final Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) {
792     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/lessons");
793     if (ourClient == null) {
794       login(project);
795     }
796
797     setHeaders(request, CONTENT_TYPE_APPL_JSON);
798     String requestBody = new Gson().toJson(new StepicWrappers.LessonWrapper(lesson));
799     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
800
801     try {
802       final CloseableHttpResponse response = ourClient.execute(request);
803       final HttpEntity responseEntity = response.getEntity();
804       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
805       final StatusLine line = response.getStatusLine();
806       if (line.getStatusCode() == HttpStatus.SC_FORBIDDEN) {
807         if (login(project)) {
808           return postLesson(project, lesson, indicator);
809         }
810       }
811       if (line.getStatusCode() != HttpStatus.SC_CREATED) {
812         LOG.error("Failed to push " + responseString);
813         return 0;
814       }
815       final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0);
816       lesson.setId(postedLesson.getId());
817       for (Task task : lesson.getTaskList()) {
818         indicator.checkCanceled();
819         postTask(project, task, postedLesson.getId());
820       }
821       return postedLesson.getId();
822     }
823     catch (IOException e) {
824       LOG.error(e.getMessage());
825     }
826     return -1;
827   }
828
829   public static void deleteTask(@NotNull final Integer task) {
830     final HttpDelete request = new HttpDelete(EduStepicNames.STEPIC_API_URL + EduStepicNames.STEP_SOURCES + task);
831     setHeaders(request, CONTENT_TYPE_APPL_JSON);
832     ApplicationManager.getApplication().invokeLater(() -> {
833       try {
834         final CloseableHttpResponse response = ourClient.execute(request);
835         final StatusLine line = response.getStatusLine();
836         if (line.getStatusCode() != HttpStatus.SC_NO_CONTENT) {
837           final HttpEntity responseEntity = response.getEntity();
838           final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
839           LOG.error("Failed to delete task " + responseString);
840         }
841       }
842       catch (IOException e) {
843         LOG.error(e.getMessage());
844       }
845     });
846   }
847
848   public static void postTask(final Project project, @NotNull final Task task, final int lessonId) {
849     if (ourClient == null) {
850       if (!login(project)) {
851         LOG.error("Failed to update task");
852       }
853     }
854
855     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/step-sources");
856     setHeaders(request, CONTENT_TYPE_APPL_JSON);
857     //TODO: register type adapter for task files here?
858     final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().
859       registerTypeAdapter(AnswerPlaceholder.class, new StudySerializationUtils.Json.StepicAnswerPlaceholderAdapter()).create();
860     ApplicationManager.getApplication().invokeLater(() -> {
861       final String requestBody = gson.toJson(new StepicWrappers.StepSourceWrapper(project, task, lessonId));
862       request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
863
864       try {
865         final CloseableHttpResponse response = ourClient.execute(request);
866         final StatusLine line = response.getStatusLine();
867         final HttpEntity responseEntity = response.getEntity();
868         final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
869         if (line.getStatusCode() == HttpStatus.SC_FORBIDDEN) {
870           if (login(project)) {
871             postTask(project, task, lessonId);
872             return;
873           }
874         }
875         if (line.getStatusCode() != HttpStatus.SC_CREATED) {
876           LOG.error("Failed to push " + responseString);
877           return;
878         }
879
880         final JsonObject postedTask = new Gson().fromJson(responseString, JsonObject.class);
881         final JsonObject stepSource = postedTask.getAsJsonArray("step-sources").get(0).getAsJsonObject();
882         task.setStepicId(stepSource.getAsJsonPrimitive("id").getAsInt());
883       }
884       catch (IOException e) {
885         LOG.error(e.getMessage());
886       }
887     });
888   }
889
890   static void setHeaders(@NotNull final HttpRequestBase request, String contentType) {
891     request.addHeader(new BasicHeader("referer", EduStepicNames.STEPIC_URL));
892     request.addHeader(new BasicHeader("X-CSRFToken", ourCSRFToken));
893     request.addHeader(new BasicHeader("content-type", contentType));
894   }
895 }