I have a JavaFX table which is ultimately with data received on a network thread.
Using Platform.runLater() to update the view-model is simple, but it does not fit into our architecture.
The current architecture separates applications into "view" and "network/comms" parts.
View listens to models and updates corresponding components. No knowledge of network.
Network listens to network updates and writes data into models accordingly. No knowledge of JavaFX.
So I'm in a dilemma.
To be true to the architecture and "separation of concerns" - the network reader class should not be calling Platform.runLater()
To keep it simple and have the network reader class call Platform.runLater() - just works - no additional code.
I've attempted to illustrate this in code
The simple approach
Just call Platform.runLater() from network reader
public class SimpleUpdate extends Application {
private int clock;
public class Item {
private IntegerProperty x = new SimpleIntegerProperty(0);
public final IntegerProperty xProperty() {
return this.x;
}
public final int getX() {
return this.xProperty().get();
}
public final void setX(final int x) {
this.xProperty().set(x);
}
}
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Item> viewModel = FXCollections.observableArrayList();
TableView<Item> table = new TableView<Item>(viewModel);
TableColumn<Item, Integer> colX = new TableColumn<>();
colX.setCellValueFactory(new PropertyValueFactory<Item, Integer>("x"));
table.getColumns().add(colX);
primaryStage.setScene(new Scene(table));
primaryStage.show();
new Thread(() -> {
while (true) {
Platform.runLater(() -> { // update on JavaFX thread
if (clock % 2 == 0) {
viewModel.add(new Item());
viewModel.add(new Item());
} else {
viewModel.remove(1);
}
for (Item each : viewModel) {
each.setX(each.getX() + 1);
}
clock++;
});
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Network update").start();
}
public static void main(String[] args) {
launch(args);
}
}
Pure approach
Network reader thread writes into its own model
View listener listens to network model and syncs its own view model using JavaFX thread
A lot more complicated
Code
public class PureUpdate extends Application {
private int clock;
public class Item {
private IntegerProperty x = new SimpleIntegerProperty(0);
public final IntegerProperty xProperty() {
return this.x;
}
public final int getX() {
return this.xProperty().get();
}
public final void setX(final int x) {
this.xProperty().set(x);
}
}
public class ViewItem extends Item {
private Item original;
public ViewItem(Item original) {
super();
this.original = original;
sync();
}
public void sync() {
setX(original.getX());
}
public Item getOriginal() {
return original;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<ViewItem> viewModel = FXCollections.observableArrayList();
TableView<ViewItem> table = new TableView<ViewItem>(viewModel);
TableColumn<ViewItem, Integer> colX = new TableColumn<>();
colX.setCellValueFactory(new PropertyValueFactory<ViewItem, Integer>("x"));
table.getColumns().add(colX);
primaryStage.setScene(new Scene(table));
primaryStage.show();
ObservableList<Item> networkModel = FXCollections
.synchronizedObservableList(FXCollections.observableArrayList());
networkModel.addListener((Observable obs) -> {
Platform.runLater(() -> {
List<Item> alreadyKnown = new ArrayList<>();
for (Iterator<ViewItem> it = viewModel.iterator(); it.hasNext();) {
ViewItem each = it.next();
alreadyKnown.add(each.getOriginal());
if (networkModel.contains(each.getOriginal())) {
each.sync();
} else {
it.remove();
}
}
for (Item each : networkModel.toArray(new Item[0])) {
if (!alreadyKnown.contains(each)) {
viewModel.add(new ViewItem(each));
}
}
});
});
new Thread(() -> {
while (true) {
if (clock % 2 == 0) {
networkModel.add(new Item());
networkModel.add(new Item());
} else {
networkModel.remove(1);
}
for (Item each : networkModel) {
each.setX(each.getX() + 1);
}
clock++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Network update").start();
}
public static void main(String[] args) {
launch(args);
}
}
Question. Can I achieve the pure approach without writing additional code?
Have the processor post notifications to a Runnable. or a Consumer<T>, or define a custom #FunctionalInterface.
This will make testing easier to as you design out threading and the dependency on JavaFX or any other framework needing synchronization on a specific thread.
Example using consumer:
public class NetworkReader {
private final Consumer<? super Data> consumer;
public NetworkReader(Consumer<? super Data> consumer) {
this.consumer = Objects.requireNonNull(consumer);
}
public void readStuff() {
while (...) {
Data data = ...;
consumer.accept(data);
}
}
}
The NetworkReader would be constructed with e.g. new NetworkReader(d -> Platform.runLater(() -> updateModel(d)));
When you want to test you could pass do as follows:
NetworkReader reader = new NetworkReader(d -> this.actuals = d);
reader.readStuff();
assertEquals(expecteds, actuals);
A smart consumer could coelesc updates until it has actually been processed.
Related
Issue: I've got a priority queue to process actions. When I instantiate the actions and then add them to the queue it works, however when I instantiate them directly as I add them to the queue it no longer retains the priority.
This works - Executes by priority
public static void main(String[] args) {
final Action<Void> low = new LowAction();
final Action<Void> med = new MedAction();
final Action<Integer> high = new HighAction();
final Action<Boolean> walk = new WalkAction();
final ActionScheduler scheduler = new ActionScheduler(1,10);
scheduler.queue(high);
scheduler.queue(walk);
scheduler.queue(low);
scheduler.queue(med);
}
This does not work - Executes in the order I called them
public static void main(String[] args) {
final ActionScheduler scheduler = new ActionScheduler(1,10);
scheduler.queue(new HighAction());
scheduler.queue(new WalkAction());
scheduler.queue(new LowAction());
scheduler.queue(new MedAction());
}
ActionScheduler class
public class ActionScheduler {
private ExecutorService priorityJobPoolExecutor;
private ExecutorService priorityJobScheduler
= Executors.newSingleThreadExecutor();
private PriorityBlockingQueue<Action<?>> priorityQueue;
private Future<?> result;
public ActionScheduler(Integer poolSize, Integer queueSize) {
priorityJobPoolExecutor = Executors.newFixedThreadPool(poolSize);
priorityQueue = new PriorityBlockingQueue<>(queueSize);
priorityJobScheduler.submit(() -> {
while (true) {
try {
result = priorityJobPoolExecutor.submit(priorityQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
}
public void schedule(Action<?> action) {
priorityQueue.offer(action);
}
public <T> Future<T> queue(Action<?> action) {
this.schedule(action);
return (Future<T>) result;
}
}
This also works correctly
public static void main(String[] args) {
final Action<Void> low = new LowAction();
final Action<Void> med = new MedAction();
final Action<Integer> high = new HighAction();
final Action<Boolean> walk = new WalkAction();
final ActionScheduler scheduler = new ActionScheduler(1,10);
scheduler.queue(new HighAction());
scheduler.queue(new WalkAction());
scheduler.queue(new LowAction());
scheduler.queue(new MedAction());
}
If anyone could offer any insight on why this is happening and how I can get it to execute by priority in both examples posted it would be greatly appreciated.
EDIT
Action Class
public abstract class Action<T> implements Callable<T>, Comparable<Action<?>> {
private final ActionContext context;
public Action(ActionContext context) {
this.context = context;
}
#Override
public int compareTo(Action action) {
if (action.getContext().getPriority() == this.getContext().getPriority()) {
return 0;
} else if (this.getContext().getPriority().ordinal() > action.getContext().getPriority().ordinal()) {
return -1;
} else {
return 1;
}
}
public ActionContext getContext() {
return context;
}
}
LowAction class
public class LowAction extends Action<Void> {
public LowAction() {
super(new ActionContext("low", Priority.LOW, true, false));
}
#Override
public Void call() throws Exception {
System.out.println("LOW");
return null;
}
}
There is one difference which I can see in the first approach you are binding your object with an actual type like final Action<**Void**> low = new LowAction(); on the other hand new LowAction() will be treat as a raw creation.
The internal working of the PriorityQueue is based on the Binary Heap.The elements of the priority queue are ordered according to the natural ordering, or by a comparator provided at construction time of the queue, depending on which constructor is used.
I want to create an method that fires up every time a new message is added to the groupchat arraylist.
Pseudo code:
public void listenForChange(){
while(true){
if(new message is added to the groupchat){
System.out.println(print the last added message);
}
}
}
What I have tried, but doesn't work:
public class Groupe{
ArrayList<String> groupechat;
int liveChange;
public void listenForChange() {
while(true){
if (groupchat.size() > liveChange){
liveChange= gruppenchat.size();
System.out.println(gruppenchat.get(liveChange-1));
}
}
}
Test class:
public class testGruppen extends Thread {
Gruppe gruppe;
public TestGroup(){
gruppe= new Gruppe("Monday");
}
public void run() {
System.out.println("listen");
gruppe.listenForChange();
}
public static void main(String[] args) {
testGruppen test = new testGruppen();
test.start();
test.gruppe.write("1"); // write just adds a new String to groupchat
test.gruppe.write("2");
test.gruppe.write("3");
test.gruppe.write("4");
}
}
Output: 4 instead of 1\n 2\n 3\n 4\n
What about using decorator:
public static void main(String... args) {
List<Integer> group = new FireEventListDecorator<>(new ArrayList<>());
for (int i = 0; i < 3; i++)
group.add(i);
}
public static class FireEventListDecorator<E> extends AbstractList<E> {
private final List<E> delegate;
public FireEventListDecorator(List<E> delegate) {
this.delegate = delegate;
}
#Override
public void add(int index, E element) {
delegate.add(index, element);
fireEvent(element);
}
#Override
public E get(int index) {
return delegate.get(index);
}
#Override
public int size() {
return delegate.size();
}
private void fireEvent(E element) {
System.out.println("add: " + element);
}
}
To avoid a CPU wasteful while (true) loop with polling, use a call-back method via an observer/listener pattern. One way to do this is to give your class that holds the ArrayList a PropertyChangeSupport instance, allow it to accept listeners, and then in the method that changes the ArrayList, notify listeners.
e.g.,
public class Group {
// property listened to: ADD_TEXT
public static final String ADD_TEXT = "add text";
// the support object
private PropertyChangeSupport support = new PropertyChangeSupport(this);
private List<String> chatText = new ArrayList<>();
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(ADD_TEXT, listener);
}
public void addText(String text) {
String oldValue = "";
String newValue = text;
chatText.add(text + "\n");
// notify listeners
support.firePropertyChange(ADD_TEXT, oldValue, newValue);
}
}
And then it can be used like so:
public class TestGroupChat {
public static void main(String[] args) {
final Group group = new Group();
group.addPropertyChangeListener(new GroupListener());
final String[] texts = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"};
new Thread(() -> {
for (String text : texts) {
group.addText(text);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {}
}
}) .start();
}
private static class GroupListener implements PropertyChangeListener {
#Override
public void propertyChange(PropertyChangeEvent evt) {
// call back method that is called any time the listened-to
// property has been changed
System.out.println("Notification: "+ evt.getNewValue());
}
}
}
You should take a look at LinkedBlockingQueue class.
This class is useful when you want to wake up a thread when a new element is added to a queue. In the example below, everytime you add a new message to the queue, the thread will print the message and wait for the next message.
public class Foo extends Thread {
LinkedBlockingQueue<String> messagesQueue;
public Foo(LinkedBlockingQueue<String> messagesQueue) {
this.messagesQueue = messagesQueue;
}
#Override
public voir run() {
while(true) {
String message = messagesQueue.take();
//The thread will sleep until there is a new message in the queue.
System.out.println(message);
}
}
}
I have an app where i have a TreeView which will have TreeItems holding a large number of leaf TreeItems. Having a huge number of TreeItems in the treeview hurts the performance of the app noticeably, to avoid that, what i will do, is i will allow only one non-leaf TreeItem to be expanded at a time, and once a TreeItem is folded, i will clear it's children, and load them asynchronously once needed (When the user expands the TreeItem).
The weird issue is, in this test below, when i first click the expand arrow on the treeitem, the children load fine, and if i fold it (which will clear children) and unfold it again, sometimes it works and others the program hogs and starts consuming 30% of the cpu for a couple of minutes then gets back running. What's weirder is that if i double click on the TreeItem to expand it (Not using the arrow) the hog starts right away, even at first program launch.
What could i be possibly doing wrong here?
PS:
Some of the code in the LazyTreeItem class is inspired by James_D's answer Here
I tried running the loadItems task on the fx thread(Not using the ItemLoader), but it didn't make any difference.
Same issue occurs using both JAVA 8 and JAVA 9
App.java
public class App extends Application {
private TreeView<Item> treeView = new TreeView<>();
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("TreeView Lazy Load");
primaryStage.setScene(new Scene(new StackPane(treeView), 300, 275));
initTreeView();
primaryStage.show();
}
private void initTreeView() {
treeView.setShowRoot(false);
treeView.setRoot(new TreeItem<>(null));
List<SingleItem> items = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
items.add(new SingleItem(String.valueOf(i)));
}
TreeItem<Item> parentItem = new TreeItem<>(new Item());
parentItem.getChildren().add(new LazyTreeItem(new MultipleItem(items)));
treeView.getRoot().getChildren().add(parentItem);
}
public static void main(String[] args) {
launch(args);
}
}
LazyTreeItem.java
public class LazyTreeItem extends TreeItem<Item> {
private boolean childrenLoaded = false;
private boolean isLoadingItems = false;
public LazyTreeItem(Item value) {
super(value);
// Unload data on folding to reduce memory
expandedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
flush();
}
});
}
#Override
public ObservableList<TreeItem<Item>> getChildren() {
if (childrenLoaded || !isExpanded()) {
return super.getChildren();
}
if (super.getChildren().size() == 0) {
// Filler node (will translate into loading icon in the
// TreeCell factory)
super.getChildren().add(new TreeItem<>(null));
}
if (getValue() instanceof MultipleItem) {
if (!isLoadingItems) {
loadItems();
}
}
return super.getChildren();
}
public void loadItems() {
Task<List<TreeItem<Item>>> task = new Task<List<TreeItem<Item>>>() {
#Override
protected List<TreeItem<Item>> call() {
isLoadingItems = true;
List<SingleItem> downloadSet = ((MultipleItem) LazyTreeItem.this.getValue()).getEntries();
List<TreeItem<Item>> treeNodes = new ArrayList<>();
for (SingleItem download : downloadSet) {
treeNodes.add(new TreeItem<>(download));
}
return treeNodes;
}
};
task.setOnSucceeded(e -> {
Platform.runLater(() -> {
super.getChildren().clear();
super.getChildren().addAll(task.getValue());
childrenLoaded = true;
isLoadingItems = false;
});
});
ItemLoader.getSingleton().load(task);
}
private void flush() {
childrenLoaded = false;
super.getChildren().clear();
}
#Override
public boolean isLeaf() {
if (childrenLoaded) {
return getChildren().isEmpty();
}
return false;
}
}
ItemLoader.java
public class ItemLoader implements Runnable {
private static ItemLoader instance;
private List<Task> queue = new ArrayList<>();
private Task prevTask = null;
private ItemLoader() {
Thread runner = new Thread(this);
runner.setName("ItemLoader thread");
runner.setDaemon(true);
runner.start();
}
public static ItemLoader getSingleton() {
if (instance == null) {
instance = new ItemLoader();
}
return instance;
}
public <T> void load(Task task) {
if (queue.size() < 1) {
queue.add(task);
}
}
#Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!queue.isEmpty()) {
Task task = queue.get(0);
if (task != prevTask) {
prevTask = task;
task.run();
queue.remove(task);
}
}
}
}
}
Model (Item.java, SingleItem.java, MultipleItem.java)
public class Item {
}
/****************************************************************
********** SingleItem ************
****************************************************************/
public class SingleItem extends Item {
private String id;
public SingleItem(String id) {
this.id = id;
}
public void setId(String id) {
this.id = id;
}
}
/****************************************************************
********** MultipleItem ************
****************************************************************/
public class MultipleItem extends Item {
private List<SingleItem> entries = new ArrayList<>();
public MultipleItem(List<SingleItem> entries) {
this.entries = entries;
}
public List<SingleItem> getEntries() {
return entries;
}
public void setEntries(List<SingleItem> entries) {
this.entries = entries;
}
}
The issue, as pointed out by #kleopatra, is caused by adding a large amount of children when there are one or more items selected. One way to fix this is to try and implement your own FocusModel, as the default FocusModel seems to be the source of the problem. Another, and in my opinion easier, way to create a workaround is to clear the selection before adding the large group of children; afterwards, you can re-select the items that were previously selected.
The way I went about doing this is by firing TreeModificationEvents with custom EventTypes. Also, I decided not to override isLeaf() inside my lazy TreeItem. I find it easier to use a placeholder TreeItem for when the parent TreeItem is a lazy branch. Since there is a placeholder the parent will register as a branch automatically.
Here's an example that browses the default FileSystem. To test if the solution worked I created a 100,000 file directory and opened it; there was no hang for me. Hopefully that means this can be adapted to your code.
Note: This example does remove the children when the branch is collapsed, just like you are doing in your code.
App.java
import java.nio.file.FileSystems;
import java.nio.file.Path;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.stage.Stage;
public class App extends Application {
private static String pathToString(Path p) {
if (p == null) {
return "null";
} else if (p.getFileName() == null) {
return p.toString();
}
return p.getFileName().toString();
}
#Override
public void start(Stage primaryStage) {
TreeView<Path> tree = new TreeView<>(new TreeItem<>());
tree.setShowRoot(false);
tree.setCellFactory(LazyTreeCell.forTreeView("Loading...", App::pathToString));
TreeViewUtils.installSelectionBugWorkaround(tree);
for (Path fsRoot : FileSystems.getDefault().getRootDirectories()) {
tree.getRoot().getChildren().add(new LoadingTreeItem<>(fsRoot, new DirectoryLoader(fsRoot)));
}
primaryStage.setScene(new Scene(tree, 800, 600));
primaryStage.show();
}
}
DirectoryLoader.java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import javafx.scene.control.TreeItem;
public class DirectoryLoader implements Callable<List<? extends TreeItem<Path>>> {
private static final Comparator<Path> COMPARATOR = (left, right) -> {
boolean leftIsDir = Files.isDirectory(left);
if (leftIsDir ^ Files.isDirectory(right)) {
return leftIsDir ? -1 : 1;
}
return left.compareTo(right);
};
private final Path directory;
public DirectoryLoader(Path directory) {
this.directory = directory;
}
#Override
public List<? extends TreeItem<Path>> call() throws Exception {
try (Stream<Path> stream = Files.list(directory)) {
return stream.sorted(COMPARATOR)
.map(this::toTreeItem)
.collect(Collectors.toList());
}
}
private TreeItem<Path> toTreeItem(Path path) {
return Files.isDirectory(path)
? new LoadingTreeItem<>(path, new DirectoryLoader(path))
: new TreeItem<>(path);
}
}
LoadingTreeItem.java
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Supplier;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.control.TreeItem;
public class LoadingTreeItem<T> extends TreeItem<T> {
private static final EventType<?> PRE_ADD_LOADED_CHILDREN
= new EventType<>(treeNotificationEvent(), "PRE_ADD_LOADED_CHILDREN");
private static final EventType<?> POST_ADD_LOADED_CHILDREN
= new EventType<>(treeNotificationEvent(), "POST_ADD_LOADED_CHILDREN");
#SuppressWarnings("unchecked")
static <T> EventType<TreeModificationEvent<T>> preAddLoadedChildrenEvent() {
return (EventType<TreeModificationEvent<T>>) PRE_ADD_LOADED_CHILDREN;
}
#SuppressWarnings("unchecked")
static <T> EventType<TreeModificationEvent<T>> postAddLoadedChildrenEvent() {
return (EventType<TreeModificationEvent<T>>) POST_ADD_LOADED_CHILDREN;
}
private final Callable<List<? extends TreeItem<T>>> callable;
private boolean needToLoadData = true;
private CompletableFuture<?> future;
public LoadingTreeItem(T value, Callable<List<? extends TreeItem<T>>> callable) {
super(value);
this.callable = callable;
super.getChildren().add(new TreeItem<>());
addExpandedListener();
}
#SuppressWarnings("unchecked")
private void addExpandedListener() {
expandedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
needToLoadData = true;
if (future != null) {
future.cancel(true);
}
super.getChildren().setAll(new TreeItem<>());
}
});
}
#Override
public ObservableList<TreeItem<T>> getChildren() {
if (needToLoadData) {
needToLoadData = false;
future = CompletableFuture.supplyAsync(new CallableToSupplierAdapter<>(callable))
.whenCompleteAsync(this::handleAsyncLoadComplete, Platform::runLater);
}
return super.getChildren();
}
private void handleAsyncLoadComplete(List<? extends TreeItem<T>> result, Throwable th) {
if (th != null) {
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), th);
} else {
Event.fireEvent(this, new TreeModificationEvent<>(preAddLoadedChildrenEvent(), this));
super.getChildren().setAll(result);
Event.fireEvent(this, new TreeModificationEvent<>(postAddLoadedChildrenEvent(), this));
}
future = null;
}
private static class CallableToSupplierAdapter<T> implements Supplier<T> {
private final Callable<T> callable;
private CallableToSupplierAdapter(Callable<T> callable) {
this.callable = callable;
}
#Override
public T get() {
try {
return callable.call();
} catch (Exception ex) {
throw new CompletionException(ex);
}
}
}
}
LazyTreeCell.java
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;
import javafx.util.Callback;
public class LazyTreeCell<T> extends TreeCell<T> {
public static <T> Callback<TreeView<T>, TreeCell<T>> forTreeView(String placeholderText,
Callback<? super T, String> toStringCallback) {
return tree -> new LazyTreeCell<>(placeholderText, toStringCallback);
}
private final String placeholderText;
private final Callback<? super T, String> toStringCallback;
public LazyTreeCell(String placeholderText, Callback<? super T, String> toStringCallback) {
this.placeholderText = placeholderText;
this.toStringCallback = toStringCallback;
}
/*
* Assumes that if "item" is null **and** the parent TreeItem is an instance of
* LoadingTreeItem that this is a "placeholder" cell.
*/
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else if (item == null && getTreeItem().getParent() instanceof LoadingTreeItem) {
setText(placeholderText);
} else {
setText(toStringCallback.call(item));
}
}
}
TreeViewUtils.java
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeItem.TreeModificationEvent;
import javafx.scene.control.TreeView;
public class TreeViewUtils {
public static <T> void installSelectionBugWorkaround(TreeView<T> tree) {
List<TreeItem<T>> selected = new ArrayList<>(0);
EventHandler<TreeModificationEvent<T>> preAdd = event -> {
event.consume();
selected.addAll(tree.getSelectionModel().getSelectedItems());
tree.getSelectionModel().clearSelection();
};
EventHandler<TreeModificationEvent<T>> postAdd = event -> {
event.consume();
selected.forEach(tree.getSelectionModel()::select);
selected.clear();
};
ChangeListener<TreeItem<T>> rootListener = (observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeEventHandler(LoadingTreeItem.preAddLoadedChildrenEvent(), preAdd);
oldValue.removeEventHandler(LoadingTreeItem.postAddLoadedChildrenEvent(), postAdd);
}
if (newValue != null) {
newValue.addEventHandler(LoadingTreeItem.preAddLoadedChildrenEvent(), preAdd);
newValue.addEventHandler(LoadingTreeItem.postAddLoadedChildrenEvent(), postAdd);
}
};
rootListener.changed(tree.rootProperty(), null, tree.getRoot());
tree.rootProperty().addListener(rootListener);
}
private TreeViewUtils() {}
}
As implemented, the utility method that installs the workaround is tied to you using LoadingTreeItems in the TreeView. I couldn't think of a good way to make the solution general enough to apply to any arbitrary TreeView; for that, I believe creating a custom FocusModel would be necessary.
There is probably a better way to implement the LazyTreeCell by using a class to wrap the real data—like what you're doing with Item. Then you could have an actual placehoder Item instance that tells the TreeCell that it is a placeholder rather than relying on what type the parent TreeItem is. As it is, my implementation is likely brittle.
I'm trying to implement this workflow with rxJava but i'm sure if i'm misusing or doing stuff wrong.
User asks to login
If a loginResult is available in cache then "emit" the cached LoginResult
Else actually perform the request to the webservice and cache the result if everything is successfull
If an error occurs retry at most 3 times and if there is a 4th time then purge the cache.
Here is my full snippet of code.
public class LoginTask extends BaseBackground<LoginResult> {
private static CachedLoginResult cachedLoginResult = new CachedLoginResult();
private XMLRPCClient xmlrpcClient;
private UserCredentialsHolder userCredentialsHolder;
#Inject
public LoginTask(XMLRPCClient client, UserCredentialsHolder userCredentialsHolder) {
this.xmlrpcClient = client;
this.userCredentialsHolder = userCredentialsHolder;
}
#Override
public LoginResult performRequest() throws Exception {
return UserApi.login(
xmlrpcClient,
userCredentialsHolder.getUserName(),
userCredentialsHolder.getPlainPassword());
}
#Override
public Observable<LoginResult> getObservable() {
return cachedLoginResult.getObservable()
.onErrorResumeNext(
Observable.create(
((Observable.OnSubscribe<LoginResult>) subscriber -> {
try {
if (!subscriber.isUnsubscribed()) {
subscriber.onNext(performRequest()); // actually performRequest
}
subscriber.onCompleted();
} catch (Exception e) {
subscriber.onError(e);
}
})
)
.doOnNext(cachedLoginResult::setLoginResult)
.retry((attempts, t) -> attempts < 3)
.doOnError(throwable -> cachedLoginResult.purgeCache())
);
}
private static class CachedLoginResult {
private LoginResult lr = null;
private long when = 0;
private CachedLoginResult() {
}
public boolean hasCache() {
return lr != null && when + TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES) > System.currentTimeMillis();
}
public void setLoginResult(LoginResult lr) {
if (lr != null) {
this.lr = lr;
this.when = System.currentTimeMillis();
}
}
public void purgeCache() {
this.lr = null;
this.when = 0;
}
public Observable<LoginResult> getObservable() {
return Observable.create(new Observable.OnSubscribe<LoginResult>() {
#Override
public void call(Subscriber<? super LoginResult> subscriber) {
if (!subscriber.isUnsubscribed()) {
if (hasCache()) {
subscriber.onNext(lr);
subscriber.onCompleted();
} else {
subscriber.onError(new RuntimeException("No cache"));
}
}
}
});
}
}
}
Since i wan't able to find any similar examples and i started "playing" with rxjava just 1 day ago i'm unsure of my implementation.
Thank you for your time.
I think this code is alright, good job :)
You were right to use Observable.create in your LoginTask because otherwise result of the call could be cached internally, and then retry wouldn't help much...
This is I think however unnecessary for the CachedLoginResult's Observable. Here you can simplify your code by using Observable.justand Observable.error utility methods, something like:
public Observable<LoginResult> getObservable() {
if (hasCache()) {
return Observable.just(lr);
} else {
return Observable.error(new RuntimeException("No cache"));
}
}
Note: just stores the value you tell it to emit internally, so that resubscriptions will always produce this value. This is what I hinted above, you shouldn't do Observable.just(performRequest()).retry(3) for example, because the performRequest will only ever be called once.
If I understand correctly, you want to perform the login once and cache the result in a reactive manner? If so, here is an example how I would do this:
import java.util.concurrent.ThreadLocalRandom;
import rx.*;
import rx.schedulers.Schedulers;
import rx.subjects.AsyncSubject;
public class CachingLogin {
static class LoginResult {
}
/** Guarded by this. */
AsyncSubject<LoginResult> cache;
public Observable<LoginResult> login(String username, String password) {
AsyncSubject<LoginResult> c;
boolean doLogin = false;
synchronized (this) {
if (cache == null || cache.hasThrowable()) {
cache = AsyncSubject.create();
doLogin = true;
}
c = cache;
}
if (doLogin) {
Observable.just(1).subscribeOn(Schedulers.io())
.map(v -> loginAPI(username, password))
.retry(3).subscribe(c);
}
return c;
}
public void purgeCache() {
synchronized (this) {
cache = null;
}
}
static LoginResult loginAPI(String username, String password) {
if (ThreadLocalRandom.current().nextDouble() < 0.3) {
throw new RuntimeException("Failed");
}
return new LoginResult();
}
}
I'm using GWT 2.4 by using the MVP pattern
I have this situation:
In one presenter (let's call it a TreePresenter since it shows a tree) i have the following code:
display.getSelectedItem().addSelectionHandler(new SelectionHandler<TreeItem>() {
#Override
public void onSelection(SelectionEvent<TreeItem> event) {
....
evtBus.fireEvent(new SelectCategoryEvent(item.getText()));
.....
}
});
And in my AppController class I have this situation:
eventBus.addHandler(SelectCategoryEvent.TYPE, new SelectCategoryEventHandler() {
#Override
public void onSelectCategory(SelectCategoryEvent event) {
saveHistoryEvent(event);
}
});
When i select one itm in the three the event is correctly fired by the instruction
evtBus.fireEvent(new SelectCategoryEvent(item.getText()));
But in my AppController the event is not propagated and I can't handle it in the code
eventBus.addHandler(SelectCategoryEvent.TYPE, new SelectCategoryEventHandler() {
#Override
public void onSelectCategory(SelectCategoryEvent event) {
saveHistoryEvent(event);
}
});
Can anybody tell me the reason?
Thank
Angelo
I'll give you some detail; I built a class for my own history management; I built this class:
public class GwtHistoryEventsMgr {
private Map<String, List<GwtEvent>> tknEvts;
private HandlerManager eventBus;
public GwtHistoryEventsMgr(HandlerManager evtBus){
tknEvts = new HashMap<String, List<GwtEvent>>();
this.eventBus = evtBus;
}
private void saveHistoryEvent( GwtEvent event ){
List<GwtEvent> eventi = null;
if( tknEvts.containsKey(History.getToken()) ){
eventi = tknEvts.get(History.getToken());
}else{
eventi = new ArrayList<GwtEvent>();
}
eventi.add(event);
tknEvts.put(History.getToken(), eventi);
}
public void addEvtHandlers(){
//Aggiungo gli handler
eventBus.addHandler(CustomEvent.TYPE, new CustomEventHandler() {
#Override
public void onEvent(CustomEvent event) {
saveHistoryEvent(event);
}
});
}
public List<GwtEvent> getTokenTransWidgetEvents(String token){
if( tknEvts.containsKey(token) ){
return tknEvts.remove(token);
}else{
return null;
}
}
}
Then in my AppController constructor I wrote this code:
public AppController(HandlerManager eventBus, StandardDispatchAsync dispatcher){
super(null);
this.eventBus = eventBus;
this.dispatcher = dispatcher;
//Gestione history
histMgr = new GwtHistoryEventsMgr(eventBus);
histMgr.addEvtHandlers();
}
This means that I should be pretty sure that the AppController registers itself to the events I want (note: I strongly reduced the code...but all the code is in the way I wrote)
Then, since I use client-side reflection, I did, where I use the client side reflection, this code (after that all widgets have been initialized):
#SuppressWarnings("rawtypes")
private void generateHistoricalEvents(){
List<GwtEvent> eventi = hisMgr.getTokenTransWidgetEvents(History.getToken());
//Ci sono eventi....vuol dire back del browser cliccato
if( eventi != null ){
for (GwtEvent gwtEvent : eventi) {
this.eventBus.fireEvent(gwtEvent);
}
}
}
According to me it's all OK; may you tell me where the problem is?
Thank you
Angelo