[from now] 2012/06/01 17:54:28

diff --git a/pom.xml b/pom.xml
index 3c47fb8..2e4f625 100644
--- a/pom.xml
+++ b/pom.xml
@@ -162,6 +162,11 @@
       <artifactId>mockito-core</artifactId>
       <version>1.9.0</version>
     </dependency>
+    <dependency>
+      <groupId>org.freemarker</groupId>
+      <artifactId>freemarker-gae</artifactId>
+      <version>2.3.19</version>
+    </dependency>
   </dependencies>
   <build>
     <finalName>slim3-maven</finalName>
[from now] 2012/06/01 19:18:04

diff --git a/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java b/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
index 2d6a92c..a26fd5d 100644
--- a/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
+++ b/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
@@ -425,8 +425,10 @@ public class CalendarService {
                 myCalendarLog.setNicoliveKeys(new ArrayList<Key>());
             }
             List<Key> importedNicoliveKeys = new LinkedList<Key>();
+            List<Key> allNicoliveKeys = new LinkedList<Key>();
             List<Nicolive> nicolives = query.asList();
             for (Nicolive nicolive : nicolives) {
+                allNicoliveKeys.add(nicolive.getKey());
                 if (!myCalendarLog
                     .getNicoliveKeys()
                     .contains(nicolive.getKey())) {
@@ -470,27 +472,20 @@ public class CalendarService {
                                 .insert(myCalendar.getCalendarId(), event)
                                 .execute();
                         } catch (IOException e) {
-                            putMyCalendarLog(myCalendarLog, importedNicoliveKeys);
+                            // 途中でインポートに失敗した場合は、成功した分までMyCalendarLogに記録して、次回のインポートの対象から外すようにする。
+                            if (0 < importedNicoliveKeys.size()) {
+                                myCalendarLog.getNicoliveKeys().addAll(
+                                    importedNicoliveKeys);
+                                Datastore.putAsync(myCalendarLog);
+                            }
                             throw e;
                         }
                     }
-
                     importedNicoliveKeys.add(nicolive.getKey());
                 }
             }
-            putMyCalendarLog(myCalendarLog, importedNicoliveKeys);
-        }
-    }
-
-    /**
-     * @param myCalendarLog
-     * @param importedNicoliveKeys
-     */
-    private void putMyCalendarLog(MyCalendarLog myCalendarLog,
-            List<Key> importedNicoliveKeys) {
-        if (0 < importedNicoliveKeys.size()) {
-            // インポートしたNicoliveをmyCalendarLogに登録して、二度とインポートしないようにする。
-            myCalendarLog.getNicoliveKeys().addAll(importedNicoliveKeys);
+            // 今回インポート対象にした範囲のNicoliveをMyCalendarLogに記録して、次回のインポート対象から外すようにする。
+            myCalendarLog.setNicoliveKeys(allNicoliveKeys);
             Datastore.putAsync(myCalendarLog);
         }
     }
[from now] 2012/06/01 22:29:58

diff --git a/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java b/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
index a26fd5d..e8a3393 100644
--- a/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
+++ b/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
@@ -1,13 +1,17 @@
 package org.ryu22e.nico2cal.service;

+import java.io.File;
 import java.io.IOException;
+import java.io.StringWriter;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.logging.Logger;
@@ -58,9 +62,16 @@ import com.google.api.services.calendar.model.Event;
 import com.google.api.services.calendar.model.EventDateTime;
 import com.google.appengine.api.datastore.Key;
 import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.appengine.api.mail.MailService.Message;
+import com.google.appengine.api.mail.MailServiceFactory;
 import com.google.appengine.api.users.User;
 import com.google.appengine.api.users.UserServiceFactory;

