From 7a0ac84fb1245a05ecd06f25763179bedaf2d060 Mon Sep 17 00:00:00 2001 From: Zhen Date: Fri, 9 Mar 2018 15:19:59 +0100 Subject: [PATCH] Return type support of `Temporal` and `Point` in Rest endpoint Return `Temporal` and `TemporalAmount` as ISO-8601 standard string. Return type info in meta data section --- .../rest/transactional/Neo4jJsonCodec.java | 113 ++++++++++++++- .../rest/AbstractRestFunctionalTestBase.java | 14 ++ .../transactional/Neo4jJsonCodecTest.java | 12 ++ .../integration/PointTypeIT.java | 20 ++- .../integration/TemporalTypeIT.java | 137 ++++++++++++++++++ 5 files changed, 277 insertions(+), 19 deletions(-) create mode 100644 community/server/src/test/java/org/neo4j/server/rest/transactional/integration/TemporalTypeIT.java diff --git a/community/server/src/main/java/org/neo4j/server/rest/transactional/Neo4jJsonCodec.java b/community/server/src/main/java/org/neo4j/server/rest/transactional/Neo4jJsonCodec.java index 6ca59bac81701..ef72c6b5e948a 100644 --- a/community/server/src/main/java/org/neo4j/server/rest/transactional/Neo4jJsonCodec.java +++ b/community/server/src/main/java/org/neo4j/server/rest/transactional/Neo4jJsonCodec.java @@ -24,6 +24,13 @@ import org.codehaus.jackson.map.SerializationConfig; import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAmount; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; @@ -37,11 +44,17 @@ import org.neo4j.graphdb.spatial.Coordinate; import org.neo4j.graphdb.spatial.Geometry; import org.neo4j.graphdb.spatial.Point; +import org.neo4j.values.storable.PointValue; import static org.neo4j.helpers.collection.MapUtil.genericMap; public class Neo4jJsonCodec extends ObjectMapper { + private enum Neo4jJsonMetaType + { + node, relationship, datetime, time, localdatetime, date, localtime, duration, point2d, point3d + } + private TransitionalPeriodTransactionMessContainer container; public Neo4jJsonCodec( TransitionalPeriodTransactionMessContainer container ) @@ -88,20 +101,22 @@ else if ( value instanceof Geometry ) { Geometry geom = (Geometry) value; Object coordinates = (geom instanceof Point) ? ((Point) geom).getCoordinate() : geom.getCoordinates(); - writeMap( out, - genericMap( new LinkedHashMap<>(), "type", geom.getGeometryType(), - "coordinates", coordinates, "crs", geom.getCRS() ) ); + writeMap( out, genericMap( new LinkedHashMap<>(), "type", geom.getGeometryType(), "coordinates", coordinates, "crs", geom.getCRS() ) ); } else if ( value instanceof Coordinate ) { Coordinate coordinate = (Coordinate) value; - writeIterator( out, coordinate.getCoordinate().iterator()); + writeIterator( out, coordinate.getCoordinate().iterator() ); } else if ( value instanceof CRS ) { CRS crs = (CRS) value; - writeMap( out, genericMap(new LinkedHashMap<>(), "name", crs.getType(), "type", "link", "properties", - genericMap(new LinkedHashMap<>(), "href", crs.getHref() + "ogcwkt/", "type", "ogcwkt" ) ) ); + writeMap( out, genericMap( new LinkedHashMap<>(), "name", crs.getType(), "type", "link", "properties", + genericMap( new LinkedHashMap<>(), "href", crs.getHref() + "ogcwkt/", "type", "ogcwkt" ) ) ); + } + else if ( value instanceof Temporal || value instanceof TemporalAmount ) + { + super.writeValue( out, value.toString() ); } else { @@ -223,7 +238,7 @@ void writeMeta( JsonGenerator out, Object value ) throws IOException Node node = (Node) value; try ( TransactionStateChecker stateChecker = TransactionStateChecker.create( container ) ) { - writeNodeOrRelationshipMeta( out, node.getId(), "node", stateChecker.isNodeDeletedInCurrentTx( node.getId() ) ); + writeNodeOrRelationshipMeta( out, node.getId(), Neo4jJsonMetaType.node.name(), stateChecker.isNodeDeletedInCurrentTx( node.getId() ) ); } } else if ( value instanceof Relationship ) @@ -231,7 +246,7 @@ else if ( value instanceof Relationship ) Relationship relationship = (Relationship) value; try ( TransactionStateChecker transactionStateChecker = TransactionStateChecker.create( container ) ) { - writeNodeOrRelationshipMeta( out, relationship.getId(), "relationship", + writeNodeOrRelationshipMeta( out, relationship.getId(), Neo4jJsonMetaType.relationship.name(), transactionStateChecker.isRelationshipDeletedInCurrentTx( relationship.getId() ) ); } } @@ -254,12 +269,80 @@ else if ( value instanceof Map ) writeMeta( out, map.get( key ) ); } } + else if ( value instanceof Geometry ) + { + writeGeometryTypeMeta( out, (Geometry) value ); + } + else if ( value instanceof Temporal ) + { + writeTemporalTypeMeta( out, (Temporal) value ); + } + else if ( value instanceof TemporalAmount ) + { + writeTypeMeta( out, Neo4jJsonMetaType.duration.name() ); + } else { out.writeNull(); } } + private void writeGeometryTypeMeta( JsonGenerator out, Geometry value ) throws IOException + { + Neo4jJsonMetaType type = null; + if ( value instanceof PointValue ) + { + PointValue p = (PointValue) value; + int size = p.coordinate().length; + if ( size == 2 ) + { + type = Neo4jJsonMetaType.point2d; + } + else if ( size == 3 ) + { + type = Neo4jJsonMetaType.point3d; + } + } + if ( type == null ) + { + throw new IllegalArgumentException( + String.format( "Unsupported Geometry type: type=%s, value=%s", value.getClass().getSimpleName(), value.toString() ) ); + } + writeTypeMeta( out, type.name() ); + } + + private void writeTemporalTypeMeta( JsonGenerator out, Temporal value ) throws IOException + { + Neo4jJsonMetaType type = null; + if ( value instanceof ZonedDateTime ) + { + type = Neo4jJsonMetaType.datetime; + } + else if ( value instanceof LocalDate ) + { + type = Neo4jJsonMetaType.date; + } + else if ( value instanceof OffsetTime ) + { + type = Neo4jJsonMetaType.time; + } + else if ( value instanceof LocalDateTime ) + { + type = Neo4jJsonMetaType.localdatetime; + } + else if ( value instanceof LocalTime ) + { + type = Neo4jJsonMetaType.localtime; + } + + if ( type == null ) + { + throw new IllegalArgumentException( + String.format( "Unsupported Temporal type: type=%s, value=%s", value.getClass().getSimpleName(), value.toString() ) ); + } + writeTypeMeta( out, type.name() ); + } + private void writeMetaPath( JsonGenerator out, Path value ) throws IOException { out.writeStartArray(); @@ -276,6 +359,20 @@ private void writeMetaPath( JsonGenerator out, Path value ) throws IOException } } + private void writeTypeMeta( JsonGenerator out, String type ) + throws IOException + { + out.writeStartObject(); + try + { + out.writeStringField( "type", type ); + } + finally + { + out.writeEndObject(); + } + } + private void writeNodeOrRelationshipMeta( JsonGenerator out, long id, String type, boolean isDeleted ) throws IOException { diff --git a/community/server/src/test/java/org/neo4j/server/rest/AbstractRestFunctionalTestBase.java b/community/server/src/test/java/org/neo4j/server/rest/AbstractRestFunctionalTestBase.java index 6f1de8d9bc978..f3fed15323c61 100644 --- a/community/server/src/test/java/org/neo4j/server/rest/AbstractRestFunctionalTestBase.java +++ b/community/server/src/test/java/org/neo4j/server/rest/AbstractRestFunctionalTestBase.java @@ -52,6 +52,8 @@ import static org.neo4j.server.rest.web.Surface.PATH_RELATIONSHIP_INDEX; import static org.neo4j.server.rest.web.Surface.PATH_SCHEMA_CONSTRAINT; import static org.neo4j.server.rest.web.Surface.PATH_SCHEMA_INDEX; +import static org.neo4j.test.server.HTTP.POST; +import static org.neo4j.test.server.HTTP.RawPayload.quotedJson; public class AbstractRestFunctionalTestBase extends SharedServerTestBase implements GraphHolder { @@ -284,4 +286,16 @@ public static int getLocalHttpPort() .resolveDependency( ConnectorPortRegister.class ); return connectorPortRegister.getLocalAddress( "http" ).getPort(); } + + + public static HTTP.Response runQuery( String query ) + { + return POST( txCommitUri(), quotedJson( "{'statements': [{'statement': '" + query + "'}]}" ) ); + } + + public static void assertNoErrors( HTTP.Response response ) throws JsonParseException + { + assertEquals( "[]", response.get( "errors" ).toString() ); + assertEquals( 0, response.get( "errors" ).size() ); + } } diff --git a/community/server/src/test/java/org/neo4j/server/rest/transactional/Neo4jJsonCodecTest.java b/community/server/src/test/java/org/neo4j/server/rest/transactional/Neo4jJsonCodecTest.java index 6f4142b922a41..01046ec420437 100644 --- a/community/server/src/test/java/org/neo4j/server/rest/transactional/Neo4jJsonCodecTest.java +++ b/community/server/src/test/java/org/neo4j/server/rest/transactional/Neo4jJsonCodecTest.java @@ -296,4 +296,16 @@ public void testGeometryWriting() throws IOException //Then verify( jsonGenerator, times( 3 ) ).writeEndObject(); } + + @Test + public void testDateTimeWriting() throws Throwable + { + // Given + + + + // When + + // Then + } } diff --git a/community/server/src/test/java/org/neo4j/server/rest/transactional/integration/PointTypeIT.java b/community/server/src/test/java/org/neo4j/server/rest/transactional/integration/PointTypeIT.java index b53077eb3da49..fa321cc9756dd 100644 --- a/community/server/src/test/java/org/neo4j/server/rest/transactional/integration/PointTypeIT.java +++ b/community/server/src/test/java/org/neo4j/server/rest/transactional/integration/PointTypeIT.java @@ -38,8 +38,6 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.neo4j.graphdb.Label.label; -import static org.neo4j.test.server.HTTP.POST; -import static org.neo4j.test.server.HTTP.RawPayload.quotedJson; import static org.neo4j.values.storable.CoordinateReferenceSystem.Cartesian; import static org.neo4j.values.storable.CoordinateReferenceSystem.WGS84; @@ -74,16 +72,16 @@ public void shouldWorkWithPoint2DArrays() throws Exception @Test public void shouldReturnPoint2DWithXAndY() throws Exception { - testPoint( "RETURN point({x: 42.05, y: 90.99})", new double[]{42.05, 90.99}, Cartesian ); + testPoint( "RETURN point({x: 42.05, y: 90.99})", new double[]{42.05, 90.99}, Cartesian, "point2d" ); } @Test public void shouldReturnPoint2DWithLatitudeAndLongitude() throws Exception { - testPoint( "RETURN point({longitude: 56.7, latitude: 12.78})", new double[]{56.7, 12.78}, WGS84 ); + testPoint( "RETURN point({longitude: 56.7, latitude: 12.78})", new double[]{56.7, 12.78}, WGS84, "point2d" ); } - private static void testPoint( String query, double[] expectedCoordinate, CoordinateReferenceSystem expectedCrs ) throws Exception + private static void testPoint( String query, double[] expectedCoordinate, CoordinateReferenceSystem expectedCrs, String expectedType ) throws Exception { HTTP.Response response = runQuery( query ); @@ -94,16 +92,16 @@ private static void testPoint( String query, double[] expectedCoordinate, Coordi assertGeometryTypeEqual( GeometryType.GEOMETRY_POINT, element ); assertCoordinatesEqual( expectedCoordinate, element ); assertCrsEqual( expectedCrs, element ); - } - private static HTTP.Response runQuery( String query ) - { - return POST( txCommitUri(), quotedJson( "{'statements': [{'statement': '" + query + "'}]}" ) ); + assertTypeEqual( expectedType, response ); } - private static void assertNoErrors( HTTP.Response response ) throws JsonParseException + private static void assertTypeEqual( String expectedType, HTTP.Response response ) throws JsonParseException { - assertEquals( 0, response.get( "errors" ).size() ); + JsonNode data = response.get( "results" ).get( 0 ).get( "data" ); + JsonNode meta = data.get( 0 ).get( "meta" ); + assertEquals( 1, meta.size() ); + assertEquals( expectedType, meta.get( 0 ).get( "type" ).asText() ); } private static JsonNode extractSingleElement( HTTP.Response response ) throws JsonParseException diff --git a/community/server/src/test/java/org/neo4j/server/rest/transactional/integration/TemporalTypeIT.java b/community/server/src/test/java/org/neo4j/server/rest/transactional/integration/TemporalTypeIT.java new file mode 100644 index 0000000000000..3a773e30ca40b --- /dev/null +++ b/community/server/src/test/java/org/neo4j/server/rest/transactional/integration/TemporalTypeIT.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.server.rest.transactional.integration; + +import org.codehaus.jackson.JsonNode; +import org.junit.Test; + +import org.neo4j.server.rest.AbstractRestFunctionalTestBase; +import org.neo4j.server.rest.domain.JsonParseException; +import org.neo4j.test.server.HTTP; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; + +public class TemporalTypeIT extends AbstractRestFunctionalTestBase +{ + @Test + public void shouldWorkWithDateTime() throws Throwable + { + HTTP.Response response = runQuery( "RETURN datetime({year: 1, month:10, day:2, timezone:\\\"+01:00\\\"})" ); + + assertEquals( 200, response.status() ); + assertNoErrors( response ); + JsonNode data = getSingleData( response ); + assertEquals( "0001-10-02T00:00+01:00", getSingle( data, "row" ).asText() ); + assertEquals( "datetime", getSingle( data, "meta" ).get( "type" ).asText() ); + } + + @Test + public void shouldWorkWithTime() throws Throwable + { + HTTP.Response response = runQuery( "RETURN time({hour: 23, minute: 19, second: 55, timezone:\\\"-07:00\\\"})" ); + + assertEquals( 200, response.status() ); + assertNoErrors( response ); + JsonNode data = getSingleData( response ); + assertEquals( "23:19:55-07:00", getSingle( data, "row" ).asText() ); + assertEquals( "time", getSingle( data, "meta" ).get( "type" ).asText() ); + } + + @Test + public void shouldWorkWithLocalDateTime() throws Throwable + { + HTTP.Response response = runQuery( "RETURN localdatetime({year: 1984, month: 10, day: 21, hour: 12, minute: 34})" ); + + assertEquals( 200, response.status() ); + assertNoErrors( response ); + JsonNode data = getSingleData( response ); + assertEquals( "1984-10-21T12:34", getSingle( data, "row" ).asText() ); + assertEquals( "localdatetime", getSingle( data, "meta" ).get( "type" ).asText() ); + } + + @Test + public void shouldWorkWithDate() throws Throwable + { + HTTP.Response response = runQuery( "RETURN date({year: 1984, month: 10, day: 11})" ); + + assertEquals( 200, response.status() ); + assertNoErrors( response ); + JsonNode data = getSingleData( response ); + assertEquals( "1984-10-11", getSingle( data, "row" ).asText() ); + assertEquals( "date", getSingle( data, "meta" ).get( "type" ).asText() ); + } + + @Test + public void shouldWorkWithLocalTime() throws Throwable + { + HTTP.Response response = runQuery( "RETURN localtime({hour:12, minute:31, second:14, nanosecond: 645876123})" ); + + assertEquals( 200, response.status() ); + assertNoErrors( response ); + JsonNode data = getSingleData( response ); + assertEquals( "12:31:14.645876123", getSingle( data, "row" ).asText() ); + assertEquals( "localtime", getSingle( data, "meta" ).get( "type" ).asText() ); + } + + @Test + public void shouldWorkWithDuration() throws Throwable + { + HTTP.Response response = runQuery( "RETURN duration({weeks:2, days:3})" ); + + assertEquals( 200, response.status() ); + assertNoErrors( response ); + JsonNode data = getSingleData( response ); + assertEquals( "P17D", getSingle( data, "row" ).asText() ); + assertEquals( "duration", getSingle( data, "meta" ).get( "type" ).asText() ); + } + + @Test + public void shouldOnlyGetNodeTypeInMetaAsNodeProperties() throws Throwable + { + HTTP.Response response = + runQuery( "CREATE (account {creationTime: localdatetime({year: 1984, month: 10, day: 21, hour: 12, minute: 34})}) RETURN account" ); + + assertEquals( 200, response.status() ); + assertNoErrors( response ); + JsonNode data = getSingleData( response ); + + JsonNode row = getSingle( data, "row" ); + assertThat( row.get( "creationTime" ).asText(), equalTo( "1984-10-21T12:34" ) ); + + JsonNode meta = getSingle( data, "meta" ); + assertThat( meta.get( "type" ).asText(), equalTo( "node" ) ); + } + + private static JsonNode getSingleData( HTTP.Response response ) throws JsonParseException + { + JsonNode data = response.get( "results" ).get( 0 ).get( "data" ); + assertEquals( 1, data.size() ); + return data.get( 0 ); + } + + private static JsonNode getSingle( JsonNode node, String key ) + { + JsonNode data = node.get( key ); + assertEquals( 1, data.size() ); + return data.get( 0 ); + } +}