[from now] 2012/05/23 17:12:16

diff --git a/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/DisConnectController.java b/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/DisConnectController.java
new file mode 100644
index 0000000..4edac7a
--- /dev/null
+++ b/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/DisConnectController.java
@@ -0,0 +1,44 @@
+package org.ryu22e.nico2cal.controller.myCalendar;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.ryu22e.nico2cal.service.CalendarService;
+import org.slim3.controller.Controller;
+import org.slim3.controller.Navigation;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonWriter;
+
+/**
+ * Google Calendarとの連携をやめるコントローラー。
+ * @author ryu22e
+ *
+ */
+public final class DisConnectController extends Controller {
+
+    /**
+     *
+     */
+    private CalendarService calendarService = new CalendarService();
+
+    /*
+     * (non-Javadoc) {@inheritDoc}
+     */
+    @Override
+    public Navigation run() throws Exception {
+        calendarService.disConnectMyCalendar();
+
+        Map<String, String> map = new HashMap<String, String>();
+        map.put("status", "ok");
+
+        JsonWriter writer = new JsonWriter(response.getWriter());
+        Type type = new TypeToken<Map<String, Object>>() {
+        }.getType();
+        new Gson().toJson(map, type, writer);
+
+        return null;
+    }
+}
diff --git a/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java b/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
index 20d5f47..2ba59bc 100644
--- a/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
+++ b/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
@@ -297,4 +297,20 @@ public class CalendarService {
         MyCalendarMeta mc = MyCalendarMeta.get();
         return Datastore.query(mc).filter(mc.user.equal(user)).asSingle();
     }
+
+    /**
+     * Google Calendar連携をやめる。
+     */
+    public void disConnectMyCalendar() {
+        User user = UserServiceFactory.getUserService().getCurrentUser();
+        if (user != null) {
+            // Access tokenが格納されているAppEngineCredentialStoreを削除する。
+            new AppEngineCredentialStore().delete(user.getUserId(), null);
+            // 連携対象のCalendar Idが格納されているMyCalendarを削除する。
+            MyCalendarMeta mc = MyCalendarMeta.get();
+            List<Key> keys =
+                    Datastore.query(mc).filter(mc.user.equal(user)).asKeyList();
+            Datastore.delete(keys);
+        }
+    }
 }
