package org.ovirt.engine.ui.frontend.communication; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import com.google.gwt.core.client.Scheduler; import com.google.inject.Inject; /** * This class processes the available operations and dispatches them to the appropriate communications * provider. */ public class OperationProcessor { /** * Retry threshold. */ private static final int RETRY_THRESHOLD = 5; /** * The communications provider. */ private final CommunicationProvider communicationProvider; /** * The collection of pending operations. */ private final Collection<VdcOperation<?, ?>> pending; /** * GWT scheduler. */ private Scheduler scheduler; /** * Constructor for {@code OperationProcess}. * @param commProvider The communication provider that communicates with the back-end. */ @Inject public OperationProcessor(final CommunicationProvider commProvider) { this.communicationProvider = commProvider; this.pending = new ArrayList<>(); } /** * Process any available operations. * @param manager The operation manager. */ public void processOperation(final VdcOperationManager manager) { if (scheduler == null) { scheduler = Scheduler.get(); } scheduler.scheduleDeferred(() -> processAvailableOperations(manager)); } /** * Setter for scheduler. * @param sched The new scheduler. */ public void setScheduler(final Scheduler sched) { this.scheduler = sched; } /** * Process any available operations. Due to this being a deferred operation, we will be able to combine * several operations into a single one. For example, lets say that the queue has 4 operations in it. 3 * of those operations came from an operation list add, and 1 from a single add. The 3 operations will all * have the same callback object. The 4th operation will have a different callback. In essence the 4 operations * only have 2 call backs. * <p> * If the operations are all of the same type, we can merge all those operations into a single list of operations * that is handed to the communications provider. If the provider has a way of sending that list in a single * communications request we will have saved a round trip to the back end. In order to effectively handle error * conditions we will want to replace the call backs in the operations with our own so we can add behavior to * the call backs. When we replace those call backs with our own we need to make sure that we only create 2 * call backs and that they line up with the original call backs. * <p> * When the provider returns the results, we can then use our replaced call backs to handle errors or other things * and call the original call backs. * * @param manager The operations manager. */ @SuppressWarnings({ "rawtypes", "unchecked" }) void processAvailableOperations(final VdcOperationManager manager) { List<VdcOperation<?, ?>> operations = new ArrayList<>(); Map<VdcOperationCallback<VdcOperation<?, ?>, ?>, VdcOperationCallback<VdcOperation<?, ?>, ?>> usedCallbacks = new HashMap<>(); VdcOperation<?, ?> operation; while ((operation = manager.pollOperation()) != null) { if (!operation.allowDuplicates() && (pending.contains(operation) || operations.contains(operation))) { // Skip this one as the result is pending. continue; } // Check if the original callback from the operation has been replaced already. This happens only in // the case where the original operation is part of an operation list. VdcOperationCallback<?, ?> replacementCallback = usedCallbacks.get(operation.getCallback()); if (replacementCallback == null) { // Replace the original callback with our own callback. This allows us to manipulate the result // before calling the original callback. For instance we can implement retries on failure. // Only create a replacement callback if we have not encountered the original callback of the // operation before. Re-use existing wrapper callback from the usedCallback map. if (!(operation.getCallback() instanceof VdcOperationCallbackList)) { replacementCallback = createCallback(manager); } else { replacementCallback = createListCallback(manager); } usedCallbacks.put(operation.getCallback(), (VdcOperationCallback<VdcOperation<?, ?>, ?>) replacementCallback); } operations.add(new VdcOperation(operation, replacementCallback)); } // Mark the operations pending. addPending(operations); communicationProvider.transmitOperationList(operations); } /** * Create a callback object for an operation that expects a regular callback. This callback * is called by the provider instead of the original callback. We can then do what we want * and call the original callback if needed. * @param manager The {@code VdcOperationManager} that has the operation. * @return A VdcOperationCallback. */ @SuppressWarnings("unchecked") private VdcOperationCallback<?, ?> createCallback(final VdcOperationManager manager) { return new VdcOperationCallback<VdcOperation<?, ?>, Object>() { @Override public void onSuccess(final VdcOperation<?, ?> operation, final Object result) { // Do nothing more than call original callback. VdcOperation<?, ?> originalOperation = getOriginalOperation(operation); originalOperation.getCallback().onSuccess(originalOperation, result); removePending(operation); // Finished, check for more operations. processOperation(manager); } @Override public void onFailure(final VdcOperation<?, ?> operation, final Throwable exception) { // Remove pending, so it won't accidentally stop the re-add if possible. removePending(operation); // If the failure is recoverable, then add the request back into the queue. if (!operation.allowDuplicates() && operation.getCopyCount() < RETRY_THRESHOLD) { manager.addOperation(operation); } else { VdcOperation<?, ?> originalOperation = getOriginalOperation(operation); originalOperation.getCallback().onFailure(originalOperation, exception); } // Finished, check for more operations. processOperation(manager); } }; } /** * Create a callback object for an operation that expects a list callback. This callback * is called by the provider instead of the original callback. We can then do what we want * and call the original callback if needed. * @param manager The {@code VdcOperationManager} that has the operation. * @return A VdcOperationCallback for a list callback. */ @SuppressWarnings("unchecked") private VdcOperationCallback<?, ?> createListCallback(final VdcOperationManager manager) { return new VdcOperationCallbackList<VdcOperation<?, ?>, Object>() { @Override public void onSuccess(final List<VdcOperation<?, ?>> operationList, final Object result) { if (!operationList.isEmpty()) { // All the call-backs in the list are the same, just have to call one. VdcOperation<?, ?> originalOperation = getOriginalOperation(operationList.get(0)); VdcOperationCallback<List<VdcOperation<?, ?>>, Object> originalCallback = originalOperation.getCallback(); originalCallback.onSuccess(operationList, result); removePending(operationList); // Finished, check for more operations. processOperation(manager); } } @Override public void onFailure(final List<VdcOperation<?, ?>> operationList, final Throwable exception) { // If the failure is recoverable, then add the request back into the queue. removePending(operationList); VdcOperation<?, ?> originalOperation = getOriginalOperation(operationList.get(0)); // If the operation allows duplicates, it means we shouldn't retry the operation. if (!operationList.get(0).allowDuplicates() && operationList.get(0).getCopyCount() < RETRY_THRESHOLD) { manager.addOperationList(operationList); } else { VdcOperationCallbackList<VdcOperation<?, ?>, Object> originalCallback = (VdcOperationCallbackList<VdcOperation<?, ?>, Object>) originalOperation.getCallback(); originalCallback.onFailure(operationList, exception); } // Finished, check for more operations. processOperation(manager); } }; } /** * Add a list of operations to the pending list. * @param operations The list to add. */ private void addPending(final List<VdcOperation<?, ?>> operations) { pending.addAll(operations); } /** * Remove operations from pending list as the operations are completed. * @param operationList The list of operations to remove. */ private void removePending(final List<VdcOperation<?, ?>> operationList) { pending.removeAll(operationList); } /** * Remove operation from pending list as the operation completed. * @param operation The operation to remove. */ protected void removePending(final VdcOperation<?, ?> operation) { pending.remove(operation); } /** * Traverse the source operation tree to find the original one. * @param operation The operation to use to traverse. * @return The original operation. */ private VdcOperation<?, ?> getOriginalOperation(final VdcOperation<?, ?> operation) { // Get the original operation. VdcOperation<?, ?> originalOperation = operation; while (originalOperation.getSource() != null) { originalOperation = originalOperation.getSource(); } return originalOperation; } /** * Log out the user. * @param callback The callback to call when the operation is completed. */ public void logoutUser(final UserCallback<?> callback) { communicationProvider.logout(callback); } /** * Default store where the caller doesn't care about the result. * @param key The key * @param value The value to store. */ public void storeInHttpSession(final String key, final String value) { storeInHttpSession(key, value, new StorageCallback() { @Override public void onSuccess(String result) { // Do nothing } @Override public void onFailure(Throwable caught) { //Do nothing } }); } public void storeInHttpSession(final String key, final String value, final StorageCallback callback) { communicationProvider.storeInHttpSession(key, value, callback); } public void retrieveFromHttpSession(final String key, final StorageCallback callback) { communicationProvider.retrieveFromHttpSession(key, callback); } }