[SYNCOPE-1255] provides max execution time for all operations executed through submit...
authorfmartelli <fabio.martelli@gmail.com>
Thu, 21 Dec 2017 18:58:46 +0000 (19:58 +0100)
committerfmartelli <fabio.martelli@gmail.com>
Thu, 21 Dec 2017 19:01:16 +0000 (20:01 +0100)
14 files changed:
client/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleApplication.java
client/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java
client/console/src/main/java/org/apache/syncope/client/console/panels/NotificationPanel.java
client/console/src/main/java/org/apache/syncope/client/console/wizards/AjaxWizard.java
client/console/src/main/java/org/apache/syncope/client/console/wizards/AjaxWizardBuilder.java
client/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ResourceWizardBuilder.java
client/console/src/main/resources/console.properties
client/console/src/main/resources/org/apache/syncope/client/console/SyncopeConsoleApplication.properties
client/console/src/main/resources/org/apache/syncope/client/console/SyncopeConsoleApplication_it.properties
client/console/src/main/resources/org/apache/syncope/client/console/SyncopeConsoleApplication_pt_BR.properties
client/console/src/main/resources/org/apache/syncope/client/console/SyncopeConsoleApplication_ru.properties
client/console/src/main/resources/org/apache/syncope/client/console/panels/NotificationPanel.html
fit/console-reference/src/main/resources/console.properties
fit/core-reference/src/test/resources/console.properties