diff --git a/src/test/java/org/ryu22e/nico2cal/controller/myCalendar/DisConnectControllerTest.java b/src/test/java/org/ryu22e/nico2cal/controller/myCalendar/DisConnectControllerTest.java
new file mode 100644
index 0000000..afbff76
--- /dev/null
+++ b/src/test/java/org/ryu22e/nico2cal/controller/myCalendar/DisConnectControllerTest.java
@@ -0,0 +1,100 @@
+package org.ryu22e.nico2cal.controller.myCalendar;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+import org.ryu22e.nico2cal.model.MyCalendar;
+import org.slim3.datastore.Datastore;
+import org.slim3.tester.ControllerTestCase;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.extensions.appengine.auth.oauth2.AppEngineCredentialStore;
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * @author ryu22e
+ *
+ */
+public final class DisConnectControllerTest extends ControllerTestCase {
+
+    /**
+     * @throws Exception
+     */
+    @Test
+    public void GoogleCalendar連携をやめる() throws Exception {
+        tester.environment.setEmail("dummy@gmail.com");
+        Map<String, Object> envAttributes = new HashMap<String, Object>();
+        envAttributes.put(
+            "com.google.appengine.api.users.UserService.user_id_key",
+            "42");
+        tester.environment.setAttributes(envAttributes);
+        // テストデータの登録。
+        MyCalendar testData = new MyCalendar();
+        testData.setCalendarId("test1");
+        testData.setUser(UserServiceFactory.getUserService().getCurrentUser());
+        Key testDataKey = Datastore.put(testData);
+        Credential credential = new GoogleCredential();
+        new AppEngineCredentialStore().store("42", credential);
+        assertThat(
+            new AppEngineCredentialStore().load("42", credential),
+            is(true));
+
+        tester.start("/myCalendar/disConnect");
+        DisConnectController controller = tester.getController();
+        assertThat(controller, is(notNullValue()));
+        assertThat(tester.isRedirect(), is(false));
+        assertThat(tester.response.getStatus(), is(200));
+        assertThat(tester.getDestinationPath(), is(nullValue()));
+        String output = tester.response.getOutputAsString();
+        assertThat(output, is(notNullValue()));
+        Gson gson = new Gson();
+        Type type = new TypeToken<Map<String, Object>>() {
+        }.getType();
+        Map<String, String> json = gson.fromJson(output, type);
+        assertThat(json, is(notNullValue()));
+        assertThat(json.get("status"), is("ok"));
+
+        int count = Datastore.query(MyCalendar.class, testDataKey).count();
+        assertThat(count, is(0));
+        assertThat(
+            new AppEngineCredentialStore().load("42", credential),
+            is(false));
+    }
+
+    /**
+     * @throws Exception
+     */
+    @Test
+    public void GoogleCalendar連携をやめる_元々連携していない場合() throws Exception {
+        tester.environment.setEmail("dummy@gmail.com");
+        Map<String, Object> envAttributes = new HashMap<String, Object>();
+        envAttributes.put(
+            "com.google.appengine.api.users.UserService.user_id_key",
+            "42");
+        tester.environment.setAttributes(envAttributes);
+
+        tester.start("/myCalendar/disConnect");
+        DisConnectController controller = tester.getController();
+        assertThat(controller, is(notNullValue()));
+        assertThat(tester.isRedirect(), is(false));
+        assertThat(tester.response.getStatus(), is(200));
+        assertThat(tester.getDestinationPath(), is(nullValue()));
+        String output = tester.response.getOutputAsString();
+        assertThat(output, is(notNullValue()));
+        Gson gson = new Gson();
+        Type type = new TypeToken<Map<String, Object>>() {
+        }.getType();
+        Map<String, String> json = gson.fromJson(output, type);
+        assertThat(json, is(notNullValue()));
+        assertThat(json.get("status"), is("ok"));
+    }
+}
diff --git a/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java b/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
index f1335d6..3d613bd 100644
--- a/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
+++ b/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
@@ -25,6 +25,9 @@ import org.slim3.tester.AppEngineTestCase;
 import org.slim3.util.DateUtil;

 import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.extensions.appengine.auth.oauth2.AppEngineCredentialStore;
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
 import com.google.api.client.http.javanet.NetHttpTransport;
 import com.google.api.client.json.jackson.JacksonFactory;
 import com.google.api.services.calendar.model.CalendarList;
@@ -403,4 +406,82 @@ public final class CalendarServiceTest extends AppEngineTestCase {
         MyCalendar result = service.getMyCalendar();
         assertThat(result, is(nullValue()));
     }
+
+    /**
+     * @throws Exception
+     */
+    @Test
+    public void GoogleCalendar連携をやめる_ログインしている場合() throws Exception {
+        assertThat(service, is(notNullValue()));
+
+        tester.environment.setEmail("dummy@gmail.com");
+        Map<String, Object> envAttributes = new HashMap<String, Object>();
+        envAttributes.put(
+            "com.google.appengine.api.users.UserService.user_id_key",
+            "42");
+        tester.environment.setAttributes(envAttributes);
+        // テストデータの登録。
+        MyCalendar testData = new MyCalendar();
+        testData.setCalendarId("test1");
+        testData.setUser(UserServiceFactory.getUserService().getCurrentUser());
+        Key testDataKey = Datastore.put(testData);
+        Credential credential = new GoogleCredential();
+        new AppEngineCredentialStore().store("42", credential);
+        assertThat(
+            new AppEngineCredentialStore().load("42", credential),
+            is(true));
+
+        service.disConnectMyCalendar();
+
+        int count = Datastore.query(MyCalendar.class, testDataKey).count();
+        assertThat(count, is(0));
+        assertThat(
+            new AppEngineCredentialStore().load("42", credential),
+            is(false));
+    }
+
+    /**
+     * @throws Exception
+     */
+    @Test
+    public void GoogleCalendar連携をやめる_元々連携していない場合() throws Exception {
+        assertThat(service, is(notNullValue()));
+
+        tester.environment.setEmail("dummy@gmail.com");
+        Map<String, Object> envAttributes = new HashMap<String, Object>();
+        envAttributes.put(
+            "com.google.appengine.api.users.UserService.user_id_key",
+            "42");
+        tester.environment.setAttributes(envAttributes);
+
+        // 例外は発生しない。
+        service.disConnectMyCalendar();
+    }
+
+    /**
+     * @throws Exception
+     */
+    @Test
+    public void GoogleCalendar連携をやめる_ログインしていない場合() throws Exception {
+        assertThat(service, is(notNullValue()));
+
+        // テストデータの登録。
+        MyCalendar testData = new MyCalendar();
+        testData.setCalendarId("test1");
+        testData.setUser(UserServiceFactory.getUserService().getCurrentUser());
+        Key testDataKey = Datastore.put(testData);
+        Credential credential = new GoogleCredential();
+        new AppEngineCredentialStore().store("42", credential);
+        assertThat(
+            new AppEngineCredentialStore().load("42", credential),
+            is(true));
+
+        service.disConnectMyCalendar();
+
+        int count = Datastore.query(MyCalendar.class, testDataKey).count();
+        assertThat(count, is(1));
+        assertThat(
+            new AppEngineCredentialStore().load("42", credential),
+            is(true));
+    }
 }