+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
 /**
  * iCalendarファイルを操作するサービスクラス。
  * @author ryu22e
@@ -159,6 +170,43 @@ public class CalendarService {
     }

     /**
+     * Google Calendarインポートに失敗した旨をユーザーにメールで通知する。
+     * @param myCalendar {@link MyCalendar}
+     */
+    protected void sendImportErrorMail(MyCalendar myCalendar) {
+        Message message = new Message();
+        message.setTo(myCalendar.getUser().getEmail());
+        message.setSubject("Google Calendarへのインポートに失敗しました");
+        message.setSender("ryu22e@gmail.com");
+        Configuration config = new Configuration();
+        config.setObjectWrapper(new DefaultObjectWrapper());
+
+        try {
+            config.setDirectoryForTemplateLoading(new File(
+                "src/main/resources/templates/mail/"));
+            Template template = config.getTemplate("importError.flt");
+            StringWriter writer = new StringWriter();
+            Map<String, String> root = new HashMap<String, String>();
+            root.put("userId", myCalendar.getUser().getUserId());
+            template.process(root, writer);
+            message.setTextBody(new String(writer.getBuffer()));
+        } catch (IOException e) {
+            LOGGER.warning(e.getMessage());
+            return;
+        } catch (TemplateException e) {
+            LOGGER.warning(e.getMessage());
+            return;
+        }
+        com.google.appengine.api.mail.MailService mailService =
+                MailServiceFactory.getMailService();
+        try {
+            mailService.send(message);
+        } catch (IOException e) {
+            LOGGER.warning(e.getMessage());
+        }
+    }
+
+    /**
      * Datastoreに登録されたRSSフィードをiCalendar形式のデータに変換する。
      * @param condition 検索条件
      * @return iCalendar形式のデータ
@@ -478,6 +526,11 @@ public class CalendarService {
                                     importedNicoliveKeys);
                                 Datastore.putAsync(myCalendarLog);
                             }
+                            if (myCalendar.isNotifyErrorMail()) {
+                                sendImportErrorMail(myCalendar);
+                            }
+                            myCalendar.setDisabled(true);
+                            Datastore.putAsync(myCalendar);
                             throw e;
                         }
                     }
diff --git a/src/main/resources/templates/mail/importError.flt b/src/main/resources/templates/mail/importError.flt
new file mode 100644
index 0000000..c39ee33
--- /dev/null
+++ b/src/main/resources/templates/mail/importError.flt
@@ -0,0 +1,27 @@
+【注意】
+このメールはnico2icalから自動送信されています。
+
+${userId!} さん
+Google Calendarへのニコニコ生放送放送予定日のインポートに失敗しました。
+以下の原因で失敗した可能性があります。
+（１）${userId!} さんのGoogleアカウントの「Google アカウント に許可されたアクセス」からnico2icalからGoogle Calendarへのアクセス許可が取り消しになっている。
+（２）Google Calendar APIの許可有効期限が終了した。
+（３）Google Calendar APIの１日の呼び出し上限をオーバーした。
+（４）nico2icalの動作環境であるGoogle App Engineに何らかの障害が発生した。
+原因が解決するまで、インポートは一時中断されます。
+
+お手数ですが、以下の手順でGoogle Calendarとの連携を再設定してください。
+（１）（２）が原因の場合は、これでインポートが再開されます。
+【手順１】
+nico2icalの「マイカレンダー」にアクセスする。
+https://nico2ical.appspot.com/myCalendar/
+【手順２】
+「連携のON/OFF」タブをクリックし、「Google Calendar連携OFF」ボタンでGoogle Calendarとの連携を一旦解除する。
+【手順３】
+「Google Calendar連携ON」ボタンでGoogle Calendarとの連携を再び開始する。
+
+（３）（４）が原因の場合は、上記手順ではインポートは再開されません。
+管理人の方で、下記ブログにアナウンスを載せますので、しばらくお待ちください。
+http://blog.livedoor.jp/ryu22e/archives/cat_60250685.html
+
+<#include "/signature.flt">
\ No newline at end of file
diff --git a/src/main/resources/templates/mail/signature.flt b/src/main/resources/templates/mail/signature.flt
new file mode 100644
index 0000000..b7285c4
--- /dev/null
+++ b/src/main/resources/templates/mail/signature.flt
@@ -0,0 +1,3 @@
+--
+nico2ical
+http://nico2ical.appspot.com/
\ No newline at end of file
diff --git a/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java b/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
index da1539f..9ce7bf7 100644
--- a/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
+++ b/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
@@ -40,6 +40,7 @@ import com.google.appengine.api.NamespaceManager;
 import com.google.appengine.api.datastore.Key;
 import com.google.appengine.api.datastore.Link;
 import com.google.appengine.api.datastore.Text;
+import com.google.appengine.api.mail.MailServicePb.MailMessage;
 import com.google.appengine.api.users.User;
 import com.google.appengine.api.users.UserServiceFactory;

@@ -655,4 +656,28 @@ public final class CalendarServiceTest extends AppEngineTestCase {
         assertThat(myCalendarLog, is(notNullValue()));
         assertThat(myCalendarLog.getNicoliveKeys(), is(testDataKeys));
     }
+
+    /**
+     * @throws Exception
+     */
+    @Test
+    public void GoogleCalendarインポートに失敗した旨をユーザーにメールで通知する() throws Exception {
+        assertThat(service, is(notNullValue()));
+
+        MyCalendar myCalendar = new MyCalendar();
+        myCalendar.setCalendarId("calendar1");
+        myCalendar.setUser(new User("dummy@gmail.com", "example.com", "dummy"));
+
+        service.sendImportErrorMail(myCalendar);
+        assertThat(tester.mailMessages.size(), is(1));
+        MailMessage mailMessage = tester.mailMessages.get(0);
+        assertThat(mailMessage.tos().size(), is(1));
+        assertThat(mailMessage.tos().get(0), is("dummy@gmail.com"));
+        assertThat(mailMessage.getSender(), is("ryu22e@gmail.com"));
+        assertThat(
+            mailMessage.getSubject(),
+            is("Google Calendarへのインポートに失敗しました"));
+        assertThat(mailMessage.getTextBody(), is(notNullValue()));
+        assertThat(mailMessage.getTextBody(), not(""));
+    }
 }