index ba08b8a..e9dd9a6 100644 (file)
@@ -106,6 +106,8 @@ public class SyncopeConsoleApplication extends AuthenticatedWebApplication {
 
     private Integer maxUploadFileSizeMB;
 
+    private Integer maxWaitTime;
+
     private List<String> domains;
 
     private Map<String, Class<? extends BasePage>> pageClasses;
@@ -159,6 +161,8 @@ public class SyncopeConsoleApplication extends AuthenticatedWebApplication {
                 ? null
                 : Integer.valueOf(props.getProperty("maxUploadFileSizeMB"));
 
+        maxWaitTime = Integer.valueOf(props.getProperty("maxWaitTimeOnApplyChanges", "30"));
+
         String csrf = props.getProperty("csrf");
 
         // process page properties
@@ -289,6 +293,10 @@ public class SyncopeConsoleApplication extends AuthenticatedWebApplication {
         return maxUploadFileSizeMB;
     }
 
+    public Integer getMaxWaitTimeInSeconds() {
+        return maxWaitTime;
+    }
+
     public SyncopeClientFactoryBean newClientFactory() {
         return new SyncopeClientFactoryBean().
                 setAddress(scheme + "://" + host + ":" + port + "/" + rootPath).
index 9bba50c..3ca1edd 100644 (file)
@@ -26,7 +26,9 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import javax.ws.rs.core.EntityTag;
 import javax.ws.rs.core.MediaType;
@@ -120,6 +122,10 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession {
         executorService.execute(command);
     }
 
+    public <T> Future<T> execute(final Callable<T> command) {
+        return executorService.submit(command);
+    }
+
     public PlatformInfo getPlatformInfo() {
         return platformInfo;
     }
index b113195..221ae41 100644 (file)
@@ -47,7 +47,7 @@ public class NotificationPanel extends Panel implements IFeedback, IGenericCompo
                 "[ { type: 'success', template: $('#successTemplate').html() },"
                 + " { type: 'info', template: $('#successTemplate').html() },"
                 + " { type: 'error', template: $('#errorTemplate').html() },"
-                + " { type: 'warning', template: $('#errorTemplate').html() } ] ");
+                + " { type: 'warning', template: $('#warningTemplate').html() } ] ");
 
         notification = new Notification(Constants.FEEDBACK, options) {
 
@@ -66,6 +66,10 @@ public class NotificationPanel extends Panel implements IFeedback, IGenericCompo
         for (FeedbackMessage message : this.getModelObject()) {
             if (message.isError()) {
                 this.notification.error(handler, message.getMessage());
+            } else if (message.isWarning()) {
+                // this is necessary before check for success and info in order to show warnings: isSuccess and isInfo
+                // return true also in case of warnings ...
+                this.notification.warn(handler, message.getMessage());
             } else if (message.isSuccess() || message.isInfo()) {
                 this.notification.success(handler, message.getMessage());
             } else {
index 04449e2..a26f6b7 100644 (file)
@@ -22,7 +22,14 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.client.console.SyncopeConsoleApplication;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.client.console.pages.BasePage;
 import org.apache.wicket.Component;
@@ -41,7 +48,10 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.apache.syncope.client.console.panels.SubmitableModalPanel;
 import org.apache.syncope.client.console.panels.WizardModalPanel;
+import org.apache.wicket.Application;
 import org.apache.wicket.PageReference;
+import org.apache.wicket.Session;
+import org.apache.wicket.ThreadContext;
 import org.apache.wicket.markup.html.list.ListItem;
 import org.apache.wicket.markup.html.list.ListView;
 import org.apache.wicket.model.IModel;
@@ -149,7 +159,13 @@ public abstract class AjaxWizard<T extends Serializable> extends Wizard
 
     protected abstract void onCancelInternal();
 
-    protected abstract Serializable onApplyInternal(final AjaxRequestTarget target);
+    /**
+     * Apply operation
+     *
+     * @param target request target
+     * @return a pair of payload (maybe null) and resulting object.
+     */
+    protected abstract Pair<Serializable, Serializable> onApplyInternal(final AjaxRequestTarget target);
 
     /**
      * @see org.apache.wicket.extensions.wizard.Wizard#onCancel()
@@ -179,12 +195,21 @@ public abstract class AjaxWizard<T extends Serializable> extends Wizard
     public final void onFinish() {
         final AjaxRequestTarget target = RequestCycle.get().find(AjaxRequestTarget.class);
         try {
-            final Serializable res = onApplyInternal(target);
+            final Serializable res = onApply(target);
             if (eventSink == null) {
                 send(AjaxWizard.this, Broadcast.BUBBLE, new NewItemFinishEvent<>(item, target).setResult(res));
             } else {
                 send(eventSink, Broadcast.EXACT, new NewItemFinishEvent<>(item, target).setResult(res));
             }
+        } catch (TimeoutException te) {
+            LOG.warn("Operation applying took to long", te);
+            if (eventSink == null) {
+                send(AjaxWizard.this, Broadcast.BUBBLE, new NewItemCancelEvent<>(item, target));
+            } else {
+                send(eventSink, Broadcast.EXACT, new NewItemCancelEvent<>(item, target));
+            }
+            SyncopeConsoleSession.get().warn(getString("timeout"));
+            ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
         } catch (Exception e) {
             LOG.error("Wizard error on finish", e);
             SyncopeConsoleSession.get().error(StringUtils.isBlank(e.getMessage())
@@ -337,11 +362,68 @@ public abstract class AjaxWizard<T extends Serializable> extends Wizard
 
     @Override
     public void onSubmit(final AjaxRequestTarget target, final Form<?> form) {
-        onApplyInternal(target);
+        try {
+            onApply(target);
+        } catch (TimeoutException te) {
+            LOG.warn("Operation applying took to long", te);
+            send(eventSink, Broadcast.EXACT, new NewItemCancelEvent<>(item, target));
+            SyncopeConsoleSession.get().warn(getString("timeout"));
+            ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
+        }
     }
 
     @Override
     public void onError(final AjaxRequestTarget target, final Form<?> form) {
         ((BasePage) getPage()).getNotificationPanel().refresh(target);
     }
+
+    private Serializable onApply(final AjaxRequestTarget target) throws TimeoutException {
+        try {
+            final Future<Pair<Serializable, Serializable>> executor
+                    = SyncopeConsoleSession.get().execute(new ApplyFuture(target));
+
+            final Pair<Serializable, Serializable> res
+                    = executor.get(SyncopeConsoleApplication.get().getMaxWaitTimeInSeconds(), TimeUnit.SECONDS);
+
+            if (res.getLeft() != null) {
+                send(pageRef.getPage(), Broadcast.BUBBLE, res.getLeft());
+            }
+
+            return res.getRight();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private class ApplyFuture implements Callable<Pair<Serializable, Serializable>>, Serializable {
+
+        private static final long serialVersionUID = -4657123322652656848L;
+
+        private final AjaxRequestTarget target;
+
+        private final Application application;
+
+        private final RequestCycle requestCycle;
+
+        private final Session session;
+
+        ApplyFuture(final AjaxRequestTarget target) {
+            this.target = target;
+            this.application = Application.get();
+            this.requestCycle = RequestCycle.get();
+            this.session = Session.exists() ? Session.get() : null;
+        }
+
+        @Override
+        public Pair<Serializable, Serializable> call() throws Exception {
+            try {
+                ThreadContext.setApplication(this.application);
+                ThreadContext.setRequestCycle(this.requestCycle);
+                ThreadContext.setSession(this.session);
+                return AjaxWizard.this.onApplyInternal(this.target);
+            } finally {
+                ThreadContext.detach();
+            }
+        }
+    }
 }
index 32514ad..f43b57b 100644 (file)
@@ -22,10 +22,10 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import org.apache.commons.lang3.tuple.Pair;
 import org.apache.wicket.Component;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.event.Broadcast;
 import org.apache.wicket.extensions.wizard.WizardModel;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -100,8 +100,8 @@ public abstract class AjaxWizardBuilder<T extends Serializable> extends Abstract
             }
 
             @Override
-            protected Serializable onApplyInternal(final AjaxRequestTarget target) {
-                final Serializable res = AjaxWizardBuilder.this.onApplyInternal(modelObject);
+            protected Pair<Serializable, Serializable> onApplyInternal(final AjaxRequestTarget target) {
+                Serializable res = AjaxWizardBuilder.this.onApplyInternal(modelObject);
 
                 Serializable payload;
                 switch (mode) {
@@ -116,11 +116,7 @@ public abstract class AjaxWizardBuilder<T extends Serializable> extends Abstract
                         payload = null;
                 }
 
-                if (payload != null) {
-                    send(pageRef.getPage(), Broadcast.BUBBLE, payload);
-                }
-
-                return res;
+                return Pair.of(payload, res);
             }
         }.setEventSink(eventSink).addOuterObject(outerObjects);
     }
index 14187b0..b08e29b 100644 (file)
@@ -81,13 +81,13 @@ public class ResourceWizardBuilder extends AbstractResourceWizardBuilder<Resourc
 
     @Override
     protected ResourceTO onApplyInternal(final Serializable modelObject) {
-        final ResourceTO resourceTO = ResourceTO.class.cast(modelObject);
+        ResourceTO resourceTO = ResourceTO.class.cast(modelObject);
         if (createFlag) {
-            return resourceRestClient.create(resourceTO);
+            resourceTO = resourceRestClient.create(resourceTO);
         } else {
             resourceRestClient.update(resourceTO);
-            return resourceTO;
         }
+        return resourceTO;
     }
 
     @Override
index bc9a009..9c46f7a 100644 (file)
@@ -28,6 +28,9 @@ rootPath=/syncope/rest/
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
+# Max wait time on apply changes from modals/wizards (given in seconds)
+maxWaitTimeOnApplyChanges=30
+
 csrf=true
 
 flowableModelerDirectory=${flowable-modeler.directory}
index cca9dea..f668185 100644 (file)
@@ -72,3 +72,5 @@ intAttrNameInfo.help=Besides auto-completed attributes, you can also refer to gr
 confirmGlobalLogout=Do you really want to perform global logout?
 administration=Administration
 implementations=Implementations
+
+timeout=Operation is taking to long: it will be executed in background. Please check later for the result (errors won't be triggered).
index 8aadcef..fa983f7 100644 (file)
@@ -72,3 +72,5 @@ intAttrNameInfo.help=Oltre agli attributi auto-completati, \u00e8 possibile fare
 confirmGlobalLogout=Vuoi davvero procedere al logout globale?
 administration=Amministrazione
 implementations=Implementazioni
+
+timeout=L'operazione sta durando troppo: sar\u00e0 eseguita in background. Verifica il risultato pi\u00f9 tardi (gli errori non saranno notificati).
index 6916cf2..8676cdb 100644 (file)
@@ -72,3 +72,5 @@ intAttrNameInfo.help=Besides auto-completed attributes, you can also refer to gr
 confirmGlobalLogout=Do you really want to perform global logout?
 administration=Administra\u00e7\u00e3o
 implementations=Implementa\u00e7\u00f5es
+
+timeout=Operation is taking to long: it will be executed in background. Please check later for the result (errors won't be triggered).
index fc2a17a..e77a0d4 100644 (file)
@@ -71,3 +71,5 @@ intAttrNameInfo.help=\u041f\u043e\u043c\u0438\u043c\u043e \u0430\u0432\u0442\u04
 confirmGlobalLogout=Do you really want to perform global logout?
 administration=Administration
 implementations=\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438
+
+timeout=Operation is taking to long: it will be executed in background. Please check later for the result (errors won't be triggered).
index 2870588..1d5a8a2 100644 (file)
@@ -40,6 +40,14 @@ under the License.
       #= message #     
       </div>
     </script>
+    
+    <script id="warningTemplate" type="text/x-kendo-template">
+      <div class="alert alert-warning alert-dismissible" style="margin-bottom: 0px; min-width: 370px">
+      <button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
+      <h4><i class="icon fa fa-ban"></i> Warning!</h4>
+      #= message #     
+      </div>
+    </script>
 
     <script id="successTemplate" type="text/x-kendo-template">
       <div class="alert alert-success alert-dismissible" style="margin-bottom: 0px; min-width: 370px">
index 2612894..bce070a 100644 (file)
@@ -28,6 +28,9 @@ rootPath=/syncope/rest/
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
+# Max wait time on apply changes from modals/wizards (given in seconds)
+maxWaitTimeOnApplyChanges=30
+
 csrf=true
 
 flowableModelerDirectory=${flowable-modeler.directory}
index 24fe5f9..43ee365 100644 (file)
@@ -28,6 +28,9 @@ rootPath=/syncope/rest/
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
+# Max wait time on apply changes from modals/wizards (given in seconds)
+maxWaitTimeOnApplyChanges=30
+
 csrf=false
 
 flowableModelerDirectory=${flowable-modeler.directory}