JaroslavTulach closed pull request #3: Computed property enhancement
URL: https://github.com/apache/incubator-netbeans-html4j/pull/3
This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:
As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):
diff --git a/json/src/main/java/net/java/html/json/ComputedProperty.java
b/json/src/main/java/net/java/html/json/ComputedProperty.java
index 4224cbc..94484a1 100644
--- a/json/src/main/java/net/java/html/json/ComputedProperty.java
+++ b/json/src/main/java/net/java/html/json/ComputedProperty.java
@@ -22,24 +22,56 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.netbeans.html.json.spi.Technology;
/** Can be used in classes annotated with {@link Model} annotation to
* define a derived property. Value of derived property is based on values
- * of {@link Property} as enumerated by {@link Model#properties()}.
+ * of regular {@link Property properties} as specified by {@link
Model#properties()}.
+ * The name of the computed/derived property is the name of the annotated
method.
* <p>
- * The name of the derived property is the name of the method. The arguments
+ * <b>Classic Example</b>
+ * <p>
+ * Imagine one wants to represent a formula with two variables and certain
+ * computations performed on top of them. One can represent such computations
+ * with {@code @ComputedProperty}:
+ *
+ * {@codesnippet net.java.html.json.SquaresTest}
+ *
+ * There are two variables {@code a} and {@code b} with appropriate getters and
+ * setters. In addition there are derived properties {@code plus}, {@code
minus}
+ * and {@code aPlusBTimesAMinusB} which is based on the previous two derived
+ * properties. The usage then follows classical bean patterns:
+ * <p>
+ *
+ * {@codesnippet net.java.html.json.SquaresTest#threeAndTwo}
+ * {@codesnippet net.java.html.json.SquaresTest#fiveAndTwo}
+ *
+ * <p>
+ * <b>Shorthand Syntax</b>
+ * <p>
+ * Sometimes using the getters to compute a property may lead to
<em>unnecessary
+ * verbosity</em>. As such there is a shorthand syntax for defining the derived
+ * properties. Rather than writing:
+ * <p>
+ * {@codesnippet net.java.html.json.SquaresTest#aSquareMinusBSquareClassic}
+ * <p>
+ * one can choose <em>decomposition - pattern matching</em> and name the
+ * properties one wants to access in the signature of the method:
+ * <p>
+ * {@codesnippet net.java.html.json.SquaresTest#aSquareMinusBSquare}
+ * <p>
+ * The arguments
* of the method must match names and types of some of the properties
- * from {@link Model#properties()} list. As soon as one of these properties
- * changes, the method is called to recompute its new value.
- * This applies to inner changes in derived properties as well - e.g.
- * if the dependant property is another type generated by {@link Model @Model}
annotation -
- * changes in its own properties trigger recomputation of this derived
- * property as well (since version 0.9).
+ * from {@link Model#properties()} list.
+ * <p>
+ * As soon as one of the properties the derived property method is accessing
+ * changes, the method is called again to recompute its new value and the
+ * change is notified to the underlying {@linkplain Technology (rendering)
technology}.
* <p>
* Method's return type defines the type of the derived property. It may be
* any primitive type, {@link String}, {@link Enum enum type} or a
* type generated by {@link Model @Model} annotation. One may
- * also return an array by returning a list of such (boxed) type
+ * also return a list of such (boxed) type
* (for example {@link java.util.List List}<{@link String}> or {@link
java.util.List List}<{@link Integer}>).
* <p>
* An example testing <a target="_blank"
href="http://dew.apidesign.org/dew/#7545568">
@@ -51,27 +83,50 @@
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface ComputedProperty {
- /** Name of a method to handle changes to the computed property.
- * By default the computed properties are read-only, however one can
- * make them mutable by defining a static method that takes
+ /** Defines mutable computed property.
+ * The value of this attribute references another method in the same class
+ * by its name. The method is then called to handle changes to this
computed property
+ * made by its generated setter.
+ * <p>
+ * By default the computed properties are read-only (e.g. they only have a
getter),
+ * however one can make them mutable by defining a static method that takes
* two parameters:
* <ol>
- * <li>the model class</li>
- * <li>the value - either exactly the return the method annotated
- * by this property or a superclass (like {@link Object})</li>
+ * <li>model class - provides access to <em>"this"</em> and
+ * allows you to call appropriate setters in response of changing
+ * the computed property value
+ * </li>
+ * <li>value - either exactly of the same type as is the return type of
the annotated method
+ * or a superclass (like {@link Object}) of that type
+ * </li>
* </ol>
- * Sample code snippet using the <b>write</b> feature of {@link
ComputedProperty}
- * could look like this (assuming the {@link Model model class} named
- * <em>DataModel</em> has <b>int</b> property <em>value</em>):
- * <pre>
- * {@link ComputedProperty @ComputedProperty}(write="setPowerValue")
- * <b>static int</b> powerValue(<b>int</b> value) {
- * <b>return</b> value * value;
- * }
- * <b>static void</b> setPowerValue(DataModel m, <b>int</b> value) {
- * m.setValue((<b>int</b>){@link Math}.sqrt(value));
- * }
- * </pre>
+ * <p>
+ * <b>Power of a Value Example</b>
+ * <p>
+ * Imagine you want to compute power of a number. You can define simple
+ * model class and a computed property {@code pow} inside it:
+ * <p>
+ * {@codesnippet net.java.html.json.PowerTest}
+ * <p>
+ * Such code allows you to find out that the power of three is nine:
+ * <p>
+ * {@codesnippet net.java.html.json.PowerTest#computesPower}
+ * <p>
+ * However you can change the above example to also compute a square of the
+ * value by making the {@code power} property writable:
+ * <p>
+ * {@codesnippet net.java.html.json.PowerTest#sqrt}
+ * <p>
+ * Then it is possible to use the same model to find out that square of
four
+ * is two:
+ * <p>
+ * {@codesnippet net.java.html.json.PowerTest#canSetComputedProperty}
+ * <p>
+ * <em>Implementation note</em>:
+ * There cannot be two properties of the same name and different
+ * behavior and as such the example is using two properties {@code pow}
+ * and {@code power}. One of them is read-only and the second is writable.
+ * The body of both methods is the same however.
*
* @return the name of a method to handle changes to the computed
* property
diff --git a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java
b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java
index 2cce93e..9886955 100644
--- a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java
+++ b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java
@@ -818,33 +818,59 @@ private boolean generateComputedProperties(
w.write(" " + gs[0] + "() {\n");
int arg = 0;
boolean deep = false;
- for (VariableElement pe : ee.getParameters()) {
- final String dn = pe.getSimpleName().toString();
- if (!verifyPropName(pe, dn, fixedProps)) {
- ok = false;
- }
- final TypeMirror pt = pe.asType();
- if (isModel(pt)) {
- deep = true;
- }
- final String dt = fqn(pt, ee);
- if (dt.startsWith("java.util.List") && pt instanceof
DeclaredType) {
- final List<? extends TypeMirror> ptArgs =
((DeclaredType)pt).getTypeArguments();
- if (ptArgs.size() == 1 && isModel(ptArgs.get(0))) {
+ final List<? extends VariableElement> methodParameters =
ee.getParameters();
+
+ String unknownSingleProperty = methodParameters.size() != 1 ? null
:
+ verifyPropName(methodParameters.get(0), fixedProps);
+
+ if (unknownSingleProperty == null) {
+ for (VariableElement pe : methodParameters) {
+ final String dn = pe.getSimpleName().toString();
+
+ String unknownPropertyError = verifyPropName(pe,
fixedProps);
+ if (unknownPropertyError != null) {
+ error(unknownPropertyError, e);
+ ok = false;
+ }
+ final TypeMirror pt = pe.asType();
+ if (isModel(pt)) {
deep = true;
}
+ final String dt = fqn(pt, ee);
+ if (dt.startsWith("java.util.List") && pt instanceof
DeclaredType) {
+ final List<? extends TypeMirror> ptArgs =
((DeclaredType)pt).getTypeArguments();
+ if (ptArgs.size() == 1 && isModel(ptArgs.get(0))) {
+ deep = true;
+ }
+ }
+ String[] call = toGetSet(dn, dt, false);
+ w.write(" " + dt + " arg" + (++arg) + " = ");
+ w.write(call[0] + "();\n");
+
+ Collection<String[]> depends = deps.get(dn);
+ if (depends == null) {
+ depends = new LinkedHashSet<String[]>();
+ deps.put(dn, depends);
+ }
+ depends.add(new String[] { sn, gs[0] });
}
- String[] call = toGetSet(dn, dt, false);
- w.write(" " + dt + " arg" + (++arg) + " = ");
- w.write(call[0] + "();\n");
-
- Collection<String[]> depends = deps.get(dn);
- if (depends == null) {
- depends = new LinkedHashSet<String[]>();
- deps.put(dn, depends);
+ } else {
+ VariableElement firstProp = methodParameters.get(0);
+ TypeMirror type = firstProp.asType();
+ CharSequence simpleName;
+ if (type.getKind() == TypeKind.DECLARED) {
+ simpleName = ((DeclaredType)
type).asElement().getSimpleName();
+ } else {
+ simpleName = type.toString();
}
- depends.add(new String[] { sn, gs[0] });
+ if (simpleName.toString().equals(className)) {
+ } else {
+ error("Single parameter needs to be of type " + className
+ " or " + unknownSingleProperty, e);
+ ok = false;
+ continue NEXT_ANNOTATION;
+ }
+ w.write(" " + simpleName + " arg" + (++arg) + " = this;\n");
}
w.write(" try {\n");
if (tp != null) {
@@ -965,12 +991,13 @@ private static String findBoxedType(String ret) {
return null;
}
- private boolean verifyPropName(Element e, String propName, Prprt[]
existingProps) {
+ private String verifyPropName(Element e, Prprt[] existingProps) {
+ String propName = e.getSimpleName().toString();
StringBuilder sb = new StringBuilder();
String sep = "";
for (Prprt Prprt : existingProps) {
if (Prprt.name().equals(propName)) {
- return true;
+ return null;
}
sb.append(sep);
sb.append('"');
@@ -978,11 +1005,7 @@ private boolean verifyPropName(Element e, String
propName, Prprt[] existingProps
sb.append('"');
sep = ", ";
}
- error(
- propName + " is not one of known properties: " + sb
- , e
- );
- return false;
+ return propName + " has to be one of known properties: " + sb;
}
private static String findPkgName(Element e) {
diff --git a/json/src/main/java/org/netbeans/html/json/spi/Observers.java
b/json/src/main/java/org/netbeans/html/json/spi/Observers.java
index e49b162..9c70221 100644
--- a/json/src/main/java/org/netbeans/html/json/spi/Observers.java
+++ b/json/src/main/java/org/netbeans/html/json/spi/Observers.java
@@ -49,7 +49,7 @@ static void verifyUnlocked(Proto p) {
for (Watcher w : GLOBAL) {
if (w.proto == p) {
if (w.owner == Thread.currentThread()) {
- throw new IllegalStateException("Re-entrant attempt to
access " + p);
+ throw new IllegalStateException("Re-entrant attempt to
access " + p.toStr());
}
}
}
diff --git a/json/src/test/java/net/java/html/json/PowerTest.java
b/json/src/test/java/net/java/html/json/PowerTest.java
new file mode 100644
index 0000000..90431f4
--- /dev/null
+++ b/json/src/test/java/net/java/html/json/PowerTest.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 net.java.html.json;
+
+import static org.testng.Assert.assertEquals;
+import org.testng.annotations.Test;
+
+// BEGIN: net.java.html.json.PowerTest
+@Model(className = "Power", properties = {
+ @Property(name = "value", type = double.class)
+})
+public class PowerTest {
+ @ComputedProperty
+ static double pow(double value) {
+ return value * value;
+ }
+// FINISH: net.java.html.json.PowerTest
+
+// BEGIN: net.java.html.json.PowerTest#sqrt
+ @ComputedProperty(write = "sqrt")
+ static double power(double value) {
+ return value * value;
+ }
+
+ static void sqrt(Power model, double value) {
+ model.setValue(Math.sqrt(value));
+ }
+// END: net.java.html.json.PowerTest#sqrt
+
+ @Test
+ public void computesPower() {
+// BEGIN: net.java.html.json.PowerTest#computesPower
+ Power p = new Power(3);
+ assertEquals(p.getPow(), 9, 0.01);
+// END: net.java.html.json.PowerTest#computesPower
+ }
+
+ @Test
+ public void canSetComputedProperty() {
+// BEGIN: net.java.html.json.PowerTest#canSetComputedProperty
+ Power p = new Power();
+ p.setPower(4.0);
+ assertEquals(2.0, p.getValue(), 0.01, "Adjusted to square of four");
+ assertEquals(4.0, p.getPower(), 0.01, "Was set to four");
+ assertEquals(4.0, p.getPow(), 0.01, "Kept in sync with power
property");
+// END: net.java.html.json.PowerTest#canSetComputedProperty
+ }
+}
diff --git a/json/src/test/java/net/java/html/json/SquaresTest.java
b/json/src/test/java/net/java/html/json/SquaresTest.java
new file mode 100644
index 0000000..ff07a87
--- /dev/null
+++ b/json/src/test/java/net/java/html/json/SquaresTest.java
@@ -0,0 +1,117 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 net.java.html.json;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+import org.testng.annotations.Test;
+
+// BEGIN: net.java.html.json.SquaresTest
+@Model(className = "Formula", properties = {
+ @Property(name = "a", type = int.class),
+ @Property(name = "b", type = int.class),
+})
+public class SquaresTest {
+ @ComputedProperty
+ static int plus(Formula model) {
+ return model.getA() + model.getB();
+ }
+
+ @ComputedProperty
+ static int minus(Formula model) {
+ return model.getA() - model.getB();
+ }
+
+ @ComputedProperty
+ static int aPlusBTimesAMinusB(Formula both) {
+ return both.getPlus() * both.getMinus();
+ }
+// FINISH: net.java.html.json.SquaresTest
+
+ @ComputedProperty(write = "unsquare")
+// BEGIN: net.java.html.json.SquaresTest#aSquareMinusBSquareClassic
+ static int aSquareMinusBSquareClassic(Formula squares) {
+ return squares.getA() * squares.getA() - squares.getB() *
squares.getB();
+ }
+// END: net.java.html.json.SquaresTest#aSquareMinusBSquareClassic
+
+// BEGIN: net.java.html.json.SquaresTest#aSquareMinusBSquare
+ @ComputedProperty
+ static int aSquareMinusBSquare(int a, int b) {
+ return a * a - b * b;
+ }
+// END: net.java.html.json.SquaresTest#aSquareMinusBSquare
+
+ static void unsquare(Formula f, int compute) {
+ for (int a = 0; a < 10; a++) {
+ for (int b = 0; b < 10; b++) {
+ Formula tmp = new Formula(a, b);
+ if (tmp.getAPlusBTimesAMinusB() == compute) {
+ f.setA(a);
+ f.setB(b);
+ return;
+ }
+ }
+ }
+ throw new IllegalStateException("Cannot find values of a and b for " +
compute);
+ }
+
+ @ComputedProperty
+ static int dontTouchA(Formula both) {
+ both.setA(10);
+ return -1;
+ }
+
+ @Test
+ public void threeAndTwo() {
+// BEGIN: net.java.html.json.SquaresTest#threeAndTwo
+ final Formula formula = new Formula(3, 2);
+ assertEquals(formula.getAPlusBTimesAMinusB(), 5);
+// END: net.java.html.json.SquaresTest#threeAndTwo
+ assertEquals(formula.getASquareMinusBSquare(), 5);
+ }
+
+ @Test
+ public void fiveAndTwo() {
+// BEGIN: net.java.html.json.SquaresTest#fiveAndTwo
+ final Formula formula = new Formula(5, 2);
+ assertEquals(formula.getAPlusBTimesAMinusB(), 21);
+// END: net.java.html.json.SquaresTest#fiveAndTwo
+ assertEquals(formula.getASquareMinusBSquare(), 21);
+ }
+
+ @Test
+ public void findTwoAndFive() {
+ final Formula formula = new Formula();
+ formula.setASquareMinusBSquareClassic(21);
+
+ assertEquals(formula.getA(), 5);
+ assertEquals(formula.getB(), 2);
+ }
+
+ @Test
+ public void dontTouchA() {
+ try {
+ int result = new Formula(3, 2).getDontTouchA();
+ fail("No result shall be produced" + result);
+ } catch (IllegalStateException ex) {
+ // OK
+ }
+ }
+}
diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html
index 9e5378f..49a44e4 100644
--- a/src/main/javadoc/overview.html
+++ b/src/main/javadoc/overview.html
@@ -120,6 +120,14 @@ <h3>Highlights</h3>
CSS navigation and IDE integration.
</p>
+ <h3>New version will include</h3>
+
+ <p>
+ {@link net.java.html.json.ComputedProperty Computed properties} can
+ depend on other computed properties -
+ <a target="_blank"
href="https://github.com/apache/incubator-netbeans-html4j/pull/3">PR #3</a>.
+ </p>
+
<h3>New in version 1.5.1</h3>
<p>
----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
[email protected]
With regards,
Apache Git Services