fix #9931 catching unusaully fast thrown MailConnectExceptions in LoginPresenter
[cdm-vaadin.git] / src / main / java / eu / etaxonomy / cdm / vaadin / view / LoginPresenter.java
index 910c5c6c8dcb302d2ac2623310605022a83015c7..27e4973edac5bb651a97d53dc915a888bf072af1 100644 (file)
@@ -8,23 +8,46 @@
 */
 package eu.etaxonomy.cdm.vaadin.view;
 
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import javax.mail.internet.AddressException;
 
 import org.apache.commons.lang.StringUtils;
 import org.apache.log4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.context.event.EventListener;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.env.Environment;
+import org.springframework.mail.MailException;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.vaadin.spring.events.Event;
+import org.vaadin.spring.events.EventBus;
+import org.vaadin.spring.events.EventBusListener;
+import org.vaadin.spring.events.annotation.EventBusListenerMethod;
 
+import com.vaadin.data.validator.AbstractStringValidator;
 import com.vaadin.spring.annotation.SpringComponent;
-import com.vaadin.spring.annotation.UIScope;
+import com.vaadin.spring.annotation.ViewScope;
+import com.vaadin.ui.themes.ValoTheme;
 
+import eu.etaxonomy.cdm.api.application.ICdmRepository;
+import eu.etaxonomy.cdm.api.config.CdmConfigurationKeys;
+import eu.etaxonomy.cdm.api.service.security.AccountSelfManagementException;
+import eu.etaxonomy.cdm.api.service.security.EmailAddressNotFoundException;
 import eu.etaxonomy.cdm.vaadin.event.AuthenticationAttemptEvent;
 import eu.etaxonomy.cdm.vaadin.event.AuthenticationSuccessEvent;
+import eu.etaxonomy.cdm.vaadin.event.UserAccountEvent;
+import eu.etaxonomy.cdm.vaadin.ui.UserAccountSelfManagementUI;
+import eu.etaxonomy.cdm.vaadin.util.VaadinServletUtilities;
 import eu.etaxonomy.vaadin.mvp.AbstractPresenter;
 import eu.etaxonomy.vaadin.ui.navigation.NavigationEvent;
 import eu.etaxonomy.vaadin.ui.navigation.NavigationManager;
@@ -46,8 +69,8 @@ import eu.etaxonomy.vaadin.ui.navigation.NavigationManager;
  *
  */
 @SpringComponent
