postgresql-jdbc.git / summary / log / commit / refs

commit    bf00387461bafdf0f19a8557bff554e469207cce
Author:   Jorge Solórzano <jorsol@gmail.com>
Commit:   GitHub <noreply@github.com>
Date:     Wed Sep 29 02:02:42 2021 +0000

    fix: handle nulls when the following clasess are used: PGbox, PGcircle, PGline, PGlseg, PGpath, PGpoint, PGpolygon, and PGmoney (#2271)
    
    1) PGobject.setValue(null) can be used to set the wrapped value to null
    Note: previously the sub-classes of PGobject threw NPE for setValue(null)
    2) Return null from `getValue` in case the typed null is received from the database
    3) Assume PGBinaryObject#lengthInBytes == 0 means the value should be set as null when sending data to the database
    
    see #1870
    
    Co-authored-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>


CHANGELOG.md | 1 + .../main/java/org/postgresql/geometric/PGbox.java | 59 +++++++--- .../java/org/postgresql/geometric/PGcircle.java | 29 +++-- .../main/java/org/postgresql/geometric/PGline.java | 70 ++++++++--- .../main/java/org/postgresql/geometric/PGlseg.java | 36 ++++-- .../main/java/org/postgresql/geometric/PGpath.java | 45 ++++--- .../java/org/postgresql/geometric/PGpoint.java | 31 ++++- .../java/org/postgresql/geometric/PGpolygon.java | 36 +++--- .../java/org/postgresql/jdbc/PgConnection.java | 4 +- .../org/postgresql/jdbc/PgPreparedStatement.java | 7 +- .../main/java/org/postgresql/util/PGInterval.java | 40 ++++++- .../src/main/java/org/postgresql/util/PGmoney.java | 24 +++- .../main/java/org/postgresql/util/PGobject.java | 14 ++- .../org/postgresql/test/jdbc2/PGObjectGetTest.java | 130 +++++++++++++++++++++ .../org/postgresql/test/jdbc2/PGObjectSetTest.java | 127 ++++++++++++++++++++ 15 files changed, 562 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d69d4aa..0172589c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ is executed a second time it will fail. - The driver returns enum and jsonb arrays elements as String objects (like in 42.2.14 and earlier versions) [PR 1879](https://github.com/pgjdbc/pgjdbc/pull/1879). - PgTokenizer was ignoring last empty token [PR #1882](https://github.com/pgjdbc/pgjdbc/pull/1882) - Remove osgi from karaf fixes Issue #1891 [PR #1902](https://github.com/pgjdbc/pgjdbc/pull/1902) +- Handle nulls when the following clasess are used: PGbox, PGcircle, PGline, PGlseg, PGpath, PGpoint, PGpolygon, and PGmoney. ## [42.2.16] (2020-08-20) ### Known issues diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGbox.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGbox.java index 2476f40a..db74e048 100644 --- a/pgjdbc/src/main/java/org/postgresql/geometric/PGbox.java +++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGbox.java @@ -5,6 +5,8 @@ package org.postgresql.geometric; +import static org.postgresql.util.internal.Nullness.castNonNull; + import org.postgresql.util.GT; import org.postgresql.util.PGBinaryObject; import org.postgresql.util.PGobject; @@ -24,7 +26,7 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo /** * These are the two points. */ - public PGpoint[] point = new PGpoint[2]; + public PGpoint @Nullable [] point; /** * @param x1 first x coordinate @@ -33,9 +35,7 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo * @param y2 second y coordinate */ public PGbox(double x1, double y1, double x2, double y2) { - this(); - this.point[0] = new PGpoint(x1, y1); - this.point[1] = new PGpoint(x2, y2); + this(new PGpoint(x1, y1), new PGpoint(x2, y2)); } /** @@ -44,8 +44,7 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo */ public PGbox(PGpoint p1, PGpoint p2) { this(); - this.point[0] = p1; - this.point[1] = p2; + this.point = new PGpoint[]{p1, p2}; } /** @@ -73,7 +72,11 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo * @throws SQLException thrown if value is invalid for this type */ @Override - public void setValue(String value) throws SQLException { + public void setValue(@Nullable String value) throws SQLException { + if (value == null) { + this.point = null; + return; + } PGtokenizer t = new PGtokenizer(value, ','); if (t.getSize() != 2) { throw new PSQLException( @@ -81,6 +84,10 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo PSQLState.DATA_TYPE_MISMATCH); } + PGpoint[] point = this.point; + if (point == null) { + this.point = point = new PGpoint[2]; + } point[0] = new PGpoint(t.getToken(0)); point[1] = new PGpoint(t.getToken(1)); } @@ -89,10 +96,15 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo * @param b Definition of this point in PostgreSQL's binary syntax */ public void setByteValue(byte[] b, int offset) { + PGpoint[] point = this.point; + if (point == null) { + this.point = point = new PGpoint[2]; + } point[0] = new PGpoint(); point[0].setByteValue(b, offset); point[1] = new PGpoint(); point[1].setByteValue(b, offset + point[0].lengthInBytes()); + this.point = point; } /** @@ -104,26 +116,34 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo PGbox p = (PGbox) obj; // Same points. - if (p.point[0].equals(point[0]) && p.point[1].equals(point[1])) { + PGpoint[] point = this.point; + PGpoint[] pPoint = p.point; + if (point == null) { + return pPoint == null; + } else if (pPoint == null) { + return false; + } + + if (pPoint[0].equals(point[0]) && pPoint[1].equals(point[1])) { return true; } // Points swapped. - if (p.point[0].equals(point[1]) && p.point[1].equals(point[0])) { + if (pPoint[0].equals(point[1]) && pPoint[1].equals(point[0])) { return true; } // Using the opposite two points of the box: // (x1,y1),(x2,y2) -> (x1,y2),(x2,y1) - if (p.point[0].x == point[0].x && p.point[0].y == point[1].y - && p.point[1].x == point[1].x && p.point[1].y == point[0].y) { + if (pPoint[0].x == point[0].x && pPoint[0].y == point[1].y + && pPoint[1].x == point[1].x && pPoint[1].y == point[0].y) { return true; } // Using the opposite two points of the box, and the points are swapped // (x1,y1),(x2,y2) -> (x2,y1),(x1,y2) - if (p.point[0].x == point[1].x && p.point[0].y == point[0].y - && p.point[1].x == point[0].x && p.point[1].y == point[1].y) { + if (pPoint[0].x == point[1].x && pPoint[0].y == point[0].y + && pPoint[1].x == point[0].x && pPoint[1].y == point[1].y) { return true; } } @@ -136,7 +156,8 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo // its X and Y components; we end up with an exclusive-OR of the two X and // two Y components, which is equal whenever equals() would return true // since xor is commutative. - return point[0].hashCode() ^ point[1].hashCode(); + PGpoint[] point = this.point; + return point == null ? 0 : point[0].hashCode() ^ point[1].hashCode(); } public Object clone() throws CloneNotSupportedException { @@ -155,15 +176,21 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo /** * @return the PGbox in the syntax expected by org.postgresql */ - public String getValue() { - return point[0].toString() + "," + point[1].toString(); + public @Nullable String getValue() { + PGpoint[] point = this.point; + return point == null ? null : point[0].toString() + "," + point[1].toString(); } public int lengthInBytes() { + PGpoint[] point = this.point; + if (point == null) { + return 0; + } return point[0].lengthInBytes() + point[1].lengthInBytes(); } public void toBytes(byte[] bytes, int offset) { + PGpoint[] point = castNonNull(this.point); point[0].toBytes(bytes, offset); point[1].toBytes(bytes, offset + point[0].lengthInBytes()); } diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGcircle.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGcircle.java index 14c7df82..8e842126 100644 --- a/pgjdbc/src/main/java/org/postgresql/geometric/PGcircle.java +++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGcircle.java @@ -71,7 +71,11 @@ public class PGcircle extends PGobject implements Serializable, Cloneable { * @throws SQLException on conversion failure */ @Override - public void setValue(String s) throws SQLException { + public void setValue(@Nullable String s) throws SQLException { + if (s == null) { + center = null; + return; + } PGtokenizer t = new PGtokenizer(PGtokenizer.removeAngle(s), ','); if (t.getSize() != 2) { throw new PSQLException(GT.tr("Conversion to type {0} failed: {1}.", type, s), @@ -94,17 +98,26 @@ public class PGcircle extends PGobject implements Serializable, Cloneable { public boolean equals(@Nullable Object obj) { if (obj instanceof PGcircle) { PGcircle p = (PGcircle) obj; - return p.radius == radius && equals(p.center, center); + PGpoint center = this.center; + PGpoint pCenter = p.center; + if (center == null) { + return pCenter == null; + } else if (pCenter == null) { + return false; + } + + return p.radius == radius && equals(pCenter, center); } return false; } public int hashCode() { - long bits = Double.doubleToLongBits(radius); - int v = (int)(bits ^ (bits >>> 32)); - if (center != null) { - v = v * 31 + center.hashCode(); + if (center == null) { + return 0; } + long bits = Double.doubleToLongBits(radius); + int v = (int) (bits ^ (bits >>> 32)); + v = v * 31 + center.hashCode(); return v; } @@ -119,7 +132,7 @@ public class PGcircle extends PGobject implements Serializable, Cloneable { /** * @return the PGcircle in the syntax expected by org.postgresql */ - public String getValue() { - return "<" + center + "," + radius + ">"; + public @Nullable String getValue() { + return center == null ? null : "<" + center + "," + radius + ">"; } } diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGline.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGline.java index c2b98398..ca7f73f1 100644 --- a/pgjdbc/src/main/java/org/postgresql/geometric/PGline.java +++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGline.java @@ -36,6 +36,8 @@ public class PGline extends PGobject implements Serializable, Cloneable { */ public double c; + private boolean isNull; + /** * @param a coefficient of x * @param b coefficient of y @@ -54,31 +56,57 @@ public class PGline extends PGobject implements Serializable, Cloneable { * @param x2 coordinate for second point on the line * @param y2 coordinate for second point on the line */ + @SuppressWarnings("method.invocation.invalid") public PGline(double x1, double y1, double x2, double y2) { this(); - if (x1 == x2) { - a = -1; - b = 0; - } else { - a = (y2 - y1) / (x2 - x1); - b = -1; - } - c = y1 - a * x1; + setValue(x1, y1, x2, y2); } /** * @param p1 first point on the line * @param p2 second point on the line */ - public PGline(PGpoint p1, PGpoint p2) { - this(p1.x, p1.y, p2.x, p2.y); + @SuppressWarnings("method.invocation.invalid") + public PGline(@Nullable PGpoint p1, @Nullable PGpoint p2) { + this(); + setValue(p1, p2); } /** * @param lseg Line segment which calls on this line. */ - public PGline(PGlseg lseg) { - this(lseg.point[0], lseg.point[1]); + @SuppressWarnings("method.invocation.invalid") + public PGline(@Nullable PGlseg lseg) { + this(); + if (lseg == null) { + isNull = true; + return; + } + PGpoint[] point = lseg.point; + if (point == null) { + isNull = true; + return; + } + setValue(point[0], point[1]); + } + + private void setValue(@Nullable PGpoint p1, @Nullable PGpoint p2) { + if (p1 == null || p2 == null) { + isNull = true; + } else { + setValue(p1.x, p1.y, p2.x, p2.y); + } + } + + private void setValue(double x1, double y1, double x2, double y2) { + if (x1 == x2) { + a = -1; + b = 0; + } else { + a = (y2 - y1) / (x2 - x1); + b = -1; + } + c = y1 - a * x1; } /** @@ -103,7 +131,11 @@ public class PGline extends PGobject implements Serializable, Cloneable { * @throws SQLException on conversion failure */ @Override - public void setValue(String s) throws SQLException { + public void setValue(@Nullable String s) throws SQLException { + isNull = s == null; + if (s == null) { + return; + } if (s.trim().startsWith("{")) { PGtokenizer t = new PGtokenizer(PGtokenizer.removeCurlyBrace(s), ','); if (t.getSize() != 3) { @@ -143,6 +175,11 @@ public class PGline extends PGobject implements Serializable, Cloneable { } PGline pGline = (PGline) obj; + if (isNull) { + return pGline.isNull; + } else if (pGline.isNull) { + return false; + } return Double.compare(pGline.a, a) == 0 && Double.compare(pGline.b, b) == 0 @@ -150,6 +187,9 @@ public class PGline extends PGobject implements Serializable, Cloneable { } public int hashCode() { + if (isNull) { + return 0; + } int result = super.hashCode(); long temp; temp = Double.doubleToLongBits(a); @@ -164,8 +204,8 @@ public class PGline extends PGobject implements Serializable, Cloneable { /** * @return the PGline in the syntax expected by org.postgresql */ - public String getValue() { - return "{" + a + "," + b + "," + c + "}"; + public @Nullable String getValue() { + return isNull ? null : "{" + a + "," + b + "," + c + "}"; } @Override diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGlseg.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGlseg.java index 698fe40f..b8e667f7 100644 --- a/pgjdbc/src/main/java/org/postgresql/geometric/PGlseg.java +++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGlseg.java @@ -23,7 +23,7 @@ public class PGlseg extends PGobject implements Serializable, Cloneable { /** * These are the two points. */ - public PGpoint[] point = new PGpoint[2]; + public PGpoint @Nullable [] point; /** * @param x1 coordinate for first point @@ -41,8 +41,7 @@ public class PGlseg extends PGobject implements Serializable, Cloneable { */ public PGlseg(PGpoint p1, PGpoint p2) { this(); - this.point[0] = p1; - this.point[1] = p2; + point = new PGpoint[]{p1, p2}; } /** @@ -67,13 +66,21 @@ public class PGlseg extends PGobject implements Serializable, Cloneable { * @throws SQLException on conversion failure */ @Override - public void setValue(String s) throws SQLException { + public void setValue(@Nullable String s) throws SQLException { + if (s == null) { + point = null; + return; + } PGtokenizer t = new PGtokenizer(PGtokenizer.removeBox(s), ','); if (t.getSize() != 2) { throw new PSQLException(GT.tr("Conversion to type {0} failed: {1}.", type, s), PSQLState.DATA_TYPE_MISMATCH); } + PGpoint[] point = this.point; + if (point == null) { + this.point = point = new PGpoint[2]; + } point[0] = new PGpoint(t.getToken(0)); point[1] = new PGpoint(t.getToken(1)); } @@ -85,13 +92,24 @@ public class PGlseg extends PGobject implements Serializable, Cloneable { public boolean equals(@Nullable Object obj) { if (obj instanceof PGlseg) { PGlseg p = (PGlseg) obj; - return (p.point[0].equals(point[0]) && p.point[1].equals(point[1])) - || (p.point[0].equals(point[1]) && p.point[1].equals(point[0])); + PGpoint[] point = this.point; + PGpoint[] pPoint = p.point; + if (point == null) { + return pPoint == null; + } else if (pPoint == null) { + return false; + } + return (pPoint[0].equals(point[0]) && pPoint[1].equals(point[1])) + || (pPoint[0].equals(point[1]) && pPoint[1].equals(point[0])); } return false; } public int hashCode() { + PGpoint[] point = this.point; + if (point == null) { + return 0; + } return point[0].hashCode() ^ point[1].hashCode(); } @@ -111,7 +129,11 @@ public class PGlseg extends PGobject implements Serializable, Cloneable { /** * @return the PGlseg in the syntax expected by org.postgresql */ - public String getValue() { + public @Nullable String getValue() { + PGpoint[] point = this.point; + if (point == null) { + return null; + } return "[" + point[0] + "," + point[1] + "]"; } } diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGpath.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGpath.java index c263735b..68e290f6 100644 --- a/pgjdbc/src/main/java/org/postgresql/geometric/PGpath.java +++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGpath.java @@ -34,7 +34,7 @@ public class PGpath extends PGobject implements Serializable, Cloneable { * @param points the PGpoints that define the path * @param open True if the path is open, false if closed */ - public PGpath(PGpoint[] points, boolean open) { + public PGpath(PGpoint @Nullable [] points, boolean open) { this(); this.points = points; this.open = open; @@ -61,7 +61,11 @@ public class PGpath extends PGobject implements Serializable, Cloneable { * @param s Definition of the path in PostgreSQL's syntax * @throws SQLException on conversion failure */ - public void setValue(String s) throws SQLException { + public void setValue(@Nullable String s) throws SQLException { + if (s == null) { + points = null; + return; + } // First test to see if were open if (s.startsWith("[") && s.endsWith("]")) { open = true; @@ -91,24 +95,24 @@ public class PGpath extends PGobject implements Serializable, Cloneable { if (obj instanceof PGpath) { PGpath p = (PGpath) obj; - if (p.open != open) { + PGpoint[] points = this.points; + PGpoint[] pPoints = p.points; + if (points == null) { + return pPoints == null; + } else if (pPoints == null) { return false; } - if (points == null ^ p.points == null) { + if (p.open != open) { return false; } - if (points == null) { - return true; - } - - if (p.points.length != points.length) { + if (pPoints.length != points.length) { return false; } for (int i = 0; i < points.length; i++) { - if (!points[i].equals(p.points[i])) { + if (!points[i].equals(pPoints[i])) { return false; } } @@ -119,11 +123,12 @@ public class PGpath extends PGobject implements Serializable, Cloneable { } public int hashCode() { - // XXX not very good.. - int hash = open ? 1231 : 1237; + PGpoint[] points = this.points; if (points == null) { - return hash; + return 0; } + // XXX not very good.. + int hash = open ? 1231 : 1237; for (int i = 0; i < points.length && i < 5; ++i) { hash = hash * 31 + points[i].hashCode(); } @@ -144,12 +149,16 @@ public class PGpath extends PGobject implements Serializable, Cloneable { /** * This returns the path in the syntax expected by org.postgresql. + * @return the value of this object */ - public String getValue() { + public @Nullable String getValue() { + PGpoint[] points = this.points; + if (points == null) { + return null; + } StringBuilder b = new StringBuilder(open ? "[" : "("); - PGpoint[] points = this.points; - for (int p = 0; points != null && p < points.length; p++) { + for (int p = 0; p < points.length; p++) { if (p > 0) { b.append(","); } @@ -161,11 +170,11 @@ public class PGpath extends PGobject implements Serializable, Cloneable { } public boolean isOpen() { - return open; + return open && points != null; } public boolean isClosed() { - return !open; + return !open && points != null; } public void closePath() { diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGpoint.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGpoint.java index 4e1ebdb7..cc0af5f0 100644 --- a/pgjdbc/src/main/java/org/postgresql/geometric/PGpoint.java +++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGpoint.java @@ -35,6 +35,11 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C */ public double y; + /** + * True if the point represents {@code null::point}. + */ + public boolean isNull; + /** * @param x coordinate * @param y coordinate @@ -70,7 +75,11 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C * @throws SQLException on conversion failure */ @Override - public void setValue(String s) throws SQLException { + public void setValue(@Nullable String s) throws SQLException { + isNull = s == null; + if (s == null) { + return; + } PGtokenizer t = new PGtokenizer(PGtokenizer.removePara(s), ','); try { x = Double.parseDouble(t.getToken(0)); @@ -85,6 +94,7 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C * @param b Definition of this point in PostgreSQL's binary syntax */ public void setByteValue(byte[] b, int offset) { + this.isNull = false; x = ByteConverter.float8(b, offset); y = ByteConverter.float8(b, offset + 8); } @@ -96,12 +106,20 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C public boolean equals(@Nullable Object obj) { if (obj instanceof PGpoint) { PGpoint p = (PGpoint) obj; + if (isNull) { + return p.isNull; + } else if (p.isNull) { + return false; + } return x == p.x && y == p.y; } return false; } public int hashCode() { + if (isNull) { + return 0; + } long v1 = Double.doubleToLongBits(x); long v2 = Double.doubleToLongBits(y); return (int) (v1 ^ v2 ^ (v1 >>> 32) ^ (v2 >>> 32)); @@ -110,18 +128,21 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C /** * @return the PGpoint in the syntax expected by org.postgresql */ - public String getValue() { - return "(" + x + "," + y + ")"; + public @Nullable String getValue() { + return isNull ? null : "(" + x + "," + y + ")"; } public int lengthInBytes() { - return 16; + return isNull ? 0 : 16; } /** * Populate the byte array with PGpoint in the binary syntax expected by org.postgresql. */ public void toBytes(byte[] b, int offset) { + if (isNull) { + return; + } ByteConverter.float8(b, offset, x); ByteConverter.float8(b, offset + 8, y); } @@ -143,6 +164,7 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C * @param y double amount to add on the y axis */ public void translate(double x, double y) { + this.isNull = false; this.x += x; this.y += y; } @@ -164,6 +186,7 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C * @param y double coordinate */ public void move(double x, double y) { + this.isNull = false; this.x = x; this.y = y; } diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGpolygon.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGpolygon.java index 8726e5df..11ff85ed 100644 --- a/pgjdbc/src/main/java/org/postgresql/geometric/PGpolygon.java +++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGpolygon.java @@ -53,11 +53,17 @@ public class PGpolygon extends PGobject implements Serializable, Cloneable { * @param s Definition of the polygon in PostgreSQL's syntax * @throws SQLException on conversion failure */ - public void setValue(String s) throws SQLException { + public void setValue(@Nullable String s) throws SQLException { + if (s == null) { + points = null; + return; + } PGtokenizer t = new PGtokenizer(PGtokenizer.removePara(s), ','); int npoints = t.getSize(); - PGpoint[] points = new PGpoint[npoints]; - this.points = points; + PGpoint[] points = this.points; + if (points == null || points.length != npoints) { + this.points = points = new PGpoint[npoints]; + } for (int p = 0; p < npoints; p++) { points[p] = new PGpoint(t.getToken(p)); } @@ -71,20 +77,20 @@ public class PGpolygon extends PGobject implements Serializable, Cloneable { if (obj instanceof PGpolygon) { PGpolygon p = (PGpolygon) obj; - if (points == null ^ p.points == null) { - return false; - } - + PGpoint[] points = this.points; + PGpoint[] pPoints = p.points; if (points == null) { - return true; + return pPoints == null; + } else if (pPoints == null) { + return false; } - if (p.points.length != points.length) { + if (pPoints.length != points.length) { return false; } for (int i = 0; i < points.length; i++) { - if (!points[i].equals(p.points[i])) { + if (!points[i].equals(pPoints[i])) { return false; } } @@ -95,7 +101,6 @@ public class PGpolygon extends PGobject implements Serializable, Cloneable { } public int hashCode() { - // XXX not very good.. int hash = 0; PGpoint[] points = this.points; if (points == null) { @@ -124,11 +129,14 @@ public class PGpolygon extends PGobject implements Serializable, Cloneable { /** * @return the PGpolygon in the syntax expected by org.postgresql */ - public String getValue() { + public @Nullable String getValue() { + PGpoint[] points = this.points; + if (points == null) { + return null; + } StringBuilder b = new StringBuilder(); b.append("("); - PGpoint[] points = this.points; - for (int p = 0; points != null && p < points.length; p++) { + for (int p = 0; p < points.length; p++) { if (p > 0) { b.append(","); } diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java index 9968aa20..2b98f5e1 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java @@ -651,14 +651,14 @@ public class PgConnection implements BaseConnection { PGBinaryObject binObj = (PGBinaryObject) obj; binObj.setByteValue(byteValue, 0); } else { - obj.setValue(castNonNull(value)); + obj.setValue(value); } } else { // If className is null, then the type is unknown. // so return a PGobject with the type set, and the value set obj = new PGobject(); obj.setType(type); - obj.setValue(castNonNull(value)); + obj.setValue(value); } return obj; diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java index 0aeb7e72..221760e0 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java @@ -489,7 +489,12 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement { if ((x instanceof PGBinaryObject) && connection.binaryTransferSend(oid)) { PGBinaryObject binObj = (PGBinaryObject) x; - byte[] data = new byte[binObj.lengthInBytes()]; + int length = binObj.lengthInBytes(); + if (length == 0) { + preparedParameters.setNull(parameterIndex, oid); + return; + } + byte[] data = new byte[length]; binObj.toBytes(data, 0); bindBytes(parameterIndex, data, oid); } else { diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java b/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java index 37d8bb39..c28f9fe6 100644 --- a/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java +++ b/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java @@ -30,6 +30,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { private int minutes; private int wholeSeconds; private int microSeconds; + private boolean isNull; /** * required by the driver. @@ -135,7 +136,13 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param value String representated interval (e.g. '3 years 2 mons') * @throws SQLException Is thrown if the string representation has an unknown format */ - public void setValue(String value) throws SQLException { + public void setValue(@Nullable String value) throws SQLException { + isNull = value == null; + if (value == null) { + setValue(0, 0, 0, 0, 0, 0); + isNull = true; + return; + } final boolean PostgresFormat = !value.startsWith("@"); if (value.startsWith("P")) { parseISO8601Format(value); @@ -247,7 +254,10 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * * @return String represented interval */ - public String getValue() { + public @Nullable String getValue() { + if (isNull) { + return null; + } DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(Locale.US); df.applyPattern("0.0#####"); @@ -278,6 +288,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param years years to set */ public void setYears(int years) { + isNull = false; this.years = years; } @@ -296,6 +307,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param months months to set */ public void setMonths(int months) { + isNull = false; this.months = months; } @@ -314,6 +326,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param days days to set */ public void setDays(int days) { + isNull = false; this.days = days; } @@ -332,6 +345,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param hours hours to set */ public void setHours(int hours) { + isNull = false; this.hours = hours; } @@ -350,6 +364,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param minutes minutes to set */ public void setMinutes(int minutes) { + isNull = false; this.minutes = minutes; } @@ -376,6 +391,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param seconds seconds to set */ public void setSeconds(double seconds) { + isNull = false; wholeSeconds = (int) seconds; microSeconds = (int) Math.round((seconds - wholeSeconds) * MICROS_IN_SECOND); } @@ -386,6 +402,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param cal Calendar instance to add to */ public void add(Calendar cal) { + if (isNull) { + return; + } final int milliseconds = (microSeconds + ((microSeconds < 0) ? -500 : 500)) / 1000 + wholeSeconds * 1000; @@ -403,6 +422,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param date Date instance to add to */ public void add(Date date) { + if (isNull) { + return; + } final Calendar cal = Calendar.getInstance(); cal.setTime(date); add(cal); @@ -416,6 +438,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param interval intval to add */ public void add(PGInterval interval) { + if (isNull || interval.isNull) { + return; + } interval.setYears(interval.getYears() + getYears()); interval.setMonths(interval.getMonths() + getMonths()); interval.setDays(interval.getDays() + getDays()); @@ -433,6 +458,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { * @param factor scale factor */ public void scale(int factor) { + if (isNull) { + return; + } setYears(factor * getYears()); setMonths(factor * getMonths()); setDays(factor * getDays()); @@ -483,6 +511,11 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { } final PGInterval pgi = (PGInterval) obj; + if (isNull) { + return pgi.isNull; + } else if (pgi.isNull) { + return false; + } return pgi.years == years && pgi.months == months @@ -500,6 +533,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable { */ @Override public int hashCode() { + if (isNull) { + return 0; + } return (((((((8 * 31 + microSeconds) * 31 + wholeSeconds) * 31 + minutes) * 31 + hours) * 31 + days) * 31 + months) * 31 + years) * 31; } diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGmoney.java b/pgjdbc/src/main/java/org/postgresql/util/PGmoney.java index 8607d19b..249dc240 100644 --- a/pgjdbc/src/main/java/org/postgresql/util/PGmoney.java +++ b/pgjdbc/src/main/java/org/postgresql/util/PGmoney.java @@ -19,6 +19,11 @@ public class PGmoney extends PGobject implements Serializable, Cloneable { */ public double val; + /** + * If the object represents {@code null::money} + */ + public boolean isNull; + /** * @param value of field */ @@ -40,7 +45,11 @@ public class PGmoney extends PGobject implements Serializable, Cloneable { type = "money"; } - public void setValue(String s) throws SQLException { + public void setValue(@Nullable String s) throws SQLException { + isNull = s == null; + if (s == null) { + return; + } try { String s1; boolean negative; @@ -68,6 +77,9 @@ public class PGmoney extends PGobject implements Serializable, Cloneable { @Override public int hashCode() { + if (isNull) { + return 0; + } final int prime = 31; int result = super.hashCode(); long temp; @@ -79,12 +91,20 @@ public class PGmoney extends PGobject implements Serializable, Cloneable { public boolean equals(@Nullable Object obj) { if (obj instanceof PGmoney) { PGmoney p = (PGmoney) obj; + if (isNull) { + return p.isNull; + } else if (p.isNull) { + return false; + } return val == p.val; } return false; } - public String getValue() { + public @Nullable String getValue() { + if (isNull) { + return null; + } if (val < 0) { return "-$" + (-val); } else { diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGobject.java b/pgjdbc/src/main/java/org/postgresql/util/PGobject.java index 291ce93f..e52b786d 100644 --- a/pgjdbc/src/main/java/org/postgresql/util/PGobject.java +++ b/pgjdbc/src/main/java/org/postgresql/util/PGobject.java @@ -43,7 +43,7 @@ public class PGobject implements Serializable, Cloneable { * @param value a string representation of the value of the object * @throws SQLException thrown if value is invalid for this type */ - public void setValue(String value) throws SQLException { + public void setValue(@Nullable String value) throws SQLException { this.value = value; } @@ -53,7 +53,7 @@ public class PGobject implements Serializable, Cloneable { * @return the type name of this object */ public final String getType() { - return castNonNull(type); + return castNonNull(type, "PGobject#type is uninitialized. Please call setType(String)"); } /** @@ -66,6 +66,16 @@ public class PGobject implements Serializable, Cloneable { return value; } + /** + * Returns true if the current object wraps `null` value. + * This might be helpful + * + * @return true if the current object wraps `null` value. + */ + public boolean isNull() { + return getValue() != null; + } + /** * This must be overidden to allow comparisons of objects. * diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectGetTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectGetTest.java new file mode 100644 index 00000000..c59abcfc --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectGetTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.test.jdbc2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.postgresql.geometric.PGbox; +import org.postgresql.geometric.PGcircle; +import org.postgresql.geometric.PGline; +import org.postgresql.geometric.PGlseg; +import org.postgresql.geometric.PGpath; +import org.postgresql.geometric.PGpoint; +import org.postgresql.geometric.PGpolygon; +import org.postgresql.util.PGInterval; +import org.postgresql.util.PGmoney; +import org.postgresql.util.PGobject; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class PGObjectGetTest extends BaseTest4 { + private final String sqlExpression; + private final Class<? extends PGobject> type; + private final String expected; + private final String stringValue; + + public PGObjectGetTest(BinaryMode binaryMode, String sqlExpression, + Class<? extends PGobject> type, String expected, String stringValue) { + setBinaryMode(binaryMode); + this.sqlExpression = sqlExpression; + this.type = type; + this.expected = expected; + this.stringValue = stringValue; + } + + @Parameterized.Parameters(name = "binary = {0}, sql = {1}, type = {2}") + public static Iterable<Object[]> data() { + Collection<Object[]> ids = new ArrayList<Object[]>(); + for (BinaryMode binaryMode : BinaryMode.values()) { + ids.add(new Object[]{binaryMode, "null::inet", PGobject.class, + "PGobject(type=inet, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::box", PGbox.class, + "PGbox(type=box, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::circle", PGcircle.class, + "PGcircle(type=circle, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::line", PGline.class, + "PGline(type=line, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::lseg", PGlseg.class, + "PGlseg(type=lseg, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::path", PGpath.class, + "PGpath(type=path, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::point", PGpoint.class, + "PGpoint(type=point, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::polygon", PGpolygon.class, + "PGpolygon(type=polygon, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::money", PGmoney.class, + "PGmoney(type=money, value=null)", null}); + ids.add(new Object[]{binaryMode, "null::interval", PGInterval.class, + "PGInterval(type=interval, value=null)", null}); + } + return ids; + } + + @Test + public void getAsPGobject() throws SQLException { + testGet(sqlExpression, expected, PGobject.class); + } + + @Test + public void getAsPGobjectSubtype() throws SQLException { + testGet(sqlExpression, expected, type); + } + + @Test + public void getAsString() throws SQLException { + PreparedStatement ps = con.prepareStatement("select " + sqlExpression); + ResultSet rs = ps.executeQuery(); + rs.next(); + assertEquals( + "'" + sqlExpression + "'.getString(1)", + stringValue, + rs.getString(1) + ); + } + + private void testGet(final String s, String expected, Class<? extends PGobject> type) throws SQLException { + PreparedStatement ps = con.prepareStatement("select " + s); + ResultSet rs = ps.executeQuery(); + rs.next(); + assertEquals( + "'" + s + "'.getObject(1, " + type.getSimpleName() + ".class)", + expected, + printObject(rs.getObject(1, type)) + ); + if (expected.contains("value=null)")) { + // For some reason we return objects as nulls + assertNull( + "'select " + s + "'.getObject(1)", + rs.getObject(1) + ); + } else { + assertEquals( + "'select " + s + "'.getObject(1)", + expected, + printObject(rs.getObject(1)) + ); + } + } + + String printObject(@Nullable Object object) { + if (!(object instanceof PGobject)) { + return String.valueOf(object); + } + PGobject pg = (PGobject) object; + return pg.getClass().getSimpleName() + "(type=" + pg.getType() + ", value=" + pg.getValue() + ")"; + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectSetTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectSetTest.java new file mode 100644 index 00000000..7ee8c400 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectSetTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2020, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.test.jdbc2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.postgresql.geometric.PGbox; +import org.postgresql.geometric.PGcircle; +import org.postgresql.geometric.PGline; +import org.postgresql.geometric.PGlseg; +import org.postgresql.geometric.PGpath; +import org.postgresql.geometric.PGpoint; +import org.postgresql.geometric.PGpolygon; +import org.postgresql.util.PGInterval; +import org.postgresql.util.PGmoney; +import org.postgresql.util.PGobject; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.lang.reflect.InvocationTargetException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class PGObjectSetTest extends BaseTest4 { + private final String typeName; + private final String expected; + private final Class<? extends PGobject> type; + + public PGObjectSetTest(BinaryMode binaryMode, Class<? extends PGobject> type, + String typeName, String expected) { + setBinaryMode(binaryMode); + this.expected = expected; + this.type = type; + this.typeName = typeName; + } + + @Parameterized.Parameters(name = "binary = {0}, sql = {2}, type = {1}") + public static Iterable<Object[]> data() { + Collection<Object[]> ids = new ArrayList<Object[]>(); + for (BinaryMode binaryMode : BinaryMode.values()) { + ids.add(new Object[]{binaryMode, PGobject.class, "inet", + "PGobject(type=inet, value=null)"}); + ids.add(new Object[]{binaryMode, PGbox.class, "box", + "PGbox(type=box, value=null)"}); + ids.add(new Object[]{binaryMode, PGcircle.class, "circle", + "PGcircle(type=circle, value=null)"}); + ids.add(new Object[]{binaryMode, PGline.class, "line", + "PGline(type=line, value=null)"}); + ids.add(new Object[]{binaryMode, PGlseg.class, "lseg", + "PGlseg(type=lseg, value=null)"}); + ids.add(new Object[]{binaryMode, PGpath.class, "path", + "PGpath(type=path, value=null)"}); + ids.add(new Object[]{binaryMode, PGpoint.class, "point", + "PGpoint(type=point, value=null)"}); + ids.add(new Object[]{binaryMode, PGpolygon.class, "polygon", + "PGpolygon(type=polygon, value=null)"}); + ids.add(new Object[]{binaryMode, PGmoney.class, "money", + "PGmoney(type=money, value=null)"}); + ids.add(new Object[]{binaryMode, PGInterval.class, "interval", + "PGInterval(type=interval, value=null)"}); + } + return ids; + } + + @Test + public void setNullAsPGobject() throws SQLException { + PGobject object = new PGobject(); + object.setType(typeName); + object.setValue(null); + testSet(object, expected, PGobject.class); + } + + @Test + public void setNullAsPGobjectSubtype() throws SQLException, NoSuchMethodException, + IllegalAccessException, InvocationTargetException, InstantiationException { + if (type == PGobject.class) { + // We can't use PGobject without setType + return; + } + PGobject object = type.getConstructor().newInstance(); + object.setValue(null); + testSet(object, expected, type); + } + + private void testSet(PGobject value, String expected, Class<? extends PGobject> type) throws SQLException { + PreparedStatement ps = con.prepareStatement("select ?::" + value.getType()); + ps.setObject(1, value); + ResultSet rs = ps.executeQuery(); + rs.next(); + assertEquals( + "'select ?::" + value.getType() + "'.withParam(" + printObject(value) + ").getObject(1, " + type.getSimpleName() + ".class)", + expected, + printObject(rs.getObject(1, type)) + ); + if (expected.contains("value=null)")) { + assertNull( + "'select ?::" + value.getType() + "'.withParam(" + printObject(value) + ").getObject(1)", + rs.getObject(1) + ); + } else { + assertEquals( + "'select ?::" + value.getType() + "'.withParam(" + printObject(value) + ").getObject(1)", + expected, + printObject(rs.getObject(1)) + ); + } + } + + String printObject(@Nullable Object object) { + if (!(object instanceof PGobject)) { + return String.valueOf(object); + } + PGobject pg = (PGobject) object; + return pg.getClass().getSimpleName() + "(type=" + pg.getType() + ", value=" + pg.getValue() + ")"; + } +} [parent: a2085ca41c9b]