You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2015/02/13 12:44:49 UTC

[45/51] [partial] syncope git commit: [SYNCOPE-620] Re-organization completed

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/commons/status/StatusUtils.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/commons/status/StatusUtils.java b/client/console/src/main/java/org/apache/syncope/client/console/commons/status/StatusUtils.java
new file mode 100644
index 0000000..3c0e56e
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/commons/status/StatusUtils.java
@@ -0,0 +1,324 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.commons.status;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.syncope.client.console.commons.ConnIdSpecialAttributeName;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.panels.ImagePanel;
+import org.apache.syncope.client.console.panels.StatusPanel;
+import org.apache.syncope.client.console.rest.AbstractSubjectRestClient;
+import org.apache.syncope.common.lib.mod.StatusMod;
+import org.apache.syncope.common.lib.to.AbstractAttributableTO;
+import org.apache.syncope.common.lib.to.AbstractSubjectTO;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.ConnObjectTO;
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.html.image.Image;
+import org.apache.wicket.request.resource.ContextRelativeResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class StatusUtils implements Serializable {
+
+    private static final long serialVersionUID = 7238009174387184309L;
+
+    /**
+     * Logger.
+     */
+    private static final Logger LOG = LoggerFactory.getLogger(StatusUtils.class);
+
+    private static final String IMG_PREFIX = "/img/statuses/";
+
+    private final AbstractSubjectRestClient restClient;
+
+    public StatusUtils(final AbstractSubjectRestClient restClient) {
+        this.restClient = restClient;
+    }
+
+    public List<ConnObjectWrapper> getConnectorObjects(final AbstractSubjectTO subject) {
+        final List<ConnObjectWrapper> objects = new ArrayList<>();
+        objects.addAll(getConnectorObjects(subject, subject.getResources()));
+        return objects;
+    }
+
+    public List<ConnObjectWrapper> getConnectorObjects(
+            final Collection<AbstractSubjectTO> subjects, final Collection<String> resources) {
+
+        final List<ConnObjectWrapper> objects = new ArrayList<>();
+
+        for (AbstractSubjectTO subject : subjects) {
+            objects.addAll(getConnectorObjects(subject, resources));
+        }
+
+        return objects;
+    }
+
+    private List<ConnObjectWrapper> getConnectorObjects(
+            final AbstractSubjectTO subject, final Collection<String> resources) {
+
+        final List<ConnObjectWrapper> objects = new ArrayList<>();
+
+        for (String resourceName : resources) {
+            ConnObjectTO objectTO = null;
+            try {
+                objectTO = restClient.getConnectorObject(resourceName, subject.getKey());
+            } catch (Exception e) {
+                LOG.warn("ConnObject '{}' not found on resource '{}'", subject.getKey(), resourceName);
+            }
+
+            objects.add(new ConnObjectWrapper(subject, resourceName, objectTO));
+        }
+
+        return objects;
+    }
+
+    public StatusBean getStatusBean(
+            final AbstractAttributableTO attributable,
+            final String resourceName,
+            final ConnObjectTO objectTO,
+            final boolean isRole) {
+
+        final StatusBean statusBean = new StatusBean(attributable, resourceName);
+
+        if (objectTO != null) {
+            final Boolean enabled = isEnabled(objectTO);
+
+            final Status status = enabled == null
+                    ? (isRole ? Status.ACTIVE : Status.UNDEFINED)
+                    : enabled
+                            ? Status.ACTIVE
+                            : Status.SUSPENDED;
+
+            final String accountLink = getAccountLink(objectTO);
+
+            statusBean.setStatus(status);
+            statusBean.setAccountLink(accountLink);
+        }
+
+        return statusBean;
+    }
+
+    private Boolean isEnabled(final ConnObjectTO objectTO) {
+        final Map<String, AttrTO> attributeTOs = objectTO.getPlainAttrMap();
+
+        final AttrTO status = attributeTOs.get(ConnIdSpecialAttributeName.ENABLE);
+
+        return status != null && status.getValues() != null && !status.getValues().isEmpty()
+                ? Boolean.parseBoolean(status.getValues().get(0))
+                : null;
+    }
+
+    private String getAccountLink(final ConnObjectTO objectTO) {
+        final Map<String, AttrTO> attributeTOs = objectTO == null
+                ? Collections.<String, AttrTO>emptyMap()
+                : objectTO.getPlainAttrMap();
+
+        final AttrTO name = attributeTOs.get(ConnIdSpecialAttributeName.NAME);
+
+        return name != null && name.getValues() != null && !name.getValues().isEmpty()
+                ? name.getValues().get(0)
+                : null;
+    }
+
+    public static StatusMod buildStatusMod(final Collection<StatusBean> statuses) {
+        return buildStatusMod(statuses, null);
+    }
+
+    public static StatusMod buildStatusMod(final Collection<StatusBean> statuses, final Boolean enable) {
+        StatusMod statusMod = new StatusMod();
+        statusMod.setOnSyncope(false);
+
+        for (StatusBean status : statuses) {
+            if (enable == null
+                    || (enable && !status.getStatus().isActive()) || (!enable && status.getStatus().isActive())) {
+
+                if ("syncope".equalsIgnoreCase(status.getResourceName())) {
+                    statusMod.setOnSyncope(true);
+                } else {
+                    statusMod.getResourceNames().add(status.getResourceName());
+                }
+
+            }
+        }
+
+        return statusMod;
+    }
+
+    public static void update(
+            final AbstractAttributableTO attributable,
+            final StatusPanel statusPanel,
+            final AjaxRequestTarget target,
+            final Collection<String> resourcesToAdd,
+            final Collection<String> resourcesToRemove) {
+
+        if (statusPanel != null) {
+            Map<String, StatusBean> statusMap = new LinkedHashMap<>();
+            for (StatusBean statusBean : statusPanel.getStatusBeans()) {
+                statusMap.put(statusBean.getResourceName(), statusBean);
+            }
+
+            for (String resourceName : resourcesToAdd) {
+                if (!statusMap.keySet().contains(resourceName)) {
+                    StatusBean statusBean;
+                    if (statusPanel.getInitialStatusBeanMap().containsKey(resourceName)) {
+                        statusBean = statusPanel.getInitialStatusBeanMap().get(resourceName);
+                    } else {
+                        statusBean = new StatusBean(attributable, resourceName);
+                        statusBean.setStatus(Status.NOT_YET_SUBMITTED);
+                    }
+
+                    statusMap.put(statusBean.getResourceName(), statusBean);
+                }
+            }
+
+            for (String resource : resourcesToRemove) {
+                statusMap.remove(resource);
+            }
+
+            statusPanel.updateStatusBeans(new ArrayList<>(statusMap.values()));
+            target.add(statusPanel);
+        }
+    }
+
+    public ConnObjectTO getConnObjectTO(
+            final Long attributableId, final String resourceName, final List<ConnObjectWrapper> objects) {
+
+        for (ConnObjectWrapper object : objects) {
+            if (attributableId.equals(object.getAttributable().getKey())
+                    && resourceName.equalsIgnoreCase(object.getResourceName())) {
+
+                return object.getConnObjectTO();
+            }
+        }
+
+        return null;
+    }
+
+    public Image getStatusImage(final String componentId, final Status status) {
+        final String alt, title, statusName;
+
+        switch (status) {
+
+            case NOT_YET_SUBMITTED:
+                statusName = Status.UNDEFINED.toString();
+                alt = "undefined icon";
+                title = "Not yet submitted";
+                break;
+
+            case ACTIVE:
+                statusName = Status.ACTIVE.toString();
+                alt = "active icon";
+                title = "Enabled";
+                break;
+
+            case UNDEFINED:
+                statusName = Status.UNDEFINED.toString();
+                alt = "undefined icon";
+                title = "Undefined status";
+                break;
+
+            case OBJECT_NOT_FOUND:
+                statusName = Status.OBJECT_NOT_FOUND.toString();
+                alt = "notfound icon";
+                title = "Not found";
+                break;
+
+            default:
+                statusName = Status.SUSPENDED.toString();
+                alt = "inactive icon";
+                title = "Disabled";
+        }
+
+        final Image img = new Image(componentId,
+                new ContextRelativeResource(IMG_PREFIX + statusName + Constants.PNG_EXT));
+        img.add(new Behavior() {
+
+            private static final long serialVersionUID = 1469628524240283489L;
+
+            @Override
+            public void onComponentTag(final Component component, final ComponentTag tag) {
+                tag.put("alt", alt);
+                tag.put("title", title);
+            }
+        });
+
+        return img;
+    }
+
+    public ImagePanel getStatusImagePanel(final String componentId, final Status status) {
+        final String alt, title, statusName;
+
+        switch (status) {
+
+            case NOT_YET_SUBMITTED:
+                statusName = Status.UNDEFINED.toString();
+                alt = "undefined icon";
+                title = "Not yet submitted";
+                break;
+
+            case ACTIVE:
+                statusName = Status.ACTIVE.toString();
+                alt = "active icon";
+                title = "Enabled";
+                break;
+
+            case UNDEFINED:
+                statusName = Status.UNDEFINED.toString();
+                alt = "undefined icon";
+                title = "Undefined status";
+                break;
+
+            case OBJECT_NOT_FOUND:
+                statusName = Status.OBJECT_NOT_FOUND.toString();
+                alt = "notfound icon";
+                title = "Not found";
+                break;
+
+            default:
+                statusName = Status.SUSPENDED.toString();
+                alt = "inactive icon";
+                title = "Disabled";
+        }
+
+        final ImagePanel imagePanel = new ImagePanel(componentId,
+                new ContextRelativeResource(IMG_PREFIX + statusName + Constants.PNG_EXT));
+        imagePanel.add(new Behavior() {
+
+            private static final long serialVersionUID = 1469628524240283489L;
+
+            @Override
+            public void onComponentTag(final Component component, final ComponentTag tag) {
+                tag.put("alt", alt);
+                tag.put("title", title);
+            }
+        });
+
+        return imagePanel;
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/init/ConsoleInitializer.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/init/ConsoleInitializer.java b/client/console/src/main/java/org/apache/syncope/client/console/init/ConsoleInitializer.java
new file mode 100644
index 0000000..ec51d3e
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/init/ConsoleInitializer.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.init;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Take care of all initializations needed by Syncope Console to run up and safe.
+ */
+@Component
+public class ConsoleInitializer implements InitializingBean, BeanFactoryAware {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ConsoleInitializer.class);
+
+    private DefaultListableBeanFactory beanFactory;
+
+    @Override
+    public void setBeanFactory(final BeanFactory beanFactory) throws BeansException {
+        this.beanFactory = (DefaultListableBeanFactory) beanFactory;
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+        Map<String, SyncopeConsoleLoader> loaderMap = beanFactory.getBeansOfType(SyncopeConsoleLoader.class);
+
+        List<SyncopeConsoleLoader> loaders = new ArrayList<>(loaderMap.values());
+        Collections.sort(loaders, new Comparator<SyncopeConsoleLoader>() {
+
+            @Override
+            public int compare(final SyncopeConsoleLoader o1, final SyncopeConsoleLoader o2) {
+                return o1.getPriority().compareTo(o2.getPriority());
+            }
+        });
+
+        LOG.debug("Starting initialization...");
+        for (SyncopeConsoleLoader loader : loaders) {
+            LOG.debug("Invoking {} with priority {}", AopUtils.getTargetClass(loader).getName(), loader.getPriority());
+            loader.load();
+        }
+        LOG.debug("Initialization completed");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/init/ImplementationClassNamesLoader.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/init/ImplementationClassNamesLoader.java b/client/console/src/main/java/org/apache/syncope/client/console/init/ImplementationClassNamesLoader.java
new file mode 100644
index 0000000..0ff7282
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/init/ImplementationClassNamesLoader.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.init;
+
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.console.panels.AbstractExtensionPanel;
+import org.apache.syncope.client.console.BinaryPreview;
+import org.apache.syncope.client.console.wicket.markup.html.form.preview.AbstractBinaryPreviewer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
+import org.springframework.core.type.filter.AssignableTypeFilter;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ClassUtils;
+
+@Component
+public class ImplementationClassNamesLoader implements SyncopeConsoleLoader {
+
+    /**
+     * Logger.
+     */
+    private static final Logger LOG = LoggerFactory.getLogger(ImplementationClassNamesLoader.class);
+
+    private List<Class<? extends AbstractBinaryPreviewer>> previewers;
+
+    private List<Class<? extends AbstractExtensionPanel>> extPanels;
+
+    @Override
+    public Integer getPriority() {
+        return 0;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void load() {
+        previewers = new ArrayList<>();
+        extPanels = new ArrayList<>();
+
+        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
+        scanner.addIncludeFilter(new AssignableTypeFilter(AbstractBinaryPreviewer.class));
+        scanner.addIncludeFilter(new AssignableTypeFilter(AbstractExtensionPanel.class));
+
+        for (BeanDefinition bd : scanner.findCandidateComponents(StringUtils.EMPTY)) {
+            try {
+                Class<?> clazz = ClassUtils.resolveClassName(
+                        bd.getBeanClassName(), ClassUtils.getDefaultClassLoader());
+                boolean isAbsractClazz = Modifier.isAbstract(clazz.getModifiers());
+
+                if (AbstractBinaryPreviewer.class.isAssignableFrom(clazz) && !isAbsractClazz) {
+                    previewers.add((Class<? extends AbstractBinaryPreviewer>) clazz);
+                } else if (AbstractExtensionPanel.class.isAssignableFrom(clazz) && !isAbsractClazz) {
+                    extPanels.add((Class<? extends AbstractExtensionPanel>) clazz);
+                }
+
+            } catch (Throwable t) {
+                LOG.warn("Could not inspect class {}", bd.getBeanClassName(), t);
+            }
+        }
+        previewers = Collections.unmodifiableList(previewers);
+        extPanels = Collections.unmodifiableList(extPanels);
+
+        LOG.debug("Binary previewers found: {}", previewers);
+        LOG.debug("Extension panels found: {}", extPanels);
+    }
+
+    public Class<? extends AbstractBinaryPreviewer> getPreviewerClass(final String mimeType) {
+        LOG.debug("Searching for previewer class for MIME type: {}", mimeType);
+        Class<? extends AbstractBinaryPreviewer> previewer = null;
+        for (Class<? extends AbstractBinaryPreviewer> candidate : previewers) {
+            LOG.debug("Evaluating previewer class {} for MIME type {}", candidate.getName(), mimeType);
+            if (ArrayUtils.contains(candidate.getAnnotation(BinaryPreview.class).mimeTypes(), mimeType)) {
+                LOG.debug("Found existing previewer for MIME type {}: {}", mimeType, candidate.getName());
+                previewer = candidate;
+            }
+        }
+        return previewer;
+    }
+
+    public List<Class<? extends AbstractBinaryPreviewer>> getPreviewerClasses() {
+        return previewers;
+    }
+
+    public List<Class<? extends AbstractExtensionPanel>> getExtPanelClasses() {
+        return extPanels;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/init/MIMETypesLoader.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/init/MIMETypesLoader.java b/client/console/src/main/java/org/apache/syncope/client/console/init/MIMETypesLoader.java
new file mode 100644
index 0000000..7a2f878
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/init/MIMETypesLoader.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.init;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.wicket.util.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class MIMETypesLoader implements SyncopeConsoleLoader {
+
+    /**
+     * Logger.
+     */
+    private static final Logger LOG = LoggerFactory.getLogger(MIMETypesLoader.class);
+
+    private List<String> mimeTypes;
+
+    @Override
+    public Integer getPriority() {
+        return 10;
+    }
+
+    @Override
+    public void load() {
+        final Set<String> mediaTypes = new HashSet<>();
+        this.mimeTypes = new ArrayList<>();
+        try {
+            final String mimeTypesFile = IOUtils.toString(getClass().getResourceAsStream("/MIMETypes"));
+            for (String fileRow : mimeTypesFile.split("\n")) {
+                if (StringUtils.isNotBlank(fileRow) && !fileRow.startsWith("#")) {
+                    mediaTypes.add(fileRow);
+                }
+            }
+            this.mimeTypes.addAll(mediaTypes);
+            Collections.sort(this.mimeTypes);
+        } catch (Exception e) {
+            LOG.error("Error reading file MIMETypes from resources", e);
+        }
+    }
+
+    public List<String> getMimeTypes() {
+        LOG.debug("Returning loaded MIME types list {}", mimeTypes);
+        return mimeTypes;
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/init/SyncopeConsoleLoader.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/init/SyncopeConsoleLoader.java b/client/console/src/main/java/org/apache/syncope/client/console/init/SyncopeConsoleLoader.java
new file mode 100644
index 0000000..7c4d3d4
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/init/SyncopeConsoleLoader.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.init;
+
+/**
+ * Marker interface for Syncope console initialization.
+ */
+public interface SyncopeConsoleLoader {
+
+    /**
+     * @return the priority that the implementing class has in the initialization process.
+     */
+    Integer getPriority();
+
+    /**
+     * Perform initialization operations.
+     */
+    void load();
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractBasePage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractBasePage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractBasePage.java
new file mode 100644
index 0000000..4a32700
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractBasePage.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.commons.XMLRolesReader;
+import org.apache.syncope.client.console.init.MIMETypesLoader;
+import org.apache.syncope.client.console.panels.NotificationPanel;
+import org.apache.syncope.client.console.rest.ConfigurationRestClient;
+import org.apache.syncope.client.console.rest.ReportRestClient;
+import org.apache.syncope.client.console.rest.ResourceRestClient;
+import org.apache.syncope.client.console.rest.RoleRestClient;
+import org.apache.syncope.client.console.rest.SchemaRestClient;
+import org.apache.syncope.client.console.rest.TaskRestClient;
+import org.apache.syncope.client.console.rest.UserRestClient;
+import org.apache.syncope.client.console.rest.UserSelfRestClient;
+import org.apache.syncope.client.console.wicket.markup.head.MetaHeaderItem;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.PriorityHeaderItem;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.spring.injection.annot.SpringBean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AbstractBasePage extends WebPage {
+
+    private static final long serialVersionUID = 8611724965544132636L;
+
+    /**
+     * Logger.
+     */
+    protected static final Logger LOG = LoggerFactory.getLogger(AbstractBasePage.class);
+
+    protected static final String TASKS = "Tasks";
+
+    protected static final String FORM = "form";
+
+    protected static final String CANCEL = "cancel";
+
+    protected static final String SUBMIT = "submit";
+
+    protected static final String APPLY = "apply";
+
+    protected final HeaderItem meta = new MetaHeaderItem("X-UA-Compatible", "IE=edge");
+
+    @SpringBean
+    protected XMLRolesReader xmlRolesReader;
+
+    @SpringBean
+    protected UserRestClient userRestClient;
+
+    @SpringBean
+    protected UserSelfRestClient userSelfRestClient;
+
+    @SpringBean
+    protected RoleRestClient roleRestClient;
+
+    @SpringBean
+    protected TaskRestClient taskRestClient;
+
+    @SpringBean
+    protected SchemaRestClient schemaRestClient;
+
+    @SpringBean
+    protected ResourceRestClient resourceRestClient;
+
+    @SpringBean
+    protected ReportRestClient reportRestClient;
+
+    @SpringBean
+    protected ConfigurationRestClient confRestClient;
+
+    @SpringBean
+    protected MIMETypesLoader mimeTypesInitializer;
+
+    protected NotificationPanel feedbackPanel;
+
+    /**
+     * Response flag set by the Modal Window after the operation is completed.
+     */
+    protected boolean modalResult = false;
+
+    public AbstractBasePage() {
+        this(null);
+    }
+
+    public AbstractBasePage(final PageParameters parameters) {
+        super(parameters);
+
+        feedbackPanel = new NotificationPanel(Constants.FEEDBACK);
+        feedbackPanel.setOutputMarkupId(true);
+        add(feedbackPanel);
+    }
+
+    public NotificationPanel getFeedbackPanel() {
+        return feedbackPanel;
+    }
+
+    public boolean isModalResult() {
+        return modalResult;
+    }
+
+    public void setModalResult(final boolean operationResult) {
+        this.modalResult = operationResult;
+    }
+
+    @Override
+    public void renderHead(final IHeaderResponse response) {
+        super.renderHead(response);
+        response.render(new PriorityHeaderItem(meta));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSchedTaskModalPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSchedTaskModalPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSchedTaskModalPage.java
new file mode 100644
index 0000000..78ca906
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSchedTaskModalPage.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.commons.DateFormatROModel;
+import org.apache.syncope.client.console.wicket.markup.html.CrontabContainer;
+import org.apache.syncope.client.console.wicket.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SchedTaskTO;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
+import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxButton;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.model.PropertyModel;
+import org.apache.wicket.model.ResourceModel;
+import org.springframework.util.StringUtils;
+
+/**
+ * Modal window with Task form (to stop and start execution).
+ */
+public abstract class AbstractSchedTaskModalPage extends TaskModalPage {
+
+    private static final long serialVersionUID = 2892005971093059242L;
+
+    protected CrontabContainer crontab;
+
+    public AbstractSchedTaskModalPage(final ModalWindow window, final SchedTaskTO taskTO,
+            final PageReference pageRef) {
+
+        super(taskTO);
+
+        crontab = new CrontabContainer("crontab", new PropertyModel<String>(taskTO, "cronExpression"),
+                taskTO.getCronExpression());
+        form.add(crontab);
+
+        final AjaxTextFieldPanel name =
+                new AjaxTextFieldPanel("name", "name", new PropertyModel<String>(taskTO, "name"));
+        name.setEnabled(true);
+        profile.add(name);
+
+        final AjaxTextFieldPanel description = new AjaxTextFieldPanel("description", "description",
+                new PropertyModel<String>(taskTO, "description"));
+        description.setEnabled(true);
+        profile.add(description);
+
+        final AjaxTextFieldPanel lastExec = new AjaxTextFieldPanel("lastExec", getString("lastExec"),
+                new DateFormatROModel(new PropertyModel<String>(taskTO, "lastExec")));
+        lastExec.setEnabled(false);
+        profile.add(lastExec);
+
+        final AjaxTextFieldPanel nextExec = new AjaxTextFieldPanel("nextExec", getString("nextExec"),
+                new DateFormatROModel(new PropertyModel<String>(taskTO, "nextExec")));
+        nextExec.setEnabled(false);
+        profile.add(nextExec);
+
+        final AjaxButton submit = new IndicatingAjaxButton(APPLY, new ResourceModel(APPLY)) {
+
+            private static final long serialVersionUID = -958724007591692537L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) {
+                SchedTaskTO taskTO = (SchedTaskTO) form.getModelObject();
+                taskTO.setCronExpression(StringUtils.hasText(taskTO.getCronExpression())
+                        ? crontab.getCronExpression()
+                        : null);
+
+                try {
+                    submitAction(taskTO);
+
+                    ((BasePage) pageRef.getPage()).setModalResult(true);
+
+                    window.close(target);
+                } catch (SyncopeClientException e) {
+                    LOG.error("While creating or updating task", e);
+                    error(getString(Constants.ERROR) + ": " + e.getMessage());
+                    feedbackPanel.refresh(target);
+                }
+            }
+
+            @Override
+            protected void onError(final AjaxRequestTarget target, final Form<?> form) {
+                feedbackPanel.refresh(target);
+            }
+        };
+
+        final AjaxButton cancel = new IndicatingAjaxButton(CANCEL, new ResourceModel(CANCEL)) {
+
+            private static final long serialVersionUID = -958724007591692537L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) {
+                window.close(target);
+            }
+        };
+
+        cancel.setDefaultFormProcessing(false);
+
+        if (taskTO.getKey() > 0) {
+            MetaDataRoleAuthorizationStrategy.authorize(
+                    submit, RENDER, xmlRolesReader.getEntitlement(TASKS, "update"));
+        } else {
+            MetaDataRoleAuthorizationStrategy.authorize(
+                    submit, RENDER, xmlRolesReader.getEntitlement(TASKS, "create"));
+        }
+
+        form.add(submit);
+        form.add(cancel);
+    }
+
+    protected abstract void submitAction(SchedTaskTO taskTO);
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSchemaModalPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSchemaModalPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSchemaModalPage.java
new file mode 100644
index 0000000..1c1bd0f
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSchemaModalPage.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import org.apache.syncope.common.lib.to.AbstractSchemaTO;
+import org.apache.syncope.common.lib.types.AttributableType;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
+
+/**
+ * Modal window with Schema form.
+ */
+public abstract class AbstractSchemaModalPage<T extends AbstractSchemaTO> extends BaseModalPage {
+
+    private static final long serialVersionUID = 7369215690388444748L;
+
+    protected AttributableType kind;
+
+    public AbstractSchemaModalPage(final AttributableType kind) {
+        this.kind = kind;
+    }
+
+    public abstract void setSchemaModalPage(PageReference callerPageRef, ModalWindow window, T schema,
+            boolean createFlag);
+
+    public AttributableType getKind() {
+        return kind;
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractStatusModalPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractStatusModalPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractStatusModalPage.java
new file mode 100644
index 0000000..f5dfbca
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractStatusModalPage.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import org.apache.wicket.markup.html.panel.Fragment;
+
+public class AbstractStatusModalPage extends BaseModalPage {
+
+    private static final long serialVersionUID = 6633408683036028540L;
+
+    public AbstractStatusModalPage() {
+        add(new Fragment("pwdMgtFields", "emptyFragment", this));
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSyncTaskModalPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSyncTaskModalPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSyncTaskModalPage.java
new file mode 100644
index 0000000..aa62b41
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/AbstractSyncTaskModalPage.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.commons.SelectChoiceRenderer;
+import org.apache.syncope.client.console.wicket.markup.html.form.AjaxCheckBoxPanel;
+import org.apache.syncope.client.console.wicket.markup.html.form.AjaxDropDownChoicePanel;
+import org.apache.syncope.common.lib.to.AbstractProvisioningTaskTO;
+import org.apache.syncope.common.lib.to.ResourceTO;
+import org.apache.syncope.common.lib.types.MatchingRule;
+import org.apache.syncope.common.lib.types.UnmatchingRule;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxLink;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.LoadableDetachableModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.PropertyModel;
+
+/**
+ * Abstract Modal window for Sync and Push Task form.
+ */
+public abstract class AbstractSyncTaskModalPage extends AbstractSchedTaskModalPage {
+
+    private static final long serialVersionUID = 2148403203517274669L;
+
+    protected AjaxDropDownChoicePanel<MatchingRule> matchingRule;
+
+    protected AjaxDropDownChoicePanel<UnmatchingRule> unmatchingRule;
+
+    protected abstract List<String> getSyncActions();
+
+    final IModel<List<String>> allResources = new LoadableDetachableModel<List<String>>() {
+
+        private static final long serialVersionUID = 5275935387613157437L;
+
+        @Override
+        protected List<String> load() {
+            final List<String> resourceNames = new ArrayList<>();
+
+            for (ResourceTO resourceTO : resourceRestClient.getAll()) {
+                resourceNames.add(resourceTO.getKey());
+            }
+
+            Collections.sort(resourceNames);
+            return resourceNames;
+        }
+    };
+
+    final IModel<List<String>> syncActionsClasses = new LoadableDetachableModel<List<String>>() {
+
+        private static final long serialVersionUID = 5275935387613157438L;
+
+        @Override
+        protected List<String> load() {
+            return getSyncActions();
+        }
+    };
+
+    public AbstractSyncTaskModalPage(
+            final ModalWindow window, final AbstractProvisioningTaskTO taskTO, final PageReference pageRef) {
+
+        super(window, taskTO, pageRef);
+
+        final AjaxDropDownChoicePanel<String> resource = new AjaxDropDownChoicePanel<>("resource",
+                getString("resourceName"), new PropertyModel<String>(taskTO, "resource"));
+        resource.setChoices(allResources.getObject());
+        resource.setChoiceRenderer(new SelectChoiceRenderer<String>());
+        resource.addRequiredLabel();
+        resource.setEnabled(taskTO.getKey() == 0);
+        resource.setStyleSheet("ui-widget-content ui-corner-all long_dynamicsize");
+
+        profile.add(resource);
+
+        final WebMarkupContainer syncActionsClassNames = new WebMarkupContainer("syncActionsClassNames");
+        syncActionsClassNames.setOutputMarkupId(true);
+        profile.add(syncActionsClassNames);
+
+        final AjaxLink<Void> first = new IndicatingAjaxLink<Void>("first") {
+
+            private static final long serialVersionUID = -7978723352517770644L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target) {
+                taskTO.getActionsClassNames().add(StringUtils.EMPTY);
+                setVisible(false);
+                target.add(syncActionsClassNames);
+            }
+        };
+        first.setOutputMarkupPlaceholderTag(true);
+        first.setVisible(taskTO.getActionsClassNames().isEmpty());
+        syncActionsClassNames.add(first);
+
+        final ListView<String> actionsClasses = new ListView<String>(
+                "actionsClasses", new PropertyModel<List<String>>(taskTO, "actionsClassNames")) {
+
+                    private static final long serialVersionUID = 9101744072914090143L;
+
+                    @Override
+                    protected void populateItem(final ListItem<String> item) {
+                        final String className = item.getModelObject();
+
+                        final DropDownChoice<String> actionsClass = new DropDownChoice<String>(
+                                "actionsClass", new Model<String>(className), syncActionsClasses.getObject());
+                        actionsClass.setNullValid(true);
+                        actionsClass.setRequired(true);
+                        actionsClass.add(new AjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
+
+                            private static final long serialVersionUID = -1107858522700306810L;
+
+                            @Override
+                            protected void onUpdate(final AjaxRequestTarget target) {
+                                taskTO.getActionsClassNames().set(item.getIndex(), actionsClass.getModelObject());
+                                target.add(syncActionsClassNames);
+                            }
+                        });
+                        actionsClass.setRequired(true);
+                        actionsClass.setOutputMarkupId(true);
+                        actionsClass.setRequired(true);
+                        item.add(actionsClass);
+
+                        AjaxLink<Void> minus = new IndicatingAjaxLink<Void>("drop") {
+
+                            private static final long serialVersionUID = -7978723352517770644L;
+
+                            @Override
+                            public void onClick(final AjaxRequestTarget target) {
+                                taskTO.getActionsClassNames().remove(className);
+                                first.setVisible(taskTO.getActionsClassNames().isEmpty());
+                                target.add(syncActionsClassNames);
+                            }
+                        };
+                        item.add(minus);
+
+                        final AjaxLink<Void> plus = new IndicatingAjaxLink<Void>("add") {
+
+                            private static final long serialVersionUID = -7978723352517770644L;
+
+                            @Override
+                            public void onClick(final AjaxRequestTarget target) {
+                                taskTO.getActionsClassNames().add(StringUtils.EMPTY);
+                                target.add(syncActionsClassNames);
+                            }
+                        };
+                        plus.setOutputMarkupPlaceholderTag(true);
+                        plus.setVisible(item.getIndex() == taskTO.getActionsClassNames().size() - 1);
+                        item.add(plus);
+                    }
+                };
+        syncActionsClassNames.add(actionsClasses);
+
+        syncActionsClassNames.setEnabled(!syncActionsClasses.getObject().isEmpty());
+
+        final AjaxCheckBoxPanel creates = new AjaxCheckBoxPanel("performCreate", getString("creates"),
+                new PropertyModel<Boolean>(taskTO, "performCreate"));
+        profile.add(creates);
+
+        final AjaxCheckBoxPanel updates = new AjaxCheckBoxPanel("performUpdate", getString("updates"),
+                new PropertyModel<Boolean>(taskTO, "performUpdate"));
+        profile.add(updates);
+
+        final AjaxCheckBoxPanel deletes = new AjaxCheckBoxPanel("performDelete", getString("updates"),
+                new PropertyModel<Boolean>(taskTO, "performDelete"));
+        profile.add(deletes);
+
+        final AjaxCheckBoxPanel syncStatus = new AjaxCheckBoxPanel("syncStatus", getString("syncStatus"),
+                new PropertyModel<Boolean>(taskTO, "syncStatus"));
+        profile.add(syncStatus);
+
+        matchingRule = new AjaxDropDownChoicePanel<MatchingRule>(
+                "matchingRule", "matchingRule", new PropertyModel<MatchingRule>(taskTO, "matchingRule"));
+        matchingRule.setChoices(Arrays.asList(MatchingRule.values()));
+        ((DropDownChoice) matchingRule.getField()).setNullValid(false);
+
+        unmatchingRule = new AjaxDropDownChoicePanel<UnmatchingRule>(
+                "unmatchingRule", "unmatchingRule", new PropertyModel<UnmatchingRule>(taskTO, "unmatchingRule"));
+        unmatchingRule.setChoices(Arrays.asList(UnmatchingRule.values()));
+        ((DropDownChoice) unmatchingRule.getField()).setNullValid(false);
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/ActivitiModelerPopupPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/ActivitiModelerPopupPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/ActivitiModelerPopupPage.java
new file mode 100644
index 0000000..05658fc
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/ActivitiModelerPopupPage.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import org.apache.wicket.markup.html.WebPage;
+
+public class ActivitiModelerPopupPage extends WebPage {
+
+    private static final long serialVersionUID = -7031206743629422898L;
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/ApprovalModalPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/ApprovalModalPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/ApprovalModalPage.java
new file mode 100644
index 0000000..79deeb0
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/ApprovalModalPage.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.commons.MapChoiceRenderer;
+import org.apache.syncope.client.console.rest.ApprovalRestClient;
+import org.apache.syncope.client.console.wicket.markup.html.form.AjaxDropDownChoicePanel;
+import org.apache.syncope.client.console.wicket.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.client.console.wicket.markup.html.form.DateTimeFieldPanel;
+import org.apache.syncope.client.console.wicket.markup.html.form.FieldPanel;
+import org.apache.syncope.client.console.wicket.markup.html.form.SpinnerFieldPanel;
+import org.apache.syncope.client.console.wicket.markup.html.list.AltListView;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.WorkflowFormPropertyTO;
+import org.apache.syncope.common.lib.to.WorkflowFormTO;
+import org.apache.wicket.Page;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
+import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxButton;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.LoadableDetachableModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.spring.injection.annot.SpringBean;
+
+public class ApprovalModalPage extends BaseModalPage {
+
+    private static final long serialVersionUID = -8847854414429745216L;
+
+    private final static int USER_WIN_HEIGHT = 550;
+
+    private final static int USER_WIN_WIDTH = 800;
+
+    @SpringBean
+    private ApprovalRestClient restClient;
+
+    private final ModalWindow editUserWin;
+
+    public ApprovalModalPage(final PageReference pageRef, final ModalWindow window, final WorkflowFormTO formTO) {
+        super();
+
+        IModel<List<WorkflowFormPropertyTO>> formProps = new LoadableDetachableModel<List<WorkflowFormPropertyTO>>() {
+
+            private static final long serialVersionUID = 3169142472626817508L;
+
+            @Override
+            protected List<WorkflowFormPropertyTO> load() {
+                return formTO.getProperties();
+            }
+        };
+
+        final ListView<WorkflowFormPropertyTO> propView =
+                new AltListView<WorkflowFormPropertyTO>("propView", formProps) {
+
+                    private static final long serialVersionUID = 9101744072914090143L;
+
+                    @Override
+                    @SuppressWarnings({ "unchecked", "rawtypes" })
+                    protected void populateItem(final ListItem<WorkflowFormPropertyTO> item) {
+                        final WorkflowFormPropertyTO prop = item.getModelObject();
+
+                        Label label = new Label("key", prop.getName() == null
+                                        ? prop.getId()
+                                        : prop.getName());
+                        item.add(label);
+
+                        FieldPanel field;
+                        switch (prop.getType()) {
+                            case Boolean:
+                                field = new AjaxDropDownChoicePanel("value", label.getDefaultModelObjectAsString(),
+                                        new Model<Boolean>(Boolean.valueOf(prop.getValue()))).setChoices(Arrays.asList(
+                                                new String[] { "Yes", "No" }));
+                                break;
+
+                            case Date:
+                                SimpleDateFormat df = StringUtils.isNotBlank(prop.getDatePattern())
+                                        ? new SimpleDateFormat(prop.getDatePattern())
+                                        : new SimpleDateFormat();
+                                Date parsedDate = null;
+                                if (StringUtils.isNotBlank(prop.getValue())) {
+                                    try {
+                                        parsedDate = df.parse(prop.getValue());
+                                    } catch (ParseException e) {
+                                        LOG.error("Unparsable date: {}", prop.getValue(), e);
+                                    }
+                                }
+
+                                field = new DateTimeFieldPanel("value", label.getDefaultModelObjectAsString(),
+                                        new Model<Date>(parsedDate), df.toLocalizedPattern());
+                                break;
+
+                            case Enum:
+                                MapChoiceRenderer<String, String> enumCR =
+                                new MapChoiceRenderer<String, String>(prop.getEnumValues());
+
+                                field = new AjaxDropDownChoicePanel("value", label.getDefaultModelObjectAsString(),
+                                        new Model(prop.getValue())).setChoiceRenderer(enumCR).setChoices(new Model() {
+
+                                    private static final long serialVersionUID = -858521070366432018L;
+
+                                    @Override
+                                    public Serializable getObject() {
+                                        return new ArrayList<String>(prop.getEnumValues().keySet());
+                                    }
+                                });
+                                break;
+
+                            case Long:
+                                field = new SpinnerFieldPanel<Long>("value", label.getDefaultModelObjectAsString(),
+                                        Long.class, new Model<Long>(NumberUtils.toLong(prop.getValue())),
+                                        null, null);
+                                break;
+
+                            case String:
+                            default:
+                                field = new AjaxTextFieldPanel("value", PARENT_PATH,
+                                        new Model<String>(prop.getValue()));
+                                break;
+                        }
+
+                        field.setReadOnly(!prop.isWritable());
+                        if (prop.isRequired()) {
+                            field.addRequiredLabel();
+                        }
+
+                        item.add(field);
+                    }
+                };
+
+        final AjaxButton userDetails = new IndicatingAjaxButton("userDetails",
+                new Model<String>(getString("userDetails"))) {
+
+                    private static final long serialVersionUID = -4804368561204623354L;
+
+                    @Override
+                    protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) {
+                        editUserWin.setPageCreator(new ModalWindow.PageCreator() {
+
+                            private static final long serialVersionUID = -7834632442532690940L;
+
+                            @Override
+                            public Page createPage() {
+                                return new ViewUserModalPage(ApprovalModalPage.this.getPageReference(), editUserWin,
+                                        userRestClient.read(formTO.getUserKey())) {
+
+                                    private static final long serialVersionUID = -2819994749866481607L;
+
+                                    @Override
+                                    protected void closeAction(final AjaxRequestTarget target, final Form form) {
+                                        setResponsePage(ApprovalModalPage.this);
+                                    }
+                                };
+                            }
+                        });
+
+                        editUserWin.show(target);
+                    }
+                };
+        MetaDataRoleAuthorizationStrategy.authorize(userDetails, ENABLE,
+                xmlRolesReader.getEntitlement("Users", "read"));
+
+        final AjaxButton submit = new IndicatingAjaxButton(APPLY, new Model<String>(getString(SUBMIT))) {
+
+            private static final long serialVersionUID = -958724007591692537L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) {
+
+                Map<String, WorkflowFormPropertyTO> props = formTO.getPropertyMap();
+
+                for (int i = 0; i < propView.size(); i++) {
+                    @SuppressWarnings("unchecked")
+                    ListItem<WorkflowFormPropertyTO> item = (ListItem<WorkflowFormPropertyTO>) propView.get(i);
+                    String input = ((FieldPanel) item.get("value")).getField().getInput();
+
+                    if (!props.containsKey(item.getModelObject().getId())) {
+                        props.put(item.getModelObject().getId(), new WorkflowFormPropertyTO());
+                    }
+
+                    if (item.getModelObject().isWritable()) {
+                        switch (item.getModelObject().getType()) {
+                            case Boolean:
+                                props.get(item.getModelObject().getId()).setValue(String.valueOf("0".equals(input)));
+                                break;
+
+                            case Date:
+                            case Enum:
+                            case String:
+                            case Long:
+                            default:
+                                props.get(item.getModelObject().getId()).setValue(input);
+                                break;
+                        }
+                    }
+                }
+
+                formTO.getProperties().clear();
+                formTO.getProperties().addAll(props.values());
+                try {
+                    restClient.submitForm(formTO);
+
+                    ((Todo) pageRef.getPage()).setModalResult(true);
+                    window.close(target);
+                } catch (SyncopeClientException e) {
+                    error(getString(Constants.ERROR) + ": " + e.getMessage());
+                    LOG.error("While submitting form {}", formTO, e);
+                    feedbackPanel.refresh(target);
+                }
+            }
+
+            @Override
+            protected void onError(final AjaxRequestTarget target, final Form<?> form) {
+                feedbackPanel.refresh(target);
+            }
+        };
+
+        final AjaxButton cancel = new IndicatingAjaxButton(CANCEL, new ResourceModel(CANCEL)) {
+
+            private static final long serialVersionUID = -958724007591692537L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target, final Form form) {
+                window.close(target);
+            }
+
+            @Override
+            protected void onError(final AjaxRequestTarget target, final Form form) {
+                // nothing
+            }
+        };
+
+        cancel.setDefaultFormProcessing(false);
+
+        Form form = new Form(FORM);
+        form.add(propView);
+        form.add(userDetails);
+        form.add(submit);
+        form.add(cancel);
+
+        MetaDataRoleAuthorizationStrategy.authorize(form, ENABLE, xmlRolesReader.getEntitlement("Approval",
+                SUBMIT));
+
+        editUserWin = new ModalWindow("editUserWin");
+        editUserWin.setCssClassName(ModalWindow.CSS_CLASS_GRAY);
+        editUserWin.setInitialHeight(USER_WIN_HEIGHT);
+        editUserWin.setInitialWidth(USER_WIN_WIDTH);
+        editUserWin.setCookieName("edit-user-modal");
+        add(editUserWin);
+
+        add(form);
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/BaseModalPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/BaseModalPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/BaseModalPage.java
new file mode 100644
index 0000000..20892dd
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/BaseModalPage.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import org.apache.syncope.client.console.commons.CloseOnESCBehavior;
+
+/**
+ * Syncope Modal Window.
+ */
+public abstract class BaseModalPage extends AbstractBasePage {
+
+    private static final long serialVersionUID = -1443079028368471943L;
+
+    public BaseModalPage() {
+        super();
+
+        add(new CloseOnESCBehavior("keyup"));
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
new file mode 100644
index 0000000..2540c37
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import org.apache.syncope.client.console.SyncopeApplication;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.IAjaxIndicatorAware;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+/**
+ * Syncope Wicket base-page.
+ */
+public class BasePage extends AbstractBasePage implements IAjaxIndicatorAware {
+
+    private static final long serialVersionUID = 1571997737305598502L;
+
+    public BasePage() {
+        this(null);
+    }
+
+    public BasePage(final PageParameters parameters) {
+        super(parameters);
+
+        pageSetup();
+    }
+
+    private void pageSetup() {
+        ((SyncopeApplication) getApplication()).setupNavigationPanel(this, xmlRolesReader, true);
+
+        final String kind = getClass().getSimpleName().toLowerCase();
+        final BookmarkablePageLink kindLink = (BookmarkablePageLink) get(kind);
+        if (kindLink != null) {
+            kindLink.add(new Behavior() {
+
+                private static final long serialVersionUID = 1469628524240283489L;
+
+                @Override
+                public void onComponentTag(final Component component, final ComponentTag tag) {
+                    tag.put("class", kind);
+                }
+            });
+
+            Component kindIcon = kindLink.get(0);
+            if (kindIcon != null) {
+                kindIcon.add(new Behavior() {
+
+                    private static final long serialVersionUID = 1469628524240283489L;
+
+                    @Override
+                    public void onComponentTag(final Component component, final ComponentTag tag) {
+                        tag.put("src", "../.." + SyncopeApplication.IMG_PREFIX + kind + Constants.PNG_EXT);
+                    }
+                });
+            }
+        }
+
+        ((SyncopeApplication) getApplication()).setupEditProfileModal(this, userSelfRestClient);
+    }
+
+    @Override
+    public String getAjaxIndicatorMarkupId() {
+        return "veil";
+    }
+
+    /**
+     * Set a WindowClosedCallback for a ModalWindow instance.
+     *
+     * @param window window
+     * @param container container
+     */
+    protected void setWindowClosedCallback(final ModalWindow window, final WebMarkupContainer container) {
+
+        window.setWindowClosedCallback(new ModalWindow.WindowClosedCallback() {
+
+            private static final long serialVersionUID = 8804221891699487139L;
+
+            @Override
+            public void onClose(final AjaxRequestTarget target) {
+                target.add(container);
+                if (isModalResult()) {
+                    info(getString(Constants.OPERATION_SUCCEEDED));
+                    feedbackPanel.refresh(target);
+                    setModalResult(false);
+                }
+            }
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/BasePopupPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/BasePopupPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/BasePopupPage.java
new file mode 100644
index 0000000..856f469
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/BasePopupPage.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+public class BasePopupPage extends AbstractBasePage {
+
+    private static final long serialVersionUID = -2633667311332659505L;
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/BulkActionModalPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/BulkActionModalPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/BulkActionModalPage.java
new file mode 100644
index 0000000..95e514b
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/BulkActionModalPage.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.rest.BaseRestClient;
+import org.apache.syncope.client.console.wicket.ajax.markup.html.ClearIndicatingAjaxButton;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLinksPanel;
+import org.apache.syncope.common.lib.to.BulkAction;
+import org.apache.syncope.common.lib.to.BulkActionResult;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
+import org.apache.wicket.extensions.ajax.markup.html.repeater.data.table.AjaxFallbackDefaultDataTable;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
+import org.springframework.beans.BeanUtils;
+
+public class BulkActionModalPage<T, S> extends BaseModalPage {
+
+    private static final long serialVersionUID = 4114026480146090962L;
+
+    public BulkActionModalPage(
+            final ModalWindow window,
+            final Collection<T> items,
+            final List<IColumn<T, S>> columns,
+            final Collection<ActionLink.ActionType> actions,
+            final BaseRestClient bulkActionExecutor,
+            final String idFieldName,
+            final String pageId) {
+
+        super();
+
+        final SortableDataProvider<T, S> dataProvider = new SortableDataProvider<T, S>() {
+
+            private static final long serialVersionUID = 5291903859908641954L;
+
+            @Override
+            public Iterator<? extends T> iterator(final long first, final long count) {
+                return items.iterator();
+            }
+
+            @Override
+            public long size() {
+                return items.size();
+            }
+
+            @Override
+            public IModel<T> model(final T object) {
+                return new CompoundPropertyModel<>(object);
+            }
+        };
+
+        add(new AjaxFallbackDefaultDataTable<>(
+                "selectedObjects",
+                new ArrayList<>(columns.subList(1, columns.size() - 1)),
+                dataProvider,
+                Integer.MAX_VALUE).setVisible(items != null && !items.isEmpty()));
+
+        @SuppressWarnings("rawtypes")
+        final ActionLinksPanel actionPanel = new ActionLinksPanel("actions", new Model(), getPageReference());
+        add(actionPanel);
+
+        for (ActionLink.ActionType action : actions) {
+            final BulkAction bulkAction = new BulkAction();
+            for (T item : items) {
+                try {
+                    bulkAction.getTargets().add(getTargetId(item, idFieldName).toString());
+                } catch (Exception e) {
+                    LOG.error("Error retrieving item id {}", idFieldName, e);
+                }
+            }
+
+            switch (action) {
+                case DELETE:
+                    bulkAction.setOperation(BulkAction.Type.DELETE);
+                    break;
+                case SUSPEND:
+                    bulkAction.setOperation(BulkAction.Type.SUSPEND);
+                    break;
+                case REACTIVATE:
+                    bulkAction.setOperation(BulkAction.Type.REACTIVATE);
+                    break;
+                case EXECUTE:
+                    bulkAction.setOperation(BulkAction.Type.EXECUTE);
+                    break;
+                case DRYRUN:
+                    bulkAction.setOperation(BulkAction.Type.DRYRUN);
+                    break;
+                default:
+                    LOG.error("Bulk action type not supported");
+            }
+
+            actionPanel.add(new ActionLink() {
+
+                private static final long serialVersionUID = -3722207913631435501L;
+
+                @Override
+                public void onClick(final AjaxRequestTarget target) {
+                    try {
+                        final BulkActionResult res = (BulkActionResult) bulkActionExecutor.getClass().
+                                getMethod("bulkAction", BulkAction.class).invoke(bulkActionExecutor, bulkAction);
+
+                        setResponsePage(new BulkActionResultModalPage<>(window, items, columns, res, idFieldName));
+                    } catch (Exception e) {
+                        error(getString(Constants.ERROR)
+                                + ": Operation " + bulkAction.getOperation() + " not supported");
+                        feedbackPanel.refresh(target);
+                    }
+
+                }
+            }, action, pageId, !items.isEmpty());
+        }
+
+        final Form<Void> form = new Form<>(FORM);
+        add(form);
+
+        final AjaxButton cancel =
+                new ClearIndicatingAjaxButton(CANCEL, new ResourceModel(CANCEL), getPageReference()) {
+
+                    private static final long serialVersionUID = -958724007591692537L;
+
+                    @Override
+                    protected void onSubmitInternal(final AjaxRequestTarget target, final Form<?> form) {
+                        window.close(target);
+                    }
+                };
+
+        cancel.setDefaultFormProcessing(false);
+        form.add(cancel);
+    }
+
+    private Object getTargetId(final Object target, final String idFieldName)
+            throws IllegalAccessException, InvocationTargetException {
+
+        return BeanUtils.getPropertyDescriptor(target.getClass(), idFieldName).
+                getReadMethod().invoke(target, new Object[0]);
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/2d194636/client/console/src/main/java/org/apache/syncope/client/console/pages/BulkActionResultModalPage.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/pages/BulkActionResultModalPage.java b/client/console/src/main/java/org/apache/syncope/client/console/pages/BulkActionResultModalPage.java
new file mode 100644
index 0000000..4ada6ba
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/pages/BulkActionResultModalPage.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.pages;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.ActionResultColumn;
+import org.apache.syncope.common.lib.to.BulkActionResult;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxLink;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
+import org.apache.wicket.extensions.ajax.markup.html.repeater.data.table.AjaxFallbackDefaultDataTable;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+
+/**
+ * Show user or role status after performing a successful operation.
+ */
+public class BulkActionResultModalPage<T, S> extends BaseModalPage {
+
+    /**
+     * Serial version id.
+     */
+    private static final long serialVersionUID = 2646115294319713724L;
+
+    public BulkActionResultModalPage(
+            final ModalWindow window,
+            final Collection<T> items,
+            final List<IColumn<T, S>> columns,
+            final BulkActionResult results,
+            final String idFieldName) {
+
+        super();
+
+        final List<IColumn<T, S>> newColumnList = new ArrayList<>(columns.subList(1, columns.size() - 1));
+        newColumnList.add(newColumnList.size(), new ActionResultColumn<T, S>(results, idFieldName));
+
+        final SortableDataProvider<T, S> dataProvider = new SortableDataProvider<T, S>() {
+
+            private static final long serialVersionUID = 5291903859908641954L;
+
+            @Override
+            public Iterator<? extends T> iterator(final long first, final long count) {
+                return items.iterator();
+            }
+
+            @Override
+            public long size() {
+                return items.size();
+            }
+
+            @Override
+            public IModel<T> model(final T object) {
+                return new CompoundPropertyModel<T>(object);
+            }
+        };
+
+        add(new AjaxFallbackDefaultDataTable<T, S>(
+                "selectedObjects",
+                newColumnList,
+                dataProvider,
+                Integer.MAX_VALUE).setVisible(items != null && !items.isEmpty()));
+
+        final AjaxLink<Void> close = new IndicatingAjaxLink<Void>("close") {
+
+            private static final long serialVersionUID = -7978723352517770644L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target) {
+                window.close(target);
+            }
+        };
+
+        add(close);
+    }
+}