-@UIScope // DO NOT CHANGE !!! LoginPresenter must be in UIScope so that the LoginPresenter is available to all Views.
-public class LoginPresenter extends AbstractPresenter<LoginView> {
+@ViewScope
+public class LoginPresenter extends AbstractPresenter<LoginView> implements EventBusListener<AuthenticationAttemptEvent> {
 
     private static final long serialVersionUID = 4020699735656994791L;
 
@@ -57,10 +80,27 @@ public class LoginPresenter extends AbstractPresenter<LoginView> {
 
     private final static String PROPNAME_PASSWORD = "cdm-vaadin.login.pwd";
 
+    private String redirectToState;
+
+    protected EventBus.UIEventBus uiEventBus;
+
     @Autowired
-    protected ApplicationEventPublisher eventBus;
+    @Qualifier("cdmRepository")
+    private ICdmRepository repo;
 
-    private String redirectToState;
+    @Autowired
+    protected Environment env;
+
+//    @Override
+//    protected void eventViewBusSubscription(ViewEventBus viewEventBus) {
+//        viewEventBus.subscribe(this);
+//    }
+
+    @Autowired
+    protected void setUIEventBus(EventBus.UIEventBus uiEventBus){
+        this.uiEventBus = uiEventBus;
+        uiEventBus.subscribe(this);
+    }
 
     public boolean authenticate(String userName, String password) {
 
@@ -74,9 +114,9 @@ public class LoginPresenter extends AbstractPresenter<LoginView> {
                 log.debug("user '" + userName + "' authenticated");
                 currentSecurityContext().setAuthentication(authentication);
                 if(NavigationManager.class.isAssignableFrom(getNavigationManager().getClass())){
-                    eventBus.publishEvent(new AuthenticationSuccessEvent(userName));
+                    uiEventBus.publish(this, new AuthenticationSuccessEvent(userName));
                     log.debug("redirecting to " + redirectToState);
-                    eventBus.publishEvent(new NavigationEvent(redirectToState));
+                    uiEventBus.publish(this, new NavigationEvent(redirectToState));
                 }
             }
         } catch (AuthenticationException e){
@@ -85,11 +125,6 @@ public class LoginPresenter extends AbstractPresenter<LoginView> {
         return false;
     }
 
-
-
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void handleViewEntered() {
 
@@ -106,18 +141,156 @@ public class LoginPresenter extends AbstractPresenter<LoginView> {
             redirectToState = String.join("/", redirectToStateTokens);
         }
 
+        getView().getLoginDialog().getEmail().addValidator(new AbstractStringValidator("An account for this email address already exits. You may want to use the \"Password Revovery\" tab intsead?") {
+            private static final long serialVersionUID = 1L;
+            @Override
+            protected boolean isValidValue(String value) {
+                return !repo.getAccountRegistrationService().emailAddressExists(value);
+            }
+        });
+
         // attempt to auto login
         if(StringUtils.isNotEmpty(System.getProperty(PROPNAME_USER)) && StringUtils.isNotEmpty(System.getProperty(PROPNAME_PASSWORD))){
             log.warn("Performing autologin with user " + System.getProperty(PROPNAME_USER));
             authenticate(System.getProperty(PROPNAME_USER), System.getProperty(PROPNAME_PASSWORD));
         }
+
     }
 
-    @EventListener
-    protected void onLoginEvent(AuthenticationAttemptEvent e){
-        authenticate(e.getUserName(), getView().getLoginDialog().getPassword().getValue());
+    @Override
+    public void onEvent(Event<AuthenticationAttemptEvent> event) {
+        if(getView()!= null){
+            authenticate(event.getPayload().getUserName(), getView().getLoginDialog().getPassword().getValue());
+        } else {
+            log.info("view is NULL, not yet disposed LoginPresenter?");
+        }
     }
 
+    @EventBusListenerMethod
+    public void onPasswordRevoveryEvent(UserAccountEvent event) throws MalformedURLException, ExecutionException, MailException, AddressException, AccountSelfManagementException {
+
+        if(event.getAction().equals(UserAccountEvent.UserAccountAction.REQUEST_PASSWORD_RESET)) {
+            requestPasswordReset();
+        } else if(event.getAction().equals(UserAccountEvent.UserAccountAction.REGISTER_ACCOUNT)) {
+            requestAccountCreation();
+        }
+    }
 
+    private void requestPasswordReset() throws MalformedURLException, ExecutionException {
+        String userNameOrEmail = getView().getLoginDialog().getUserNameOrEmail().getValue();
+        URL servletBaseUrl = VaadinServletUtilities.getServletBaseUrl();
+        logger.debug("UserAccountAction.REQUEST_PASSWORD_RESET for " + servletBaseUrl + ", userNameOrEmail:" + userNameOrEmail);
+        // Implementation note: UI modifications allied in the below callback methods will not affect the UI
+        // immediately, therefore we use a CountDownLatch
+        CountDownLatch finshedSignal = new CountDownLatch(1);
+        List<Throwable> asyncException = new ArrayList<>(1);
+        ListenableFuture<Boolean> futureResult = repo.getPasswordResetService().emailResetToken(
+                userNameOrEmail,
+                servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + PasswordResetViewBean.NAME + "/%s");
+        futureResult.addCallback(
+                    successFuture -> {
+                        finshedSignal.countDown();
+                    },
+                    exception -> {
+                        // possible MailException
+                        asyncException.add(exception);
+                        finshedSignal.countDown();
+                    }
+                );
+        boolean asyncTimeout = false;
+        Boolean result = false;
+        try {
+            finshedSignal.await(2, TimeUnit.SECONDS);
+            result = futureResult.get();
+        } catch (InterruptedException e) {
+            asyncTimeout = true;
+        } catch (Exception e) {
+            // in case executing getUserNameOrEmail() causes an exception faster
+            // than futureResult.addCallback( can be processed, the exception
+            // can not be caught asynchronously
+            // so we are adding all these exceptions here
+            asyncException.add(e);
+        }
+        if(!asyncException.isEmpty()) {
+            String messageText = "An unknown error has occurred.";
+            if(asyncException.get(0) instanceof MailException) {
+                String supportText = "the support";
+                String supportEmail = env.getProperty(CdmConfigurationKeys.MAIL_ADDRESS_SUPPORT);
+                if(supportEmail != null) {
+                    supportText = "<a href=\"mailto:" + supportEmail +"\">" + supportEmail + "</a>";
+                }
+                messageText = "Sending the password reset email to you has failed. Please try again later or contact " + supportText + " in case this error persists.";
+            }
+            if(asyncException.get(0) instanceof EmailAddressNotFoundException) {
+                messageText = "There is no user accout for this email address.";
+            }
+            getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue(messageText);
+            getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
+        } else {
+            if(!asyncTimeout && result) {
+                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("An email with a password reset link has been sent to you.");
+                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_SUCCESS);
+                getView().getLoginDialog().getSendOnetimeLogin().setEnabled(false);
+                getView().getLoginDialog().getUserNameOrEmail().setEnabled(false);
+                getView().getLoginDialog().getUserNameOrEmail().setReadOnly(true);
+
+            } else {
+                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("A timeout has occured, please try again.");
+                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
+            }
+        }
+    }
+
+    private void requestAccountCreation() throws MalformedURLException, MailException, AddressException, AccountSelfManagementException, ExecutionException {
+        String emailAddress = getView().getLoginDialog().getEmail().getValue();
+        URL servletBaseUrl = VaadinServletUtilities.getServletBaseUrl();
+
+        logger.debug("UserAccountAction.REGISTER_ACCOUNT for " + servletBaseUrl + ", emailAddress:" + emailAddress);
+
+        CountDownLatch finshedSignal = new CountDownLatch(1);
+        List<Throwable> asyncException = new ArrayList<>(1);
+        ListenableFuture<Boolean> futureResult = repo.getAccountRegistrationService().emailAccountRegistrationRequest(emailAddress,
+                servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + AccountRegistrationViewBean.NAME + "/%s");
+        futureResult.addCallback(
+                    successFuture -> {
+                        finshedSignal.countDown();
+                    },
+                    exception -> {
+                        // possible MailException
+                        asyncException.add(exception);
+                        finshedSignal.countDown();
+                    }
+                );
+        boolean asyncTimeout = false;
+        Boolean result = false;
+        try {
+            finshedSignal.await(2, TimeUnit.SECONDS);
+            result = futureResult.get();
+        } catch (InterruptedException e) {
+            asyncTimeout = true;
+        } catch (Exception e) {
+            // in case executing emailAccountRegistrationRequest() causes an exception faster
+            // than futureResult.addCallback( can be processed, the exception
+            // can not be caught asynchronously
+            // so we are adding all these exceptions here
+            asyncException.add(e);
+        }
+        if(!asyncException.isEmpty()) {
+            getView().getLoginDialog().getRegisterMessageLabel()
+            .setValue("Sending the account resitration email to you has failed. Please try again later or contect the support in case this error persists.");
+            getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
+        } else {
+            if(!asyncTimeout && result) {
+                getView().getLoginDialog().getRegisterMessageLabel().setValue("An email with with further instructions has been sent to you.");
+                getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_SUCCESS);
+                getView().getLoginDialog().getEmail().setEnabled(false);
+                getView().getLoginDialog().getRegisterButton().setEnabled(false);
+
+            } else {
+                getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
+                getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
+            }
+        }
+    }
 
 }