From 28b02c195b7b80b592136fb3d31dc69df9af73b9 Mon Sep 17 00:00:00 2001 From: John Hendrikx Date: Sat, 12 Jul 2014 14:42:32 +0200 Subject: [PATCH] Added builder variant for creating a binding --- .../java/hs/mediasystem/util/MapBindings.java | 222 +++++++++++++----- .../hs/mediasystem/util/MapBindingsTest.java | 42 +++- 2 files changed, 207 insertions(+), 57 deletions(-) diff --git a/Mediasystem/src/main/java/hs/mediasystem/util/MapBindings.java b/Mediasystem/src/main/java/hs/mediasystem/util/MapBindings.java index 7c91bba1..c8644ba7 100644 --- a/Mediasystem/src/main/java/hs/mediasystem/util/MapBindings.java +++ b/Mediasystem/src/main/java/hs/mediasystem/util/MapBindings.java @@ -16,6 +16,7 @@ import javafx.beans.binding.IntegerBinding; import javafx.beans.binding.LongBinding; import javafx.beans.binding.ObjectBinding; +import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; @@ -182,6 +183,10 @@ protected T computeValue() { }; } + public static Builder get(Observable root) { + return new Builder(root); + } + private static class Helper { private final Observable[] observables; private final Property[] properties; @@ -193,7 +198,7 @@ public void invalidated(Observable observable) { }; private final WeakInvalidationListener observer = new WeakInvalidationListener(listener); private final Observable root; - private final Object[] steps; + private final Step[] steps; private final Binding binding; public Helper(Binding binding, final Observable root, final Object... steps) { @@ -204,7 +209,7 @@ public Helper(Binding binding, final Observable root, final Object... steps) properties = new Property[this.steps.length]; } - private static Object[] expandSteps(Observable root, Object[] steps) { + private static Step[] expandSteps(Observable root, Object[] steps) { List expandedSteps = new ArrayList<>(); int indicesRequired = root instanceof ObservableValue ? 0 : 1; @@ -212,6 +217,11 @@ private static Object[] expandSteps(Observable root, Object[] steps) { Object step = steps[i]; if(indicesRequired-- <= 0) { + if(step instanceof Step) { + expandedSteps.add(step); + continue; + } + if(!(step instanceof String)) { throw new IllegalArgumentException("expected String at step " + i + ": " + step); } @@ -223,15 +233,17 @@ private static Object[] expandSteps(Observable root, Object[] steps) { throw new IllegalArgumentException("invalid step format at step " + i + ": " + step); } - expandedSteps.add(subStep); - if(subStep.endsWith("[]")) { + expandedSteps.add(new Then(subStep.substring(0, subStep.length() - 2))); indicesRequired++; } + else { + expandedSteps.add(new Then(subStep)); + } } } else { - expandedSteps.add(step); + expandedSteps.add(new IndexLookup(step, i)); } } @@ -239,7 +251,7 @@ private static Object[] expandSteps(Observable root, Object[] steps) { throw new IllegalArgumentException("insufficient indices specified in steps: missing " + indicesRequired + " indices"); } - return expandedSteps.toArray(new Object[expandedSteps.size()]); + return expandedSteps.toArray(new Step[expandedSteps.size()]); } private void unregisterListeners() { @@ -254,7 +266,6 @@ private void unregisterListeners() { protected Object computeValue() { Observable observable = root; - boolean indexedStep = false; for(int index = 0; index <= steps.length; index++) { observables[index] = observable; @@ -266,12 +277,6 @@ protected Object computeValue() { Object value = observable instanceof ObservableValue ? ((ObservableValue)observable).getValue() : observable; - if(indexedStep && observable instanceof ObservableValue && !(value instanceof Observable)) { - throw new RuntimeBindException("map or list expected at step " + (index - 1) + ": " + steps[index - 1] + " : " + observable); - } - - indexedStep = steps[index] instanceof String && ((String)steps[index]).endsWith("[]") && observable instanceof ObservableValue ? true : false; - try { if(properties[index] == null || !value.getClass().equals(properties[index].getBeanClass())) { properties[index] = new Property(value.getClass(), steps[index]); @@ -293,73 +298,178 @@ protected Object computeValue() { private static class Property { private final Class cls; - private final Object name; + private final Step step; + + public Property(Class cls, Step name) { + this.cls = cls; + this.step = name; + } + + public Observable getObservable(Object bean) { + try { + return step.execute(cls, bean); + } + catch(IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + e.printStackTrace(); + return null; + } + } + + public Class getBeanClass() { + return cls; + } + } + + private interface Step { + Observable execute(Class cls, Object bean) throws IllegalAccessException, InvocationTargetException; + } + + private static class Then implements Step { + private final String propertyName; private Method method; private Field field; - public Property(Class cls, Object name) { - this.cls = cls; - - if(name instanceof String) { - String s = (String)name; + public Then(String propertyName) { + this.propertyName = propertyName; + } - if(s.endsWith("[]")) { - this.name = s.substring(0, s.length() - 2); + @Override + public Observable execute(Class cls, Object bean) throws IllegalAccessException, InvocationTargetException { + if(method == null && field == null) { + try { + method = cls.getMethod(propertyName + "Property"); } - else { - this.name = name; + catch(NoSuchMethodException e) { + try { + field = cls.getField(propertyName); + } + catch(NoSuchFieldException e2) { + throw new RuntimeBindException("No such property found: " + propertyName + " in class: " + cls); + } } } - else { - this.name = name; - } + + return method != null ? (Observable)method.invoke(bean) : (Observable)field.get(bean); + } + } + + private static class IndexLookup implements Step { + private final Object obj; + private final int stepNumber; + + public IndexLookup(Object obj, int stepNumber) { + this.obj = obj; + this.stepNumber = stepNumber; } @SuppressWarnings("unchecked") - public Observable getObservable(Object bean) { - try { - if(bean instanceof ObservableMap) { - return Bindings.valueAt((ObservableMap)bean, name); + @Override + public Observable execute(Class cls, Object bean) throws IllegalAccessException, InvocationTargetException { + if(bean instanceof ObservableMap) { + return Bindings.valueAt((ObservableMap)bean, obj); + } + + if(bean instanceof ObservableList) { + if(!(obj instanceof Integer)) { + throw new RuntimeBindException("index for ObservableList (" + bean + ") must be of type Integer: " + obj.getClass()); } - if(bean instanceof ObservableList) { - if(!(name instanceof Integer)) { - throw new RuntimeBindException("index for ObservableList (" + bean + ") must be of type Integer: " + name.getClass()); - } + int index = (Integer)obj; - int index = (Integer)name; + if(index < 0) { + throw new RuntimeBindException("index for ObservableList (" + bean + ") cannot be negative: " + index); + } - if(index < 0) { - throw new RuntimeBindException("index for ObservableList (" + bean + ") cannot be negative: " + index); - } + return Bindings.valueAt((ObservableList)bean, index); + } - return Bindings.valueAt((ObservableList)bean, index); - } + throw new RuntimeBindException("map or list expected at step " + (stepNumber - 1) + ", but got: " + cls + ": " + bean); + } + } + + public static class Builder { + private final Observable root; + private final List steps = new ArrayList<>(); + + public Builder(Observable root) { + this.root = root; + } + + public Builder then(String propertyName) { + steps.add(propertyName); + return this; + } + + public Builder thenOrDefault(String propertyName, Object defaultValue) { + steps.add(new ThenOrDefault(propertyName, defaultValue)); + return this; + } - if(name instanceof String) { - if(method == null && field == null) { + public Builder lookup(Object valueOrIndex) { + steps.add(new IndexLookup(valueOrIndex, steps.size())); + return this; + } + + public StringBinding asStringBinding() { + return MapBindings.selectString(root, steps.toArray(new Object[steps.size()])); + } + + public ObjectBinding asObjectBinding() { + return MapBindings.select(root, steps.toArray(new Object[steps.size()])); + } + + private static class ThenOrDefault implements Step { + private final String propertyName; + private final Object defaultValue; + + private Method method; + private Field field; + + public ThenOrDefault(String propertyName, Object defaultValue) { + this.propertyName = propertyName; + this.defaultValue = defaultValue; + } + + @Override + public Observable execute(Class cls, Object bean) throws IllegalAccessException, InvocationTargetException { + if(method == null && field == null) { + try { + method = cls.getMethod(propertyName + "Property"); + } + catch(NoSuchMethodException e) { try { - method = cls.getMethod(name + "Property"); + field = cls.getField(propertyName); } - catch(NoSuchMethodException e) { - field = cls.getField((String)name); + catch(NoSuchFieldException e2) { + return new ObservableValue() { + @Override + public void addListener(InvalidationListener listener) { + } + + @Override + public void removeListener(InvalidationListener listener) { + } + + @Override + public void addListener(ChangeListener listener) { + } + + @Override + public void removeListener(ChangeListener listener) { + } + + @Override + public Object getValue() { + return defaultValue; + } + }; } } - - return method != null ? (Observable)method.invoke(bean) : (Observable)field.get(bean); } - throw new IllegalArgumentException("expected string: " + name); - } - catch(NoSuchFieldException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - e.printStackTrace(); - return null; + return method != null ? (Observable)method.invoke(bean) : (Observable)field.get(bean); } } - - public Class getBeanClass() { - return cls; - } } } diff --git a/Mediasystem/src/test/java/hs/mediasystem/util/MapBindingsTest.java b/Mediasystem/src/test/java/hs/mediasystem/util/MapBindingsTest.java index 7e6fef99..d83ed645 100644 --- a/Mediasystem/src/test/java/hs/mediasystem/util/MapBindingsTest.java +++ b/Mediasystem/src/test/java/hs/mediasystem/util/MapBindingsTest.java @@ -115,7 +115,7 @@ public void shouldGetEpisodeNumberWithCombinedStep() { public void shouldThrowExceptionWhenMapExpectedButNotEncountered() { ObjectBinding select = MapBindings.select(file1.videoProperty(), "episode[]", "episodeNumber"); // episode is not a map - assertNull(select.get()); + select.get(); } /* @@ -392,4 +392,44 @@ public void shouldOnlyRegisterListenerOnce() { assertEquals(1, mock.getListenerCount()); } + + /* + * Builder tests + */ + + @Test + public void shouldGetTitleWithBuilder() { + StringBinding stringBinding = MapBindings.get(file1.videoProperty()).then("title").asStringBinding(); + + assertEquals("Alice", stringBinding.get()); + } + + @Test + public void shouldGetDefaultWithBuilder() { + StringBinding stringBinding = MapBindings.get(file1.videoProperty()).thenOrDefault("nonExistingField", "Alice").asStringBinding(); + + assertEquals("Alice", stringBinding.get()); + } + + @Test + public void shouldGetNullDefaultWithBuilder() { + StringBinding stringBinding = MapBindings.get(file1.videoProperty()).thenOrDefault("nonExistingField", null).asStringBinding(); + + assertNull(stringBinding.get()); + } + + @Test + public void shouldGetListValueWithBuilder() { + ObjectBinding select = MapBindings.get(file1.videoProperty()).then("list").lookup(1).asObjectBinding(); + + assertEquals("B", select.get()); + } + + @Test + public void shouldGetMapValueWithBuilder() { + ObjectBinding select = MapBindings.get(file1.videoProperty()).then("dataMap").lookup(YouTube.class).then("rating").asObjectBinding(); + + assertEquals(9, select.get()); + } + }