/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico de Galicia
* Copyright (C) 2010-2011 Igalia, S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.zkoss.ganttz.util;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.lang3.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.util.Clients;
/**
* Handler of long operations ( {@link Clients#showBusy(String)}, {@link Clients#clearBusy()} ).
*
* @author Óscar González Fernández <ogonzalez@igalia.com>
*/
public class LongOperationFeedback {
private static final Log LOG = LogFactory.getLog(LongOperationFeedback.class);
public interface ILongOperation {
void doAction() throws Exception;
String getName();
}
private static final ThreadLocal<Boolean> alreadyInside = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return false;
}
};
public static void execute(final Component component,
final ILongOperation longOperation) {
Validate.notNull(component);
Validate.notNull(longOperation);
if (alreadyInside.get()) {
dispatchActionDirectly(longOperation);
return;
}
Clients.showBusy(longOperation.getName());
executeLater(component, new Runnable() {
public void run() {
try {
alreadyInside.set(true);
longOperation.doAction();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
alreadyInside.remove();
Clients.clearBusy();
}
}
});
}
public static void executeLater(final Component component,
final Runnable runnable) {
Validate.notNull(runnable);
Validate.notNull(component);
final String eventName = generateEventName();
component.addEventListener(eventName, new EventListener() {
@Override
public void onEvent(Event event) {
try {
runnable.run();
} finally {
Clients.clearBusy();
component.removeEventListener(eventName, this);
}
}
});
Events.echoEvent(eventName, component, null);
}
private static void dispatchActionDirectly(final ILongOperation longOperation) {
try {
longOperation.doAction();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static String generateEventName() {
return "onLater";
}
private LongOperationFeedback() {
}
public interface IDesktopUpdatesEmitter<T> {
void doUpdate(T value);
}
public interface IDesktopUpdate {
void doUpdate();
}
public static IDesktopUpdate and(final IDesktopUpdate... desktopUpdates) {
return () -> {
for (IDesktopUpdate each : desktopUpdates) {
each.doUpdate();
}
};
}
public interface IBackGroundOperation<T> {
void doOperation(IDesktopUpdatesEmitter<T> desktopUpdateEmitter);
}
private static final ExecutorService executor = Executors.newCachedThreadPool();
public static <T> IDesktopUpdatesEmitter<T> doNothingEmitter() {
return value -> {};
}
/**
* Executes a long operation.
* The background operation can send {@link IDesktopUpdate} objects that can update desktop state.
* Trying to update the components in any other way would fail.
*/
public static void progressive(final Desktop desktop,
final IBackGroundOperation<IDesktopUpdate> operation) {
progressive(desktop, operation, (update) -> update.doUpdate());
}
/**
* Executes a long operation.
* The background operation can send
* <code>T</code> objects that can update desktop state.
* A {@link IDesktopUpdatesEmitter} that handle these objects is necessary.
* Trying to update the components in any other way would fail.
*/
public static <T> void progressive(final Desktop desktop,
final IBackGroundOperation<T> operation,
final IDesktopUpdatesEmitter<T> emitter) {
desktop.enableServerPush(true);
executor.execute(() -> {
try {
IBackGroundOperation<T> operationWithAsyncUpates = withAsyncUpates(operation, desktop);
operationWithAsyncUpates.doOperation(emitter);
} catch (Exception e) {
LOG.error("error executing background operation", e);
} finally {
desktop.enableServerPush(false);
}
});
}
private static <T> IBackGroundOperation<T> withAsyncUpates(
final IBackGroundOperation<T> backgroundOperation,
final Desktop desktop) {
return new IBackGroundOperation<T>() {
@Override
public void doOperation(IDesktopUpdatesEmitter<T> originalEmitter) {
NotBlockingDesktopUpdates<T> notBlockingDesktopUpdates =
new NotBlockingDesktopUpdates<>(desktop, originalEmitter);
Future<?> future = executor.submit(notBlockingDesktopUpdates);
try {
backgroundOperation.doOperation(notBlockingDesktopUpdates);
} finally {
notBlockingDesktopUpdates.finish();
waitUntilShowingAllUpdates(future);
}
}
private void waitUntilShowingAllUpdates(Future<?> future) {
try {
future.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
}
private static class NotBlockingDesktopUpdates<T> implements IDesktopUpdatesEmitter<T>, Runnable {
private BlockingQueue<EndOrValue<T>> queue = new LinkedBlockingQueue<>();
private final IDesktopUpdatesEmitter<T> original;
private final Desktop desktop;
NotBlockingDesktopUpdates(Desktop desktop, IDesktopUpdatesEmitter<T> original) {
this.original = original;
this.desktop = desktop;
}
@Override
public void doUpdate(T value) {
queue.add(EndOrValue.value(value));
}
void finish() {
queue.add(EndOrValue.end());
}
@Override
public void run() {
List<T> batch = new ArrayList<>();
while (true) {
batch.clear();
EndOrValue<T> current;
try {
current = queue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (current.isEnd())
return;
if (!desktop.isAlive() || !desktop.isServerPushEnabled())
return;
try {
Executions.activate(desktop);
} catch (Exception e) {
LOG.error("unable to access desktop", e);
throw new RuntimeException(e);
}
try {
original.doUpdate(current.getValue());
while ((current = queue.poll()) != null) {
if (current.isEnd()) {
break;
}
batch.add(current.getValue());
original.doUpdate(current.getValue());
}
} finally {
Executions.deactivate(desktop);
}
if (current != null && current.isEnd()) {
return;
}
}
}
}
private abstract static class EndOrValue<T> {
public static <T> EndOrValue<T> end() {
return new End<>();
}
public static <T> EndOrValue<T> value(T value) {
return new Value<>(value);
}
public abstract boolean isEnd();
public abstract T getValue() throws UnsupportedOperationException;
}
private static class Value<T> extends EndOrValue<T> {
private final T value;
Value(T value) {
Validate.notNull(value);
this.value = value;
}
public T getValue() {
return value;
}
@Override
public boolean isEnd() {
return false;
}
}
private static class End<T> extends EndOrValue<T> {
@Override
public T getValue() throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
@Override
public boolean isEnd() {
return true;
}
}
}