/*
* Copyright (c) 2010-2016 Evolveum
*
* Licensed 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 com.evolveum.midpoint.web.component.progress;
import com.evolveum.midpoint.model.api.*;
import com.evolveum.midpoint.model.api.context.ModelContext;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.security.api.SecurityEnforcer;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.exception.CommunicationException;
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.util.exception.ExpressionEvaluationException;
import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.PolicyViolationException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.SecurityViolationException;
import com.evolveum.midpoint.util.logging.LoggingUtils;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.web.component.AjaxSubmitButton;
import com.evolveum.midpoint.web.page.admin.users.DefaultGuiProgressListener;
import com.evolveum.midpoint.web.security.WebApplicationConfiguration;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.AjaxSelfUpdatingTimerBehavior;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.time.Duration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
/**
* Puts together all objects necessary for managing progress reporting and abort functionality.
* Provides a facade so that this functionality can be easily used from within relevant wicket pages
* (edit user, org, role, ...).
*
* An instance of this class has to be created for each progress reporting case - e.g. at least
* one for each instance of relevant user/org/role page.
*
* @author mederly
*/
public class ProgressReporter implements Serializable {
private static final Trace LOGGER = TraceManager.getTrace(ProgressReporter.class);
// links to wicket artefacts on parent page
private AjaxSubmitButton abortButton;
private AjaxSubmitButton backButton;
private AjaxSubmitButton continueEditingButton;
private ProgressReportingAwarePage parentPage;
private ProgressPanel progressPanel;
private AjaxSelfUpdatingTimerBehavior refreshingBehavior = null; // behavior is attached to the progress panel
// items related to asynchronously executed operation
private OperationResult asyncOperationResult; // Operation result got from the asynchronous operation (null if async op not yet finished)
private transient Thread asyncExecutionThread; // Thread in which async op is executing
private DefaultGuiProgressListener progressListener; // Listener created to receive events from the model
// TODO generalize to allow more kinds of GUI progress listeners
// configuration properties
private int refreshInterval;
private boolean asynchronousExecution;
private boolean abortEnabled;
private ModelContext<? extends ObjectType> previewResult; // Temporary - TODO rethink this...
public ProgressPanel getProgressPanel() {
return progressPanel;
}
/**
* Creates and initializes a progress reporter instance. Should be called during initialization
* of respective wicket page.
*
* @param parentPage The parent page (user, org, role, ...)
* @param id Wicket ID of the progress panel
* @return Progress reporter instance
*/
public static ProgressReporter create(String id, ProgressReportingAwarePage parentPage) {
ProgressReporter reporter = new ProgressReporter();
reporter.progressPanel = new ProgressPanel(id, new Model<>(new ProgressDto()), reporter, parentPage);
reporter.progressPanel.setOutputMarkupId(true);
reporter.progressPanel.hide();
WebApplicationConfiguration config = parentPage.getWebApplicationConfiguration();
reporter.refreshInterval = config.getProgressRefreshInterval();
reporter.asynchronousExecution = config.isProgressReportingEnabled();
reporter.abortEnabled = config.isAbortEnabled();
reporter.parentPage = parentPage;
return reporter;
}
// ===================== Dealing with the SAVE button =======================
/**
* Should be called when "save" button is submitted.
* In future it could encapsulate auxiliary functionality that has to be invoked before starting the operation.
* Parent page is then responsible for the preparation of the operation and calling the executeChanges method below.
*/
public void onSaveSubmit() {
}
/**
* Executes changes on behalf of the parent page. By default, changes are executed asynchronously (in
* a separate thread). However, when set in the midpoint configuration, changes are executed synchronously.
*
* @param deltas Deltas to be executed.
* @param options Model execution options.
* @param task Task in context of which the changes have to be executed.
* @param result Operation result.
* @param target AjaxRequestTarget into which any synchronous changes are signalized.
*/
public void executeChanges(final Collection<ObjectDelta<? extends ObjectType>> deltas,
final boolean previewOnly, final ModelExecuteOptions options, final Task task, final OperationResult result, AjaxRequestTarget target) {
parentPage.startProcessing(target, result);
ModelService modelService = parentPage.getModelService();
ModelInteractionService modelInteractionService = parentPage.getModelInteractionService();
if (asynchronousExecution) {
executeChangesAsync(deltas, previewOnly, options, task, result, target, modelService, modelInteractionService);
} else {
executeChangesSync(deltas, previewOnly, options, task, result, target, modelService, modelInteractionService);
}
}
private void executeChangesSync(Collection<ObjectDelta<? extends ObjectType>> deltas, boolean previewOnly, ModelExecuteOptions options, Task task,
OperationResult result, AjaxRequestTarget target, ModelService modelService, ModelInteractionService modelInteractionService) {
try {
if (previewOnly) {
previewResult = modelInteractionService.previewChanges(deltas, options, task, result);
} else {
modelService.executeChanges(deltas, options, task, result);
}
result.computeStatusIfUnknown();
} catch (CommunicationException |ObjectAlreadyExistsException |ExpressionEvaluationException |
PolicyViolationException |SchemaException |SecurityViolationException |
ConfigurationException |ObjectNotFoundException |RuntimeException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Error executing changes", e);
if (!result.isFatalError()) { // just to be sure the exception is recorded into the result
result.recordFatalError(e.getMessage(), e);
}
}
parentPage.finishProcessing(target, result, false);
}
private void executeChangesAsync(final Collection<ObjectDelta<? extends ObjectType>> deltas, final boolean previewOnly,
final ModelExecuteOptions options, final Task task, final OperationResult result, AjaxRequestTarget target,
final ModelService modelService, final ModelInteractionService modelInteractionService) {
final SecurityEnforcer enforcer = parentPage.getSecurityEnforcer();
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
asyncOperationResult = null;
clearProgressPanel();
startRefreshingProgressPanel(target);
showProgressPanel();
progressPanel.setTask(task);
progressListener = new DefaultGuiProgressListener(parentPage, progressPanel.getModelObject());
Runnable execution = () -> {
try {
enforcer.setupPreAuthenticatedSecurityContext(authentication);
progressPanel.recordExecutionStart();
if (previewOnly) {
previewResult = modelInteractionService.previewChanges(deltas, options, task, Collections.singleton(progressListener), result);
} else {
modelService.executeChanges(deltas, options, task, Collections.singleton(progressListener), result);
}
} catch (CommunicationException|ObjectAlreadyExistsException|ExpressionEvaluationException|
PolicyViolationException|SchemaException|SecurityViolationException|
ConfigurationException|ObjectNotFoundException|RuntimeException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Error executing changes", e);
if (!result.isFatalError()) { // just to be sure the exception is recorded into the result
result.recordFatalError(e.getMessage(), e);
}
}
progressPanel.recordExecutionStop();
asyncOperationResult = result; // signals that the operation has finished
};
if (abortEnabled) {
showAbortButton(target);
}
showBackButton(target);
result.recordInProgress(); // to disable showing not-final results (why does it work? and why is the result shown otherwise?)
asyncExecutionThread = new Thread(execution);
asyncExecutionThread.start();
}
private void startRefreshingProgressPanel(AjaxRequestTarget target) {
if (refreshingBehavior == null) { // i.e. refreshing behavior has not been set yet
refreshingBehavior = new AjaxSelfUpdatingTimerBehavior(Duration.milliseconds(refreshInterval)) {
@Override
protected void onPostProcessTarget(AjaxRequestTarget target) {
super.onPostProcessTarget(target);
if (progressPanel != null) {
progressPanel.invalidateCache();
}
if (asyncOperationResult != null) { // by checking this we know that async operation has been finished
asyncOperationResult.recomputeStatus(); // because we set it to in-progress
stopRefreshingProgressPanel(target);
parentPage.finishProcessing(target, asyncOperationResult, true);
asyncOperationResult = null;
}
}
@Override
public boolean isEnabled(Component component) {
return component != null;
}
};
progressPanel.add(refreshingBehavior);
target.add(progressPanel);
}
}
private void stopRefreshingProgressPanel(AjaxRequestTarget target) {
if (refreshingBehavior != null) {
refreshingBehavior.stop(target);
// We cannot remove the behavior, as it would cause NPE because of component == null (since wicket 7.5)
//progressPanel.remove(refreshingBehavior);
refreshingBehavior = null; // causes re-adding this behavior when re-saving changes
}
}
// =================== Dealing with the "Abort" button ========================
public void registerAbortButton(AjaxSubmitButton abortButton) {
abortButton.setOutputMarkupId(true);
abortButton.setOutputMarkupPlaceholderTag(true);
abortButton.setVisible(false);
this.abortButton = abortButton;
}
public void registerBackButton(AjaxSubmitButton backButton) {
backButton.setOutputMarkupId(true);
backButton.setOutputMarkupPlaceholderTag(true);
backButton.setVisible(false);
this.backButton = backButton;
}
public void registerContinueEditingButton(AjaxSubmitButton continueEditingButton) {
continueEditingButton.setOutputMarkupId(true);
continueEditingButton.setOutputMarkupPlaceholderTag(true);
continueEditingButton.setVisible(false);
this.continueEditingButton = continueEditingButton;
}
/**
* You have to call this method when Abort button is pressed
*/
public void onAbortSubmit(AjaxRequestTarget target) {
if (progressListener == null) {
LOGGER.error("No progressListener (abortButton.onSubmit)");
return; // should not occur
}
progressListener.setAbortRequested(true);
if (asyncExecutionThread != null) {
if (asyncExecutionThread.isAlive()) {
progressPanel.getModelObject().log("Abort requested, please wait..."); // todo i18n
asyncExecutionThread.interrupt();
} else {
progressPanel.getModelObject().log("Abort requested, but the execution seems to be already finished."); // todo i18n
}
} else {
progressPanel.getModelObject().log("Abort requested, please wait... (note: couldn't interrupt the thread)"); // todo i18n
}
hideAbortButton(target);
}
public void hideAbortButton(AjaxRequestTarget target) {
abortButton.setVisible(false);
target.add(abortButton);
}
public void showAbortButton(AjaxRequestTarget target) {
abortButton.setVisible(true);
target.add(abortButton);
}
public void hideBackButton(AjaxRequestTarget target) {
backButton.setVisible(false);
target.add(backButton);
}
public void hideContinueEditingButton(AjaxRequestTarget target) {
continueEditingButton.setVisible(false);
target.add(continueEditingButton);
}
public void showBackButton(AjaxRequestTarget target) {
backButton.setVisible(true);
target.add(backButton);
}
public void showContinueEditingButton(AjaxRequestTarget target) {
continueEditingButton.setVisible(true);
target.add(continueEditingButton);
}
// ================= Other methods =================
public boolean isAllSuccess() {
return progressPanel.getModelObject().allSuccess();
}
public void showProgressPanel() {
if (progressPanel != null) {
progressPanel.show();
}
}
public void hideProgressPanel() {
if (progressPanel != null) {
progressPanel.hide();
}
}
public void clearProgressPanel() {
progressPanel.getModelObject().clear();
}
public ModelContext<? extends ObjectType> getPreviewResult() {
return previewResult;
}
}