diff --git a/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml b/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
index be66f30..e1fcf26 100644
--- a/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
+++ b/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
@@ -1,4 +1,4 @@
-<!-- Indices written at Tue, 22 May 2012 23:43:09 JST -->
+<!-- Indices written at Wed, 23 May 2012 17:09:11 JST -->

 <datastore-indexes/>

diff --git a/war/WEB-INF/web.xml b/war/WEB-INF/web.xml
index a347db6..0cc61d1 100644
--- a/war/WEB-INF/web.xml
+++ b/war/WEB-INF/web.xml
@@ -66,6 +66,7 @@
     <filter-mapping>
         <filter-name>CsrfFilter</filter-name>
         <url-pattern>/myCalendar/save</url-pattern>
+        <url-pattern>/myCalendar/disConnect</url-pattern>
         <dispatcher>REQUEST</dispatcher>
     </filter-mapping>
     <filter-mapping>
diff --git a/war/js/nico2ical.js b/war/js/nico2ical.js
index ff40256..b545bc2 100644
--- a/war/js/nico2ical.js
+++ b/war/js/nico2ical.js
@@ -62,6 +62,30 @@ $(function () {
         this.isEnableButton = function() {
             return 0 < this.selectedValue().length;
         }.bind(this);
+        this.disConnectCalendar = function() {
+            if (confirm('実行すると連携が解除されます。よろしいですか？')) {
+                $('#disconnect-calendar')
+                    .removeClass('btn-danger')
+                    .button('loading');
+                $('input[name=calendarSummaries]').attr('disabled', 'disabled');
+                $.ajax(
+                        '/myCalendar/disConnect',
+                        {
+                            type: 'POST',
+                            dataType: 'json',
+                            data: {
+                                'csrftoken': $.cookie('csrftoken')
+                                }
+                        }
+                ).success(function(data) {
+                    $('#disconnect-calendar').hide();
+                    $('#connect-calendar').removeClass('hidden');
+                }).error(function(xhr, textStatus, errorThrown) {
+                    $('#disconnect-calendar').addClass('btn-danger').button('error');
+                }).complete(function() {
+                });
+            }
+        }.bind(this);
     };
     var selectedValue = $('input[name=calendarSummaries]:checked').val() || '';
     ko.applyBindings(new CalendarSummaries(selectedValue));
diff --git a/war/myCalendar/index.jsp b/war/myCalendar/index.jsp
index 6546b54..84b1c93 100644
--- a/war/myCalendar/index.jsp
+++ b/war/myCalendar/index.jsp
@@ -61,11 +61,20 @@
             <div class="control-group">
               <div class="controls">
                 <c:if test="${storedCredential}">
-
-                </c:if>
-                <c:if test="${!storedCredential}">
-                  <a class="btn btn-primary" href="${f:url('connect')}">Google Calendarと連携する</a>
+                  <a id="disconnect-calendar"
+                     href="#"
+                     class="btn"
+                     data-loading-text="連携解除中..."
+                     data-error-text="連携解除に失敗しました"
+                     data-bind="click: disConnectCalendar">
+                     Google Calendar連携OFF
+                  </a>
                 </c:if>
+                <a id="connect-calendar"
+                   href="${f:url('connect')}"
+                   class="btn btn-primary<c:if test="${storedCredential}"> hidden</c:if>">
+                   Google Calendar連携ON
+                </a>
               </div>
             </div><!-- /.control-group -->
           </form>
[from now] 2012/05/23 18:14:22

diff --git a/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/IndexController.java b/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/IndexController.java
index fb7e755..b06c8df 100644
--- a/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/IndexController.java
+++ b/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/IndexController.java
@@ -1,6 +1,8 @@
 package org.ryu22e.nico2cal.controller.myCalendar;

+import java.io.IOException;
 import java.util.Arrays;
+import java.util.logging.Logger;

 import org.ryu22e.nico2cal.model.MyCalendar;
 import org.ryu22e.nico2cal.service.CalendarService;
@@ -21,6 +23,12 @@ public final class IndexController extends Controller {
     /**
      *
      */
+    public static final Logger LOGGER = Logger.getLogger(IndexController.class
+        .getName());
+
+    /**
+     *
+     */
     private CalendarService calendarService = new CalendarService();

     /*
@@ -31,7 +39,12 @@ public final class IndexController extends Controller {

         boolean storedCredential;
         if (AppEngineUtil.isProduction()) {
-            CalendarList feed = calendarService.getCalendarList();
+            CalendarList feed = null;
+            try {
+                feed = calendarService.getCalendarList();
+            } catch (IOException e) {
+                LOGGER.warning(e.getMessage());
+            }
             if (feed != null) {
                 requestScope("calendars", feed.getItems());
                 storedCredential = true;
diff --git a/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml b/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
index e1fcf26..da02923 100644
--- a/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
+++ b/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
@@ -1,4 +1,4 @@
-<!-- Indices written at Wed, 23 May 2012 17:09:11 JST -->
+<!-- Indices written at Wed, 23 May 2012 17:28:31 JST -->

 <datastore-indexes/>

diff --git a/war/js/nico2ical.js b/war/js/nico2ical.js
index b545bc2..fbe889c 100644
--- a/war/js/nico2ical.js
+++ b/war/js/nico2ical.js
@@ -36,10 +36,14 @@ $(function () {

     var CalendarSummaries = function(selectedValue) {
         this.selectedValue = ko.observable(selectedValue);
+        this.isSaveSuccess = ko.observable(false);
+        this.isSavaError = ko.observable(false);
+        this.isEnableButton = function() {
+            return 0 < this.selectedValue().length;
+        }.bind(this);
         this.updateCalendar = function() {
-            $('#save-calendar')
-                .removeClass('btn-success btn-danger')
-                .button('loading');
+            var s = this;
+            $('#save-calendar').button('loading');
             $('input[name=calendarSummaries]').attr('disabled', 'disabled');
             $.ajax(
                     '/myCalendar/save',
@@ -52,22 +56,21 @@ $(function () {
                             }
                     }
             ).success(function(data) {
-                $('#save-calendar').addClass('btn-success').button('complete');
+                $('#save-calendar').button('complete');
+                s.isSaveSuccess(true);
             }).error(function(xhr, textStatus, errorThrown) {
-                $('#save-calendar').addClass('btn-danger').button('error');
+                $('#save-calendar').button('error');
+                s.isSavaError(true);
             }).complete(function() {
                 $('input[name=calendarSummaries]').removeAttr('disabled');
             });
         }.bind(this);
-        this.isEnableButton = function() {
-            return 0 < this.selectedValue().length;
-        }.bind(this);
         this.disConnectCalendar = function() {
+            var s = this;
             if (confirm('実行すると連携が解除されます。よろしいですか？')) {
                 $('#disconnect-calendar')
                     .removeClass('btn-danger')
                     .button('loading');
-                $('input[name=calendarSummaries]').attr('disabled', 'disabled');
                 $.ajax(
                         '/myCalendar/disConnect',
                         {
@@ -80,9 +83,9 @@ $(function () {
                 ).success(function(data) {
                     $('#disconnect-calendar').hide();
                     $('#connect-calendar').removeClass('hidden');
+                    s.selectedValue('');
                 }).error(function(xhr, textStatus, errorThrown) {
                     $('#disconnect-calendar').addClass('btn-danger').button('error');
-                }).complete(function() {
                 });
             }
         }.bind(this);
diff --git a/war/myCalendar/index.jsp b/war/myCalendar/index.jsp
index 84b1c93..70b93f6 100644
--- a/war/myCalendar/index.jsp
+++ b/war/myCalendar/index.jsp
@@ -48,7 +48,7 @@
                            data-loading-text="登録中..."
                            data-complete-text="登録しました"
                            data-error-text="登録に失敗しました"
-                           data-bind="css: { 'btn-primary': isEnableButton() }, enable: selectedValue().length > 0, click: updateCalendar"
+                           data-bind="css: { 'btn-primary': isEnableButton(), 'btn-success': isSaveSuccess(), 'btn-danger': isSavaError() }, enable: selectedValue().length > 0, click: updateCalendar"
                             />
                   </c:if>
                 </div><!-- /.controls -->
[from now] 2012/05/23 18:34:05

diff --git a/src/main/java/org/ryu22e/nico2cal/servlet/ConnectGoogleApiCallback.java b/src/main/java/org/ryu22e/nico2cal/servlet/ConnectGoogleApiCallback.java
index 737e99f..ce6268a 100644
--- a/src/main/java/org/ryu22e/nico2cal/servlet/ConnectGoogleApiCallback.java
+++ b/src/main/java/org/ryu22e/nico2cal/servlet/ConnectGoogleApiCallback.java
@@ -45,7 +45,7 @@ public final class ConnectGoogleApiCallback extends
     protected void onError(HttpServletRequest req, HttpServletResponse resp,
             AuthorizationCodeResponseUrl errorResponse)
             throws ServletException, IOException {
-        // handle error
+        resp.sendRedirect("/myCalendar");
     }

     @Override
diff --git a/war/js/nico2ical.js b/war/js/nico2ical.js
index fbe889c..c4978b8 100644
--- a/war/js/nico2ical.js
+++ b/war/js/nico2ical.js
@@ -38,6 +38,7 @@ $(function () {
         this.selectedValue = ko.observable(selectedValue);
         this.isSaveSuccess = ko.observable(false);
         this.isSavaError = ko.observable(false);
+        this.isDisconnectError = ko.observable(false);
         this.isEnableButton = function() {
             return 0 < this.selectedValue().length;
         }.bind(this);
@@ -68,9 +69,8 @@ $(function () {
         this.disConnectCalendar = function() {
             var s = this;
             if (confirm('実行すると連携が解除されます。よろしいですか？')) {
-                $('#disconnect-calendar')
-                    .removeClass('btn-danger')
-                    .button('loading');
+                s.isDisconnectError(false);
+                $('#disconnect-calendar').button('loading');
                 $.ajax(
                         '/myCalendar/disConnect',
                         {
@@ -83,9 +83,10 @@ $(function () {
                 ).success(function(data) {
                     $('#disconnect-calendar').hide();
                     $('#connect-calendar').removeClass('hidden');
-                    s.selectedValue('');
+                    $('#nav-calendar-select').hide();
                 }).error(function(xhr, textStatus, errorThrown) {
                     $('#disconnect-calendar').addClass('btn-danger').button('error');
+                    s.isDisconnectError(true);
                 });
             }
         }.bind(this);
diff --git a/war/myCalendar/index.jsp b/war/myCalendar/index.jsp
index 70b93f6..0513443 100644
--- a/war/myCalendar/index.jsp
+++ b/war/myCalendar/index.jsp
@@ -13,7 +13,7 @@

       <ul class="nav nav-tabs" id="my-calendar-tab">
         <c:if test="${storedCredential}">
-          <li class="active">
+          <li id="nav-calendar-select" class="active">
             <a data-target="#my-calendar-select" href="#">連携先の選択</a>
           </li>
           <li>
@@ -66,7 +66,7 @@
                      class="btn"
                      data-loading-text="連携解除中..."
                      data-error-text="連携解除に失敗しました"
-                     data-bind="click: disConnectCalendar">
+                     data-bind="css: {'btn-danger': isDisconnectError()}, click: disConnectCalendar">
                      Google Calendar連携OFF
                   </a>
                 </c:if>