From c60eac070cad43c10913c4c063d73f2e1c6e8c13 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 5 Feb 2019 22:43:59 +0100 Subject: [PATCH 001/342] initial empty commit From 8d656aff85b8c9fe30d7951ef6bf9630b134b1f5 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 5 Feb 2019 22:44:07 +0100 Subject: [PATCH 002/342] Create project draft for sdn-rx. --- .gitignore | 29 ++ .mvn/wrapper/MavenWrapperDownloader.java | 110 +++++++ .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes .mvn/wrapper/maven-wrapper.properties | 1 + etc/checkstyle/config.xml | 107 +++++++ etc/checkstyle/java-header.txt | 20 ++ etc/checkstyle/suppressions.xml | 7 + lombok.config | 2 + mvnw | 286 ++++++++++++++++++ mvnw.cmd | 161 ++++++++++ pom.xml | 261 ++++++++++++++++ spring-data-neo4j/pom.xml | 87 ++++++ .../core/mapping/Neo4jMappingContext.java | 53 ++++ .../core/mapping/Neo4jPersistentEntity.java | 29 ++ .../core/mapping/Neo4jPersistentProperty.java | 29 ++ .../data/neo4j/core/mapping/Node.java | 49 +++ .../data/neo4j/core/mapping/Property.java | 38 +++ .../data/neo4j/core/mapping/package-info.java | 9 + .../neo4j/repository/Neo4jRepository.java | 32 ++ .../repository/ReactiveNeo4jRepository.java | 33 ++ .../data/neo4j/repository/package-info.java | 7 + .../core/mapping/Neo4jMappingContextTest.java | 36 +++ 22 files changed, 1386 insertions(+) create mode 100644 .gitignore create mode 100755 .mvn/wrapper/MavenWrapperDownloader.java create mode 100755 .mvn/wrapper/maven-wrapper.jar create mode 100755 .mvn/wrapper/maven-wrapper.properties create mode 100644 etc/checkstyle/config.xml create mode 100644 etc/checkstyle/java-header.txt create mode 100644 etc/checkstyle/suppressions.xml create mode 100644 lombok.config create mode 100755 mvnw create mode 100755 mvnw.cmd create mode 100644 pom.xml create mode 100644 spring-data-neo4j/pom.xml create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Node.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Property.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/Neo4jRepository.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/ReactiveNeo4jRepository.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e46643fc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### Misc ### +.DS_Store + diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 000000000..fa4f7b499 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +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. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..01e67997377a393fd672c7dcde9dccbedf0cb1e9 GIT binary patch literal 48337 zcmbTe1CV9Qwl>;j+wQV$+qSXFw%KK)%eHN!%U!l@+x~l>b1vR}@9y}|TM-#CBjy|< zb7YRpp)Z$$Gzci_H%LgxZ{NNV{%Qa9gZlF*E2<($D=8;N5Asbx8se{Sz5)O13x)rc z5cR(k$_mO!iis+#(8-D=#R@|AF(8UQ`L7dVNSKQ%v^P|1A%aF~Lye$@HcO@sMYOb3 zl`5!ThJ1xSJwsg7hVYFtE5vS^5UE0$iDGCS{}RO;R#3y#{w-1hVSg*f1)7^vfkxrm!!N|oTR0Hj?N~IbVk+yC#NK} z5myv()UMzV^!zkX@O=Yf!(Z_bF7}W>k*U4@--&RH0tHiHY0IpeezqrF#@8{E$9d=- z7^kT=1Bl;(Q0k{*_vzz1Et{+*lbz%mkIOw(UA8)EE-Pkp{JtJhe@VXQ8sPNTn$Vkj zicVp)sV%0omhsj;NCmI0l8zzAipDV#tp(Jr7p_BlL$}Pys_SoljztS%G-Wg+t z&Q#=<03Hoga0R1&L!B);r{Cf~b$G5p#@?R-NNXMS8@cTWE^7V!?ixz(Ag>lld;>COenWc$RZ61W+pOW0wh>sN{~j; zCBj!2nn|4~COwSgXHFH?BDr8pK323zvmDK-84ESq25b;Tg%9(%NneBcs3;r znZpzntG%E^XsSh|md^r-k0Oen5qE@awGLfpg;8P@a-s<{Fwf?w3WapWe|b-CQkqlo z46GmTdPtkGYdI$e(d9Zl=?TU&uv94VR`g|=7xB2Ur%=6id&R2 z4e@fP7`y58O2sl;YBCQFu7>0(lVt-r$9|06Q5V>4=>ycnT}Fyz#9p;3?86`ZD23@7 z7n&`!LXzjxyg*P4Tz`>WVvpU9-<5MDSDcb1 zZaUyN@7mKLEPGS$^odZcW=GLe?3E$JsMR0kcL4#Z=b4P94Q#7O%_60{h>0D(6P*VH z3}>$stt2s!)w4C4 z{zsj!EyQm$2ARSHiRm49r7u)59ZyE}ZznFE7AdF&O&!-&(y=?-7$LWcn4L_Yj%w`qzwz`cLqPRem1zN; z)r)07;JFTnPODe09Z)SF5@^uRuGP~Mjil??oWmJTaCb;yx4?T?d**;AW!pOC^@GnT zaY`WF609J>fG+h?5&#}OD1<%&;_lzM2vw70FNwn2U`-jMH7bJxdQM#6+dPNiiRFGT z7zc{F6bo_V%NILyM?rBnNsH2>Bx~zj)pJ}*FJxW^DC2NLlOI~18Mk`7sl=t`)To6Ui zu4GK6KJx^6Ms4PP?jTn~jW6TOFLl3e2-q&ftT=31P1~a1%7=1XB z+H~<1dh6%L)PbBmtsAr38>m~)?k3}<->1Bs+;227M@?!S+%X&M49o_e)X8|vZiLVa z;zWb1gYokP;Sbao^qD+2ZD_kUn=m=d{Q9_kpGxcbdQ0d5<_OZJ!bZJcmgBRf z!Cdh`qQ_1NLhCulgn{V`C%|wLE8E6vq1Ogm`wb;7Dj+xpwik~?kEzDT$LS?#%!@_{ zhOoXOC95lVcQU^pK5x$Da$TscVXo19Pps zA!(Mk>N|tskqBn=a#aDC4K%jV#+qI$$dPOK6;fPO)0$0j$`OV+mWhE+TqJoF5dgA=TH-}5DH_)H_ zh?b(tUu@65G-O)1ah%|CsU8>cLEy0!Y~#ut#Q|UT92MZok0b4V1INUL-)Dvvq`RZ4 zTU)YVX^r%_lXpn_cwv`H=y49?!m{krF3Rh7O z^z7l4D<+^7E?ji(L5CptsPGttD+Z7{N6c-`0V^lfFjsdO{aJMFfLG9+wClt<=Rj&G zf6NgsPSKMrK6@Kvgarmx{&S48uc+ZLIvk0fbH}q-HQ4FSR33$+%FvNEusl6xin!?e z@rrWUP5U?MbBDeYSO~L;S$hjxISwLr&0BOSd?fOyeCWm6hD~)|_9#jo+PVbAY3wzf zcZS*2pX+8EHD~LdAl>sA*P>`g>>+&B{l94LNLp#KmC)t6`EPhL95s&MMph46Sk^9x%B$RK!2MI--j8nvN31MNLAJBsG`+WMvo1}xpaoq z%+W95_I`J1Pr&Xj`=)eN9!Yt?LWKs3-`7nf)`G6#6#f+=JK!v943*F&veRQxKy-dm(VcnmA?K_l~ zfDWPYl6hhN?17d~^6Zuo@>Hswhq@HrQ)sb7KK^TRhaM2f&td)$6zOn7we@ zd)x4-`?!qzTGDNS-E(^mjM%d46n>vPeMa;%7IJDT(nC)T+WM5F-M$|p(78W!^ck6)A_!6|1o!D97tw8k|5@0(!8W&q9*ovYl)afk z2mxnniCOSh7yHcSoEu8k`i15#oOi^O>uO_oMpT=KQx4Ou{&C4vqZG}YD0q!{RX=`#5wmcHT=hqW3;Yvg5Y^^ ziVunz9V)>2&b^rI{ssTPx26OxTuCw|+{tt_M0TqD?Bg7cWN4 z%UH{38(EW1L^!b~rtWl)#i}=8IUa_oU8**_UEIw+SYMekH;Epx*SA7Hf!EN&t!)zuUca@_Q^zW(u_iK_ zrSw{nva4E6-Npy9?lHAa;b(O z`I74A{jNEXj(#r|eS^Vfj-I!aHv{fEkzv4=F%z0m;3^PXa27k0Hq#RN@J7TwQT4u7 ztisbp3w6#k!RC~!5g-RyjpTth$lf!5HIY_5pfZ8k#q!=q*n>~@93dD|V>=GvH^`zn zVNwT@LfA8^4rpWz%FqcmzX2qEAhQ|_#u}md1$6G9qD%FXLw;fWWvqudd_m+PzI~g3 z`#WPz`M1XUKfT3&T4~XkUie-C#E`GN#P~S(Zx9%CY?EC?KP5KNK`aLlI1;pJvq@d z&0wI|dx##t6Gut6%Y9c-L|+kMov(7Oay++QemvI`JOle{8iE|2kZb=4x%a32?>-B~ z-%W$0t&=mr+WJ3o8d(|^209BapD`@6IMLbcBlWZlrr*Yrn^uRC1(}BGNr!ct z>xzEMV(&;ExHj5cce`pk%6!Xu=)QWtx2gfrAkJY@AZlHWiEe%^_}mdzvs(6>k7$e; ze4i;rv$_Z$K>1Yo9f4&Jbx80?@X!+S{&QwA3j#sAA4U4#v zwZqJ8%l~t7V+~BT%j4Bwga#Aq0&#rBl6p$QFqS{DalLd~MNR8Fru+cdoQ78Dl^K}@l#pmH1-e3?_0tZKdj@d2qu z_{-B11*iuywLJgGUUxI|aen-((KcAZZdu8685Zi1b(#@_pmyAwTr?}#O7zNB7U6P3 zD=_g*ZqJkg_9_X3lStTA-ENl1r>Q?p$X{6wU6~e7OKNIX_l9T# z>XS?PlNEM>P&ycY3sbivwJYAqbQH^)z@PobVRER*Ud*bUi-hjADId`5WqlZ&o+^x= z-Lf_80rC9>tqFBF%x#`o>69>D5f5Kp->>YPi5ArvgDwV#I6!UoP_F0YtfKoF2YduA zCU!1`EB5;r68;WyeL-;(1K2!9sP)at9C?$hhy(dfKKBf}>skPqvcRl>UTAB05SRW! z;`}sPVFFZ4I%YrPEtEsF(|F8gnfGkXI-2DLsj4_>%$_ZX8zVPrO=_$7412)Mr9BH{ zwKD;e13jP2XK&EpbhD-|`T~aI`N(*}*@yeDUr^;-J_`fl*NTSNbupyHLxMxjwmbuw zt3@H|(hvcRldE+OHGL1Y;jtBN76Ioxm@UF1K}DPbgzf_a{`ohXp_u4=ps@x-6-ZT>F z)dU`Jpu~Xn&Qkq2kg%VsM?mKC)ArP5c%r8m4aLqimgTK$atIxt^b8lDVPEGDOJu!) z%rvASo5|v`u_}vleP#wyu1$L5Ta%9YOyS5;w2I!UG&nG0t2YL|DWxr#T7P#Ww8MXDg;-gr`x1?|V`wy&0vm z=hqozzA!zqjOm~*DSI9jk8(9nc4^PL6VOS$?&^!o^Td8z0|eU$9x8s{8H!9zK|)NO zqvK*dKfzG^Dy^vkZU|p9c+uVV3>esY)8SU1v4o{dZ+dPP$OT@XCB&@GJ<5U&$Pw#iQ9qzuc`I_%uT@%-v zLf|?9w=mc;b0G%%{o==Z7AIn{nHk`>(!e(QG%(DN75xfc#H&S)DzSFB6`J(cH!@mX3mv_!BJv?ByIN%r-i{Y zBJU)}Vhu)6oGoQjT2tw&tt4n=9=S*nQV`D_MSw7V8u1-$TE>F-R6Vo0giKnEc4NYZ zAk2$+Tba~}N0wG{$_7eaoCeb*Ubc0 zq~id50^$U>WZjmcnIgsDione)f+T)0ID$xtgM zpGZXmVez0DN!)ioW1E45{!`G9^Y1P1oXhP^rc@c?o+c$^Kj_bn(Uo1H2$|g7=92v- z%Syv9Vo3VcibvH)b78USOTwIh{3%;3skO_htlfS?Cluwe`p&TMwo_WK6Z3Tz#nOoy z_E17(!pJ>`C2KECOo38F1uP0hqBr>%E=LCCCG{j6$b?;r?Fd$4@V-qjEzgWvzbQN%_nlBg?Ly`x-BzO2Nnd1 zuO|li(oo^Rubh?@$q8RVYn*aLnlWO_dhx8y(qzXN6~j>}-^Cuq4>=d|I>vhcjzhSO zU`lu_UZ?JaNs1nH$I1Ww+NJI32^qUikAUfz&k!gM&E_L=e_9}!<(?BfH~aCmI&hfzHi1~ zraRkci>zMPLkad=A&NEnVtQQ#YO8Xh&K*;6pMm$ap_38m;XQej5zEqUr`HdP&cf0i z5DX_c86@15jlm*F}u-+a*^v%u_hpzwN2eT66Zj_1w)UdPz*jI|fJb#kSD_8Q-7q9gf}zNu2h=q{)O*XH8FU)l|m;I;rV^QpXRvMJ|7% zWKTBX*cn`VY6k>mS#cq!uNw7H=GW3?wM$8@odjh$ynPiV7=Ownp}-|fhULZ)5{Z!Q z20oT!6BZTK;-zh=i~RQ$Jw>BTA=T(J)WdnTObDM#61lUm>IFRy@QJ3RBZr)A9CN!T z4k7%)I4yZ-0_n5d083t!=YcpSJ}M5E8`{uIs3L0lIaQws1l2}+w2(}hW&evDlMnC!WV?9U^YXF}!N*iyBGyCyJ<(2(Ca<>!$rID`( zR?V~-53&$6%DhW=)Hbd-oetTXJ-&XykowOx61}1f`V?LF=n8Nb-RLFGqheS7zNM_0 z1ozNap9J4GIM1CHj-%chrCdqPlP307wfrr^=XciOqn?YPL1|ozZ#LNj8QoCtAzY^q z7&b^^K&?fNSWD@*`&I+`l9 zP2SlD0IO?MK60nbucIQWgz85l#+*<{*SKk1K~|x{ux+hn=SvE_XE`oFlr7$oHt-&7 zP{+x)*y}Hnt?WKs_Ymf(J^aoe2(wsMMRPu>Pg8H#x|zQ_=(G5&ieVhvjEXHg1zY?U zW-hcH!DJPr+6Xnt)MslitmnHN(Kgs4)Y`PFcV0Qvemj;GG`kf<>?p})@kd9DA7dqs zNtGRKVr0%x#Yo*lXN+vT;TC{MR}}4JvUHJHDLd-g88unUj1(#7CM<%r!Z1Ve>DD)FneZ| z8Q0yI@i4asJaJ^ge%JPl>zC3+UZ;UDUr7JvUYNMf=M2t{It56OW1nw#K8%sXdX$Yg zpw3T=n}Om?j3-7lu)^XfBQkoaZ(qF0D=Aw&D%-bsox~`8Y|!whzpd5JZ{dmM^A5)M zOwWEM>bj}~885z9bo{kWFA0H(hv(vL$G2;pF$@_M%DSH#g%V*R(>;7Z7eKX&AQv1~ z+lKq=488TbTwA!VtgSHwduwAkGycunrg}>6oiX~;Kv@cZlz=E}POn%BWt{EEd;*GV zmc%PiT~k<(TA`J$#6HVg2HzF6Iw5w9{C63y`Y7?OB$WsC$~6WMm3`UHaWRZLN3nKiV# zE;iiu_)wTr7ZiELH$M^!i5eC9aRU#-RYZhCl1z_aNs@f`tD4A^$xd7I_ijCgI!$+| zsulIT$KB&PZ}T-G;Ibh@UPafvOc-=p7{H-~P)s{3M+;PmXe7}}&Mn+9WT#(Jmt5DW%73OBA$tC#Ug!j1BR~=Xbnaz4hGq zUOjC*z3mKNbrJm1Q!Ft^5{Nd54Q-O7<;n})TTQeLDY3C}RBGwhy*&wgnl8dB4lwkG zBX6Xn#hn|!v7fp@@tj9mUPrdD!9B;tJh8-$aE^t26n_<4^=u~s_MfbD?lHnSd^FGGL6the7a|AbltRGhfET*X;P7=AL?WPjBtt;3IXgUHLFMRBz(aWW_ zZ?%%SEPFu&+O?{JgTNB6^5nR@)rL6DFqK$KS$bvE#&hrPs>sYsW=?XzOyD6ixglJ8rdt{P8 zPAa*+qKt(%ju&jDkbB6x7aE(={xIb*&l=GF(yEnWPj)><_8U5m#gQIIa@l49W_=Qn^RCsYqlEy6Om%!&e~6mCAfDgeXe3aYpHQAA!N|kmIW~Rk}+p6B2U5@|1@7iVbm5&e7E3;c9q@XQlb^JS(gmJl%j9!N|eNQ$*OZf`3!;raRLJ z;X-h>nvB=S?mG!-VH{65kwX-UwNRMQB9S3ZRf`hL z#WR)+rn4C(AG(T*FU}`&UJOU4#wT&oDyZfHP^s9#>V@ens??pxuu-6RCk=Er`DF)X z>yH=P9RtrtY;2|Zg3Tnx3Vb!(lRLedVRmK##_#;Kjnlwq)eTbsY8|D{@Pjn_=kGYO zJq0T<_b;aB37{U`5g6OSG=>|pkj&PohM%*O#>kCPGK2{0*=m(-gKBEOh`fFa6*~Z! zVxw@7BS%e?cV^8{a`Ys4;w=tH4&0izFxgqjE#}UfsE^?w)cYEQjlU|uuv6{>nFTp| zNLjRRT1{g{?U2b6C^w{!s+LQ(n}FfQPDfYPsNV?KH_1HgscqG7z&n3Bh|xNYW4i5i zT4Uv-&mXciu3ej=+4X9h2uBW9o(SF*N~%4%=g|48R-~N32QNq!*{M4~Y!cS4+N=Zr z?32_`YpAeg5&r_hdhJkI4|i(-&BxCKru`zm9`v+CN8p3r9P_RHfr{U$H~RddyZKw{ zR?g5i>ad^Ge&h?LHlP7l%4uvOv_n&WGc$vhn}2d!xIWrPV|%x#2Q-cCbQqQ|-yoTe z_C(P))5e*WtmpB`Fa~#b*yl#vL4D_h;CidEbI9tsE%+{-4ZLKh#9^{mvY24#u}S6oiUr8b0xLYaga!(Fe7Dxi}v6 z%5xNDa~i%tN`Cy_6jbk@aMaY(xO2#vWZh9U?mrNrLs5-*n>04(-Dlp%6AXsy;f|a+ z^g~X2LhLA>xy(8aNL9U2wr=ec%;J2hEyOkL*D%t4cNg7WZF@m?kF5YGvCy`L5jus# zGP8@iGTY|ov#t&F$%gkWDoMR7v*UezIWMeg$C2~WE9*5%}$3!eFiFJ?hypfIA(PQT@=B|^Ipcu z{9cM3?rPF|gM~{G)j*af1hm+l92W7HRpQ*hSMDbh(auwr}VBG7`ldp>`FZ^amvau zTa~Y7%tH@>|BB6kSRGiWZFK?MIzxEHKGz#P!>rB-90Q_UsZ=uW6aTzxY{MPP@1rw- z&RP^Ld%HTo($y?6*aNMz8h&E?_PiO{jq%u4kr#*uN&Q+Yg1Rn831U4A6u#XOzaSL4 zrcM+0v@%On8N*Mj!)&IzXW6A80bUK&3w|z06cP!UD^?_rb_(L-u$m+#%YilEjkrlxthGCLQ@Q?J!p?ggv~0 z!qipxy&`w48T0(Elsz<^hp_^#1O1cNJ1UG=61Nc=)rlRo_P6v&&h??Qvv$ifC3oJh zo)ZZhU5enAqU%YB>+FU!1vW)i$m-Z%w!c&92M1?))n4z1a#4-FufZ$DatpJ^q)_Zif z;Br{HmZ|8LYRTi`#?TUfd;#>c4@2qM5_(H+Clt@kkQT+kx78KACyvY)?^zhyuN_Z& z-*9_o_f3IC2lX^(aLeqv#>qnelb6_jk+lgQh;TN>+6AU9*6O2h_*=74m;xSPD1^C9 zE0#!+B;utJ@8P6_DKTQ9kNOf`C*Jj0QAzsngKMQVDUsp=k~hd@wt}f{@$O*xI!a?p z6Gti>uE}IKAaQwKHRb0DjmhaF#+{9*=*^0)M-~6lPS-kCI#RFGJ-GyaQ+rhbmhQef zwco))WNA1LFr|J3Qsp4ra=_j?Y%b{JWMX6Zr`$;*V`l`g7P0sP?Y1yOY;e0Sb!AOW0Em=U8&i8EKxTd$dX6=^Iq5ZC%zMT5Jjj%0_ zbf|}I=pWjBKAx7wY<4-4o&E6vVStcNlT?I18f5TYP9!s|5yQ_C!MNnRyDt7~u~^VS@kKd}Zwc~? z=_;2}`Zl^xl3f?ce8$}g^V)`b8Pz88=9FwYuK_x%R?sbAF-dw`*@wokEC3mp0Id>P z>OpMGxtx!um8@gW2#5|)RHpRez+)}_p;`+|*m&3&qy{b@X>uphcgAVgWy`?Nc|NlH z75_k2%3h7Fy~EkO{vBMuzV7lj4B}*1Cj(Ew7oltspA6`d69P`q#Y+rHr5-m5&be&( zS1GcP5u#aM9V{fUQTfHSYU`kW&Wsxeg;S*{H_CdZ$?N>S$JPv!_6T(NqYPaS{yp0H7F~7vy#>UHJr^lV?=^vt4?8$v8vkI-1eJ4{iZ!7D5A zg_!ZxZV+9Wx5EIZ1%rbg8`-m|=>knmTE1cpaBVew_iZpC1>d>qd3`b6<(-)mtJBmd zjuq-qIxyKvIs!w4$qpl{0cp^-oq<=-IDEYV7{pvfBM7tU+ zfX3fc+VGtqjPIIx`^I0i>*L-NfY=gFS+|sC75Cg;2<)!Y`&p&-AxfOHVADHSv1?7t zlOKyXxi|7HdwG5s4T0))dWudvz8SZpxd<{z&rT<34l}XaaP86x)Q=2u5}1@Sgc41D z2gF)|aD7}UVy)bnm788oYp}Es!?|j73=tU<_+A4s5&it~_K4 z;^$i0Vnz8y&I!abOkzN|Vz;kUTya#Wi07>}Xf^7joZMiHH3Mdy@e_7t?l8^A!r#jTBau^wn#{|!tTg=w01EQUKJOca!I zV*>St2399#)bMF++1qS8T2iO3^oA`i^Px*i)T_=j=H^Kp4$Zao(>Y)kpZ=l#dSgcUqY=7QbGz9mP9lHnII8vl?yY9rU+i%X)-j0&-- zrtaJsbkQ$;DXyIqDqqq)LIJQ!`MIsI;goVbW}73clAjN;1Rtp7%{67uAfFNe_hyk= zn=8Q1x*zHR?txU)x9$nQu~nq7{Gbh7?tbgJ>i8%QX3Y8%T{^58W^{}(!9oPOM+zF3 zW`%<~q@W}9hoes56uZnNdLkgtcRqPQ%W8>o7mS(j5Sq_nN=b0A`Hr%13P{uvH?25L zMfC&Z0!{JBGiKoVwcIhbbx{I35o}twdI_ckbs%1%AQ(Tdb~Xw+sXAYcOoH_9WS(yM z2dIzNLy4D%le8Fxa31fd;5SuW?ERAsagZVEo^i};yjBhbxy9&*XChFtOPV8G77{8! zlYemh2vp7aBDMGT;YO#=YltE~(Qv~e7c=6$VKOxHwvrehtq>n|w}vY*YvXB%a58}n zqEBR4zueP@A~uQ2x~W-{o3|-xS@o>Ad@W99)ya--dRx;TZLL?5E(xstg(6SwDIpL5 zMZ)+)+&(hYL(--dxIKB*#v4mDq=0ve zNU~~jk426bXlS8%lcqsvuqbpgn zbFgxap;17;@xVh+Y~9@+-lX@LQv^Mw=yCM&2!%VCfZsiwN>DI=O?vHupbv9!4d*>K zcj@a5vqjcjpwkm@!2dxzzJGQ7#ujW(IndUuYC)i3N2<*doRGX8a$bSbyRO#0rA zUpFyEGx4S9$TKuP9BybRtjcAn$bGH-9>e(V{pKYPM3waYrihBCQf+UmIC#E=9v?or z_7*yzZfT|)8R6>s(lv6uzosT%WoR`bQIv(?llcH2Bd@26?zU%r1K25qscRrE1 z9TIIP_?`78@uJ{%I|_K;*syVinV;pCW!+zY-!^#n{3It^6EKw{~WIA0pf_hVzEZy zFzE=d-NC#mge{4Fn}we02-%Zh$JHKpXX3qF<#8__*I}+)Npxm?26dgldWyCmtwr9c zOXI|P0zCzn8M_Auv*h9;2lG}x*E|u2!*-s}moqS%Z`?O$<0amJG9n`dOV4**mypG- zE}In1pOQ|;@@Jm;I#m}jkQegIXag4K%J;C7<@R2X8IdsCNqrbsaUZZRT|#6=N!~H} zlc2hPngy9r+Gm_%tr9V&HetvI#QwUBKV&6NC~PK>HNQ3@fHz;J&rR7XB>sWkXKp%A ziLlogA`I*$Z7KzLaX^H_j)6R|9Q>IHc? z{s0MsOW>%xW|JW=RUxY@@0!toq`QXa=`j;)o2iDBiDZ7c4Bc>BiDTw+zk}Jm&vvH8qX$R`M6Owo>m%n`eizBf!&9X6 z)f{GpMak@NWF+HNg*t#H5yift5@QhoYgT7)jxvl&O=U54Z>FxT5prvlDER}AwrK4Q z*&JP9^k332OxC$(E6^H`#zw|K#cpwy0i*+!z{T23;dqUKbjP!-r*@_!sp+Uec@^f0 zIJMjqhp?A#YoX5EB%iWu;mxJ1&W6Nb4QQ@GElqNjFNRc*=@aGc$PHdoUptckkoOZC zk@c9i+WVnDI=GZ1?lKjobDl%nY2vW~d)eS6Lch&J zDi~}*fzj9#<%xg<5z-4(c}V4*pj~1z2z60gZc}sAmys^yvobWz)DKDGWuVpp^4-(!2Nn7 z3pO})bO)({KboXlQA>3PIlg@Ie$a=G;MzVeft@OMcKEjIr=?;=G0AH?dE_DcNo%n$_bFjqQ8GjeIyJP^NkX~7e&@+PqnU-c3@ABap z=}IZvC0N{@fMDOpatOp*LZ7J6Hz@XnJzD!Yh|S8p2O($2>A4hbpW{8?#WM`uJG>?} zwkDF3dimqejl$3uYoE7&pr5^f4QP-5TvJ;5^M?ZeJM8ywZ#Dm`kR)tpYieQU;t2S! z05~aeOBqKMb+`vZ2zfR*2(&z`Y1VROAcR(^Q7ZyYlFCLHSrTOQm;pnhf3Y@WW#gC1 z7b$_W*ia0@2grK??$pMHK>a$;J)xIx&fALD4)w=xlT=EzrwD!)1g$2q zy8GQ+r8N@?^_tuCKVi*q_G*!#NxxY#hpaV~hF} zF1xXy#XS|q#)`SMAA|46+UnJZ__lETDwy}uecTSfz69@YO)u&QORO~F^>^^j-6q?V z-WK*o?XSw~ukjoIT9p6$6*OStr`=+;HrF#)p>*>e|gy0D9G z#TN(VSC11^F}H#?^|^ona|%;xCC!~H3~+a>vjyRC5MPGxFqkj6 zttv9I_fv+5$vWl2r8+pXP&^yudvLxP44;9XzUr&a$&`?VNhU^$J z`3m68BAuA?ia*IF%Hs)@>xre4W0YoB^(X8RwlZ?pKR)rvGX?u&K`kb8XBs^pe}2v* z_NS*z7;4%Be$ts_emapc#zKjVMEqn8;aCX=dISG3zvJP>l4zHdpUwARLixQSFzLZ0 z$$Q+9fAnVjA?7PqANPiH*XH~VhrVfW11#NkAKjfjQN-UNz?ZT}SG#*sk*)VUXZ1$P zdxiM@I2RI7Tr043ZgWd3G^k56$Non@LKE|zLwBgXW#e~{7C{iB3&UjhKZPEj#)cH9 z%HUDubc0u@}dBz>4zU;sTluxBtCl!O4>g9ywc zhEiM-!|!C&LMjMNs6dr6Q!h{nvTrNN0hJ+w*h+EfxW=ro zxAB%*!~&)uaqXyuh~O`J(6e!YsD0o0l_ung1rCAZt~%4R{#izD2jT~${>f}m{O!i4 z`#UGbiSh{L=FR`Q`e~9wrKHSj?I>eXHduB`;%TcCTYNG<)l@A%*Ld?PK=fJi}J? z9T-|Ib8*rLE)v_3|1+Hqa!0ch>f% zfNFz@o6r5S`QQJCwRa4zgx$7AyQ7ZTv2EM7ZQHh!72CFL+qT`Y)k!)|Zr;7mcfV8T z)PB$1r*5rUzgE@y^E_kDG3Ol5n6q}eU2hJcXY7PI1}N=>nwC6k%nqxBIAx4Eix*`W zch0}3aPFe5*lg1P(=7J^0ZXvpOi9v2l*b?j>dI%iamGp$SmFaxpZod*TgYiyhF0= za44lXRu%9MA~QWN;YX@8LM32BqKs&W4&a3ve9C~ndQq>S{zjRNj9&&8k-?>si8)^m zW%~)EU)*$2YJzTXjRV=-dPAu;;n2EDYb=6XFyz`D0f2#29(mUX}*5~KU3k>$LwN#OvBx@ zl6lC>UnN#0?mK9*+*DMiboas!mmGnoG%gSYeThXI<=rE(!Pf-}oW}?yDY0804dH3o zo;RMFJzxP|srP-6ZmZ_peiVycfvH<`WJa9R`Z#suW3KrI*>cECF(_CB({ToWXSS18#3%vihZZJ{BwJPa?m^(6xyd1(oidUkrOU zlqyRQUbb@W_C)5Q)%5bT3K0l)w(2cJ-%?R>wK35XNl&}JR&Pn*laf1M#|s4yVXQS# zJvkT$HR;^3k{6C{E+{`)J+~=mPA%lv1T|r#kN8kZP}os;n39exCXz^cc{AN(Ksc%} zA561&OeQU8gIQ5U&Y;Ca1TatzG`K6*`9LV<|GL-^=qg+nOx~6 zBEMIM7Q^rkuhMtw(CZtpU(%JlBeV?KC+kjVDL34GG1sac&6(XN>nd+@Loqjo%i6I~ zjNKFm^n}K=`z8EugP20fd_%~$Nfu(J(sLL1gvXhxZt|uvibd6rLXvM%!s2{g0oNA8 z#Q~RfoW8T?HE{ge3W>L9bx1s2_L83Odx)u1XUo<`?a~V-_ZlCeB=N-RWHfs1(Yj!_ zP@oxCRysp9H8Yy@6qIc69TQx(1P`{iCh)8_kH)_vw1=*5JXLD(njxE?2vkOJ z>qQz!*r`>X!I69i#1ogdVVB=TB40sVHX;gak=fu27xf*}n^d>@*f~qbtVMEW!_|+2 zXS`-E%v`_>(m2sQnc6+OA3R z-6K{6$KZsM+lF&sn~w4u_md6J#+FzqmtncY;_ z-Q^D=%LVM{A0@VCf zV9;?kF?vV}*=N@FgqC>n-QhKJD+IT7J!6llTEH2nmUxKiBa*DO4&PD5=HwuD$aa(1 z+uGf}UT40OZAH@$jjWoI7FjOQAGX6roHvf_wiFKBfe4w|YV{V;le}#aT3_Bh^$`Pp zJZGM_()iFy#@8I^t{ryOKQLt%kF7xq&ZeD$$ghlTh@bLMv~||?Z$#B2_A4M&8)PT{ zyq$BzJpRrj+=?F}zH+8XcPvhRP+a(nnX2^#LbZqgWQ7uydmIM&FlXNx4o6m;Q5}rB z^ryM&o|~a-Zb20>UCfSFwdK4zfk$*~<|90v0=^!I?JnHBE{N}74iN;w6XS=#79G+P zB|iewe$kk;9^4LinO>)~KIT%%4Io6iFFXV9gJcIvu-(!um{WfKAwZDmTrv=wb#|71 zWqRjN8{3cRq4Ha2r5{tw^S>0DhaC3m!i}tk9q08o>6PtUx1GsUd{Z17FH45rIoS+oym1>3S0B`>;uo``+ADrd_Um+8s$8V6tKsA8KhAm z{pTv@zj~@+{~g&ewEBD3um9@q!23V_8Nb0_R#1jcg0|MyU)?7ua~tEY63XSvqwD`D zJ+qY0Wia^BxCtXpB)X6htj~*7)%un+HYgSsSJPAFED7*WdtlFhuJj5d3!h8gt6$(s ztrx=0hFH8z(Fi9}=kvPI?07j&KTkssT=Vk!d{-M50r!TsMD8fPqhN&%(m5LGpO>}L zse;sGl_>63FJ)(8&8(7Wo2&|~G!Lr^cc!uuUBxGZE)ac7Jtww7euxPo)MvxLXQXlk zeE>E*nMqAPwW0&r3*!o`S7wK&078Q#1bh!hNbAw0MFnK-2gU25&8R@@j5}^5-kHeR z!%krca(JG%&qL2mjFv380Gvb*eTLllTaIpVr3$gLH2e3^xo z=qXjG0VmES%OXAIsOQG|>{aj3fv+ZWdoo+a9tu8)4AyntBP>+}5VEmv@WtpTo<-aH zF4C(M#dL)MyZmU3sl*=TpAqU#r>c8f?-zWMq`wjEcp^jG2H`8m$p-%TW?n#E5#Th+ z7Zy#D>PPOA4|G@-I$!#Yees_9Ku{i_Y%GQyM)_*u^nl+bXMH!f_ z8>BM|OTex;vYWu`AhgfXFn)0~--Z7E0WR-v|n$XB-NOvjM156WR(eu z(qKJvJ%0n+%+%YQP=2Iz-hkgI_R>7+=)#FWjM#M~Y1xM8m_t8%=FxV~Np$BJ{^rg9 z5(BOvYfIY{$h1+IJyz-h`@jhU1g^Mo4K`vQvR<3wrynWD>p{*S!kre-(MT&`7-WK! zS}2ceK+{KF1yY*x7FH&E-1^8b$zrD~Ny9|9(!1Y)a#)*zf^Uo@gy~#%+*u`U!R`^v zCJ#N!^*u_gFq7;-XIYKXvac$_=booOzPgrMBkonnn%@#{srUC<((e*&7@YR?`CP;o zD2*OE0c%EsrI72QiN`3FpJ#^Bgf2~qOa#PHVmbzonW=dcrs92>6#{pEnw19AWk%;H zJ4uqiD-dx*w2pHf8&Jy{NXvGF^Gg!ungr2StHpMQK5^+ zEmDjjBonrrT?d9X;BHSJeU@lX19|?On)(Lz2y-_;_!|}QQMsq4Ww9SmzGkzVPQTr* z)YN>_8i^rTM>Bz@%!!v)UsF&Nb{Abz>`1msFHcf{)Ufc_a-mYUPo@ei#*%I_jWm#7 zX01=Jo<@6tl`c;P_uri^gJxDVHOpCano2Xc5jJE8(;r@y6THDE>x*#-hSKuMQ_@nc z68-JLZyag_BTRE(B)Pw{B;L0+Zx!5jf%z-Zqug*og@^ zs{y3{Za(0ywO6zYvES>SW*cd4gwCN^o9KQYF)Lm^hzr$w&spGNah6g>EQBufQCN!y zI5WH$K#67$+ic{yKAsX@el=SbBcjRId*cs~xk~3BBpQsf%IsoPG)LGs zdK0_rwz7?L0XGC^2$dktLQ9qjwMsc1rpGx2Yt?zmYvUGnURx(1k!kmfPUC@2Pv;r9 z`-Heo+_sn+!QUJTAt;uS_z5SL-GWQc#pe0uA+^MCWH=d~s*h$XtlN)uCI4$KDm4L$ zIBA|m0o6@?%4HtAHRcDwmzd^(5|KwZ89#UKor)8zNI^EsrIk z1QLDBnNU1!PpE3iQg9^HI){x7QXQV{&D>2U%b_II>*2*HF2%>KZ>bxM)Jx4}|CCEa`186nD_B9h`mv6l45vRp*L+z_nx5i#9KvHi>rqxJIjKOeG(5lCeo zLC|-b(JL3YP1Ds=t;U!Y&Gln*Uwc0TnDSZCnh3m$N=xWMcs~&Rb?w}l51ubtz=QUZsWQhWOX;*AYb)o(^<$zU_v=cFwN~ZVrlSLx| zpr)Q7!_v*%U}!@PAnZLqOZ&EbviFbej-GwbeyaTq)HSBB+tLH=-nv1{MJ-rGW%uQ1 znDgP2bU@}!Gd=-;3`KlJYqB@U#Iq8Ynl%eE!9g;d*2|PbC{A}>mgAc8LK<69qcm)piu?`y~3K8zlZ1>~K_4T{%4zJG6H?6%{q3B-}iP_SGXELeSv*bvBq~^&C=3TsP z9{cff4KD2ZYzkArq=;H(Xd)1CAd%byUXZdBHcI*%a24Zj{Hm@XA}wj$=7~$Q*>&4} z2-V62ek{rKhPvvB711`qtAy+q{f1yWuFDcYt}hP)Vd>G?;VTb^P4 z(QDa?zvetCoB_)iGdmQ4VbG@QQ5Zt9a&t(D5Rf#|hC`LrONeUkbV)QF`ySE5x+t_v z-(cW{S13ye9>gtJm6w&>WwJynxJQm8U2My?#>+(|)JK}bEufIYSI5Y}T;vs?rzmLE zAIk%;^qbd@9WUMi*cGCr=oe1-nthYRQlhVHqf{ylD^0S09pI}qOQO=3&dBsD)BWo# z$NE2Ix&L&4|Aj{;ed*A?4z4S!7o_Kg^8@%#ZW26_F<>y4ghZ0b|3+unIoWDUVfen~ z`4`-cD7qxQSm9hF-;6WvCbu$t5r$LCOh}=`k1(W<&bG-xK{VXFl-cD%^Q*x-9eq;k8FzxAqZB zH@ja_3%O7XF~>owf3LSC_Yn!iO}|1Uc5uN{Wr-2lS=7&JlsYSp3IA%=E?H6JNf()z zh>jA>JVsH}VC>3Be>^UXk&3o&rK?eYHgLwE-qCHNJyzDLmg4G(uOFX5g1f(C{>W3u zn~j`zexZ=sawG8W+|SErqc?uEvQP(YT(YF;u%%6r00FP;yQeH)M9l+1Sv^yddvGo- z%>u>5SYyJ|#8_j&%h3#auTJ!4y@yEg<(wp#(~NH zXP7B#sv@cW{D4Iz1&H@5wW(F82?-JmcBt@Gw1}WK+>FRXnX(8vwSeUw{3i%HX6-pvQS-~Omm#x-udgp{=9#!>kDiLwqs_7fYy{H z)jx_^CY?5l9#fR$wukoI>4aETnU>n<$UY!JDlIvEti908)Cl2Ziyjjtv|P&&_8di> z<^amHu|WgwMBKHNZ)t)AHII#SqDIGTAd<(I0Q_LNPk*?UmK>C5=rIN^gs}@65VR*!J{W;wp5|&aF8605*l-Sj zQk+C#V<#;=Sl-)hzre6n0n{}|F=(#JF)X4I4MPhtm~qKeR8qM?a@h!-kKDyUaDrqO z1xstrCRCmDvdIFOQ7I4qesby8`-5Y>t_E1tUTVOPuNA1De9| z8{B0NBp*X2-ons_BNzb*Jk{cAJ(^F}skK~i;p0V(R7PKEV3bB;syZ4(hOw47M*-r8 z3qtuleeteUl$FHL$)LN|q8&e;QUN4(id`Br{rtsjpBdriO}WHLcr<;aqGyJP{&d6? zMKuMeLbc=2X0Q_qvSbl3r?F8A^oWw9Z{5@uQ`ySGm@DUZ=XJ^mKZ-ipJtmiXjcu<%z?Nj%-1QY*O{NfHd z=V}Y(UnK=f?xLb-_~H1b2T&0%O*2Z3bBDf06-nO*q%6uEaLs;=omaux7nqqW%tP$i zoF-PC%pxc(ymH{^MR_aV{@fN@0D1g&zv`1$Pyu3cvdR~(r*3Y%DJ@&EU?EserVEJ` zEprux{EfT+(Uq1m4F?S!TrZ+!AssSdX)fyhyPW6C`}ko~@y#7acRviE(4>moNe$HXzf zY@@fJa~o_r5nTeZ7ceiXI=k=ISkdp1gd1p)J;SlRn^5;rog!MlTr<<6-U9|oboRBN zlG~o*dR;%?9+2=g==&ZK;Cy0pyQFe)x!I!8g6;hGl`{{3q1_UzZy)J@c{lBIEJVZ& z!;q{8h*zI!kzY#RO8z3TNlN$}l;qj10=}du!tIKJs8O+?KMJDoZ+y)Iu`x`yJ@krO zwxETN$i!bz8{!>BKqHpPha{96eriM?mST)_9Aw-1X^7&;Bf=c^?17k)5&s08^E$m^ zRt02U_r!99xfiow-XC~Eo|Yt8t>32z=rv$Z;Ps|^26H73JS1Xle?;-nisDq$K5G3y znR|l8@rlvv^wj%tdgw+}@F#Ju{SkrQdqZ?5zh;}|IPIdhy3ivi0Q41C@4934naAaY z%+otS8%Muvrr{S-Y96G?b2j0ldu1&coOqsq^vfcUT3}#+=#;fii6@M+hDp}dr9A0Y zjbhvqmB03%4jhsZ{_KQfGh5HKm-=dFxN;3tnwBej^uzcVLrrs z>eFP-jb#~LE$qTP9JJ;#$nVOw%&;}y>ezA6&i8S^7YK#w&t4!A36Ub|or)MJT z^GGrzgcnQf6D+!rtfuX|Pna`Kq*ScO#H=de2B7%;t+Ij<>N5@(Psw%>nT4cW338WJ z>TNgQ^!285hS1JoHJcBk;3I8%#(jBmcpEkHkQDk%!4ygr;Q2a%0T==W zT#dDH>hxQx2E8+jE~jFY$FligkN&{vUZeIn*#I_Ca!l&;yf){eghi z>&?fXc-C$z8ab$IYS`7g!2#!3F@!)cUquAGR2oiR0~1pO<$3Y$B_@S2dFwu~B0e4D z6(WiE@O{(!vP<(t{p|S5#r$jl6h;3@+ygrPg|bBDjKgil!@Sq)5;rXNjv#2)N5_nn zuqEURL>(itBYrT&3mu-|q;soBd52?jMT75cvXYR!uFuVP`QMot+Yq?CO%D9$Jv24r zhq1Q5`FD$r9%&}9VlYcqNiw2#=3dZsho0cKKkv$%X&gmVuv&S__zyz@0zmZdZI59~s)1xFs~kZS0C^271hR*O z9nt$5=y0gjEI#S-iV0paHx!|MUNUq&$*zi>DGt<#?;y;Gms|dS{2#wF-S`G3$^$7g z1#@7C65g$=4Ij?|Oz?X4=zF=QfixmicIw{0oDL5N7iY}Q-vcVXdyQNMb>o_?3A?e6 z$4`S_=6ZUf&KbMgpn6Zt>6n~)zxI1>{HSge3uKBiN$01WB9OXscO?jd!)`?y5#%yp zJvgJU0h+|^MdA{!g@E=dJuyHPOh}i&alC+cY*I3rjB<~DgE{`p(FdHuXW;p$a+%5` zo{}x#Ex3{Sp-PPi)N8jGVo{K!$^;z%tVWm?b^oG8M?Djk)L)c{_-`@F|8LNu|BTUp zQY6QJVzVg8S{8{Pe&o}Ux=ITQ6d42;0l}OSEA&Oci$p?-BL187L6rJ>Q)aX0)Wf%T zneJF2;<-V%-VlcA?X03zpf;wI&8z9@Hy0BZm&ac-Gdtgo>}VkZYk##OOD+nVOKLFJ z5hgXAhkIzZtCU%2M#xl=D7EQPwh?^gZ_@0p$HLd*tF>qgA_P*dP;l^cWm&iQSPJZE zBoipodanrwD0}}{H#5o&PpQpCh61auqlckZq2_Eg__8;G-CwyH#h1r0iyD#Hd_$WgM89n+ldz;=b!@pvr4;x zs|YH}rQuCyZO!FWMy%lUyDE*0)(HR}QEYxIXFexCkq7SHmSUQ)2tZM2s`G<9dq;Vc ziNVj5hiDyqET?chgEA*YBzfzYh_RX#0MeD@xco%)ON%6B7E3#3iFBkPK^P_=&8$pf zpM<0>QmE~1FX1>mztm>JkRoosOq8cdJ1gF5?%*zMDak%qubN}SM!dW6fgH<*F>4M7 zX}%^g{>ng^2_xRNGi^a(epr8SPSP>@rg7s=0PO-#5*s}VOH~4GpK9<4;g=+zuJY!& ze_ld=ybcca?dUI-qyq2Mwl~-N%iCGL;LrE<#N}DRbGow7@5wMf&d`kT-m-@geUI&U z0NckZmgse~(#gx;tsChgNd|i1Cz$quL>qLzEO}ndg&Pg4f zy`?VSk9X5&Ab_TyKe=oiIiuNTWCsk6s9Ie2UYyg1y|i}B7h0k2X#YY0CZ;B7!dDg7 z_a#pK*I7#9-$#Iev5BpN@xMq@mx@TH@SoNWc5dv%^8!V}nADI&0K#xu_#y)k%P2m~ zqNqQ{(fj6X8JqMe5%;>MIkUDd#n@J9Dm~7_wC^z-Tcqqnsfz54jPJ1*+^;SjJzJhG zIq!F`Io}+fRD>h#wjL;g+w?Wg`%BZ{f()%Zj)sG8permeL0eQ9vzqcRLyZ?IplqMg zpQaxM11^`|6%3hUE9AiM5V)zWpPJ7nt*^FDga?ZP!U1v1aeYrV2Br|l`J^tgLm;~%gX^2l-L9L`B?UDHE9_+jaMxy|dzBY4 zjsR2rcZ6HbuyyXsDV(K0#%uPd#<^V%@9c7{6Qd_kQEZL&;z_Jf+eabr)NF%@Ulz_a1e(qWqJC$tTC! zwF&P-+~VN1Vt9OPf`H2N{6L@UF@=g+xCC_^^DZ`8jURfhR_yFD7#VFmklCR*&qk;A zzyw8IH~jFm+zGWHM5|EyBI>n3?2vq3W?aKt8bC+K1`YjklQx4*>$GezfU%E|>Or9Y zNRJ@s(>L{WBXdNiJiL|^In*1VA`xiE#D)%V+C;KuoQi{1t3~4*8 z;tbUGJ2@2@$XB?1!U;)MxQ}r67D&C49k{ceku^9NyFuSgc}DC2pD|+S=qLH&L}Vd4 zM=-UK4{?L?xzB@v;qCy}Ib65*jCWUh(FVc&rg|+KnopG`%cb>t;RNv=1%4= z#)@CB7i~$$JDM>q@4ll8{Ja5Rsq0 z$^|nRac)f7oZH^=-VdQldC~E_=5%JRZSm!z8TJocv`w<_e0>^teZ1en^x!yQse%Lf z;JA5?0vUIso|MS03y${dX19A&bU4wXS~*T7h+*4cgSIX11EB?XGiBS39hvWWuyP{!5AY^x5j{!c?z<}7f-kz27%b>llPq%Z7hq+CU|Ev2 z*jh(wt-^7oL`DQ~Zw+GMH}V*ndCc~ zr>WVQHJQ8ZqF^A7sH{N5~PbeDihT$;tUP`OwWn=j6@L+!=T|+ze%YQ zO+|c}I)o_F!T(^YLygYOTxz&PYDh9DDiv_|Ewm~i7|&Ck^$jsv_0n_}q-U5|_1>*L44)nt!W|;4q?n&k#;c4wpSx5atrznZbPc;uQI^I}4h5Fy`9J)l z7yYa7Rg~f@0oMHO;seQl|E@~fd|532lLG#e6n#vXrfdh~?NP){lZ z&3-33d;bUTEAG=!4_{YHd3%GCV=WS|2b)vZgX{JC)?rsljjzWw@Hflbwg3kIs^l%y zm3fVP-55Btz;<-p`X(ohmi@3qgdHmwXfu=gExL!S^ve^MsimP zNCBV>2>=BjLTobY^67f;8mXQ1YbM_NA3R^s z{zhY+5@9iYKMS-)S>zSCQuFl!Sd-f@v%;;*fW5hme#xAvh0QPtJ##}b>&tth$)6!$ z0S&b2OV-SE<|4Vh^8rs*jN;v9aC}S2EiPKo(G&<6C|%$JQ{;JEg-L|Yob*<-`z?AsI(~U(P>cC=1V$OETG$7i# zG#^QwW|HZuf3|X|&86lOm+M+BE>UJJSSAAijknNp*eyLUq=Au z7&aqR(x8h|>`&^n%p#TPcC@8@PG% zM&7k6IT*o-NK61P1XGeq0?{8kA`x;#O+|7`GTcbmyWgf^JvWU8Y?^7hpe^85_VuRq7yS~8uZ=Cf%W^OfwF_cbBhr`TMw^MH0<{3y zU=y;22&oVlrH55eGNvoklhfPM`bPX`|C_q#*etS^O@5PeLk(-DrK`l|P*@#T4(kRZ z`AY7^%&{!mqa5}q%<=x1e29}KZ63=O>89Q)yO4G@0USgbGhR#r~OvWI4+yu4*F8o`f?EG~x zBCEND=ImLu2b(FDF3sOk_|LPL!wrzx_G-?&^EUof1C~A{feam{2&eAf@2GWem7! z|LV-lff1Dk+mvTw@=*8~0@_Xu@?5u?-u*r8E7>_l1JRMpi{9sZqYG+#Ty4%Mo$`ds zsVROZH*QoCErDeU7&=&-ma>IUM|i_Egxp4M^|%^I7ecXzq@K8_oz!}cHK#>&+$E4rs2H8Fyc)@Bva?(KO%+oc!+3G0&Rv1cP)e9u_Y|dXr#!J;n%T4+9rTF>^m_4X3 z(g+$G6Zb@RW*J-IO;HtWHvopoVCr7zm4*h{rX!>cglE`j&;l_m(FTa?hUpgv%LNV9 zkSnUu1TXF3=tX)^}kDZk|AF%7FmLv6sh?XCORzhTU%d>y4cC;4W5mn=i6vLf2 ztbTQ8RM@1gn|y$*jZa8&u?yTOlNo{coXPgc%s;_Y!VJw2Z1bf%57p%kC1*5e{bepl zwm?2YGk~x=#69_Ul8A~(BB}>UP27=M)#aKrxWc-)rLL+97=>x|?}j)_5ewvoAY?P| z{ekQQbmjbGC%E$X*x-M=;Fx}oLHbzyu=Dw>&WtypMHnOc92LSDJ~PL7sU!}sZw`MY z&3jd_wS8>a!si2Y=ijCo(rMnAqq z-o2uzz}Fd5wD%MAMD*Y&=Ct?|B6!f0jfiJt;hvkIyO8me(u=fv_;C;O4X^vbO}R_% zo&Hx7C@EcZ!r%oy}|S-8CvPR?Ns0$j`FtMB;h z`#0Qq)+6Fxx;RCVnhwp`%>0H4hk(>Kd!(Y}>U+Tr_6Yp?W%jt_zdusOcA$pTA z(4l9$K=VXT2ITDs!OcShuUlG=R6#x@t74B2x7Dle%LGwsZrtiqtTuZGFUio_Xwpl} z=T7jdfT~ld#U${?)B67E*mP*E)XebDuMO(=3~Y=}Z}rm;*4f~7ka196QIHj;JK%DU z?AQw4I4ZufG}gmfVQ3w{snkpkgU~Xi;}V~S5j~;No^-9eZEYvA`Et=Q4(5@qcK=Pr zk9mo>v!%S>YD^GQc7t4c!C4*qU76b}r(hJhO*m-s9OcsktiXY#O1<OoH z#J^Y@1A;nRrrxNFh?3t@Hx9d>EZK*kMb-oe`2J!gZ;~I*QJ*f1p93>$lU|4qz!_zH z&mOaj#(^uiFf{*Nq?_4&9ZssrZeCgj1J$1VKn`j+bH%9#C5Q5Z@9LYX1mlm^+jkHf z+CgcdXlX5);Ztq6OT@;UK_zG(M5sv%I`d2(i1)>O`VD|d1_l(_aH(h>c7fP_$LA@d z6Wgm))NkU!v^YaRK_IjQy-_+>f_y(LeS@z+B$5be|FzXqqg}`{eYpO;sXLrU{*fJT zQHUEXoWk%wh%Kal`E~jiu@(Q@&d&dW*!~9;T=gA{{~NJwQvULf;s43Ku#A$NgaR^1 z%U3BNX`J^YE-#2dM*Ov*CzGdP9^`iI&`tmD~Bwqy4*N=DHt%RycykhF* zc7BcXG28Jvv(5G8@-?OATk6|l{Rg1 zwdU2Md1Qv?#$EO3E}zk&9>x1sQiD*sO0dGSUPkCN-gjuppdE*%*d*9tEWyQ%hRp*7 zT`N^=$PSaWD>f;h@$d2Ca7 z8bNsm14sdOS%FQhMn9yC83$ z-YATg3X!>lWbLUU7iNk-`O%W8MrgI03%}@6l$9+}1KJ1cTCiT3>^e}-cTP&aEJcUt zCTh_xG@Oa-v#t_UDKKfd#w0tJfA+Ash!0>X&`&;2%qv$!Gogr4*rfMcKfFl%@{ztA zwoAarl`DEU&W_DUcIq-{xaeRu(ktyQ64-uw?1S*A>7pRHH5_F)_yC+2o@+&APivkn zwxDBp%e=?P?3&tiVQb8pODI}tSU8cke~T#JLAxhyrZ(yx)>fUhig`c`%;#7Ot9le# zSaep4L&sRBd-n&>6=$R4#mU8>T>=pB)feU9;*@j2kyFHIvG`>hWYJ_yqv?Kk2XTw` z42;hd=hm4Iu0h{^M>-&c9zKPtqD>+c$~>k&Wvq#>%FjOyifO%RoFgh*XW$%Hz$y2-W!@W6+rFJja=pw-u_s0O3WMVgLb&CrCQ)8I^6g!iQj%a%#h z<~<0S#^NV4n!@tiKb!OZbkiSPp~31?f9Aj#fosfd*v}j6&7YpRGgQ5hI_eA2m+Je) zT2QkD;A@crBzA>7T zw4o1MZ_d$)puHvFA2J|`IwSXKZyI_iK_}FvkLDaFj^&6}e|5@mrHr^prr{fPVuN1+ z4=9}DkfKLYqUq7Q7@qa$)o6&2)kJx-3|go}k9HCI6ahL?NPA&khLUL}k_;mU&7GcN zNG6(xXW}(+a%IT80=-13-Q~sBo>$F2m`)7~wjW&XKndrz8soC*br=F*A_>Sh_Y}2Mt!#A1~2l?|hj) z9wpN&jISjW)?nl{@t`yuLviwvj)vyZQ4KR#mU-LE)mQ$yThO1oohRv;93oEXE8mYE zXPQSVCK~Lp3hIA_46A{8DdA+rguh@98p?VG2+Nw(4mu=W(sK<#S`IoS9nwuOM}C0) zH9U|6N=BXf!jJ#o;z#6vi=Y3NU5XT>ZNGe^z4u$i&x4ty^Sl;t_#`|^hmur~;r;o- z*CqJb?KWBoT`4`St5}10d*RL?!hm`GaFyxLMJPgbBvjVD??f7GU9*o?4!>NabqqR! z{BGK7%_}96G95B299eErE5_rkGmSWKP~590$HXvsRGJN5-%6d@=~Rs_68BLA1RkZb zD%ccBqGF0oGuZ?jbulkt!M}{S1;9gwAVkgdilT^_AS`w6?UH5Jd=wTUA-d$_O0DuM z|9E9XZFl$tZctd`Bq=OfI(cw4A)|t zl$W~3_RkP zFA6wSu+^efs79KH@)0~c3Dn1nSkNj_s)qBUGs6q?G0vjT&C5Y3ax-seA_+_}m`aj} zvW04)0TSIpqQkD@#NXZBg9z@GK1^ru*aKLrc4{J0PjhNfJT}J;vEeJ1ov?*KVNBy< zXtNIY3TqLZ=o1Byc^wL!1L6#i6n(088T9W<_iu~$S&VWGfmD|wNj?Q?Dnc#6iskoG zt^u26JqFnt=xjS-=|ACC%(=YQh{_alLW1tk;+tz1ujzeQ--lEu)W^Jk>UmHK(H303f}P2i zrsrQ*nEz`&{V!%2O446^8qLR~-Pl;2Y==NYj^B*j1vD}R5plk>%)GZSSjbi|tx>YM zVd@IS7b>&Uy%v==*35wGwIK4^iV{31mc)dS^LnN8j%#M}s%B@$=bPFI_ifcyPd4hilEWm71chIwfIR(-SeQaf20{;EF*(K(Eo+hu{}I zZkjXyF}{(x@Ql~*yig5lAq7%>-O5E++KSzEe(sqiqf1>{Em)pN`wf~WW1PntPpzKX zn;14G3FK7IQf!~n>Y=cd?=jhAw1+bwlVcY_kVuRyf!rSFNmR4fOc(g7(fR{ANvcO< zbG|cnYvKLa>dU(Z9YP796`Au?gz)Ys?w!af`F}1#W>x_O|k9Q z>#<6bKDt3Y}?KT2tmhU>H6Umn}J5M zarILVggiZs=kschc2TKib2`gl^9f|(37W93>80keUkrC3ok1q{;PO6HMbm{cZ^ROcT#tWWsQy?8qKWt<42BGryC(Dx>^ohIa0u7$^)V@Bn17^(VUgBD> zAr*Wl6UwQ&AAP%YZ;q2cZ;@2M(QeYFtW@PZ+mOO5gD1v-JzyE3^zceyE5H?WLW?$4 zhBP*+3i<09M$#XU;jwi7>}kW~v%9agMDM_V1$WlMV|U-Ldmr|<_nz*F_kcgrJnrViguEnJt{=Mk5f4Foin7(3vUXC>4gyJ>sK<;-p{h7 z2_mr&Fca!E^7R6VvodGznqJn3o)Ibd`gk>uKF7aemX*b~Sn#=NYl5j?v*T4FWZF2D zaX(M9hJ2YuEi%b~4?RkJwT*?aCRT@ecBkq$O!i}EJJEw`*++J_a>gsMo0CG^pZ3x+ zdfTSbCgRwtvAhL$p=iIf7%Vyb!j*UJsmOMler--IauWQ;(ddOk+U$WgN-RBle~v9v z9m2~@h|x*3t@m+4{U2}fKzRoVePrF-}U{`YT|vW?~64Bv*7|Dz03 zRYM^Yquhf*ZqkN?+NK4Ffm1;6BR0ZyW3MOFuV1ljP~V(=-tr^Tgu#7$`}nSd<8?cP z`VKtIz5$~InI0YnxAmn|pJZj+nPlI3zWsykXTKRnDCBm~Dy*m^^qTuY+8dSl@>&B8~0H$Y0Zc25APo|?R= z>_#h^kcfs#ae|iNe{BWA7K1mLuM%K!_V?fDyEqLkkT&<`SkEJ;E+Py^%hPVZ(%a2P4vL=vglF|X_`Z$^}q470V+7I4;UYdcZ7vU=41dd{d#KmI+|ZGa>C10g6w1a?wxAc&?iYsEv zuCwWvcw4FoG=Xrq=JNyPG*yIT@xbOeV`$s_kx`pH0DXPf0S7L?F208x4ET~j;yQ2c zhtq=S{T%82U7GxlUUKMf-NiuhHD$5*x{6}}_eZ8_kh}(}BxSPS9<(x2m$Rn0sx>)a zt$+qLRJU}0)5X>PXVxE?Jxpw(kD0W43ctKkj8DjpYq}lFZE98Je+v2t7uxuKV;p0l z5b9smYi5~k2%4aZe+~6HyobTQ@4_z#*lRHl# zSA`s~Jl@RGq=B3SNQF$+puBQv>DaQ--V!alvRSI~ZoOJx3VP4sbk!NdgMNBVbG&BX zdG*@)^g4#M#qoT`^NTR538vx~rdyOZcfzd7GBHl68-rG|fkofiGAXTJx~`~%a&boY zZ#M4sYwHIOnu-Mr!Ltpl8!NrX^p74tq{f_F4%M@&<=le;>xc5pAi&qn4P>04D$fp` z(OuJXQia--?vD0DIE6?HC|+DjH-?Cl|GqRKvs8PSe027_NH=}+8km9Ur8(JrVx@*x z0lHuHd=7*O+&AU_B;k{>hRvV}^Uxl^L1-c-2j4V^TG?2v66BRxd~&-GMfcvKhWgwu z60u{2)M{ZS)r*=&J4%z*rtqs2syPiOQq(`V0UZF)boPOql@E0U39>d>MP=BqFeJzz zh?HDKtY3%mR~reR7S2rsR0aDMA^a|L^_*8XM9KjabpYSBu z;zkfzU~12|X_W_*VNA=e^%Za14PMOC!z`5Xt|Fl$2bP9fz>(|&VJFZ9{z;;eEGhOl zl7OqqDJzvgZvaWc7Nr!5lfl*Qy7_-fy9%f(v#t#&2#9o-ba%J3(%s#C=@dagx*I{d zB&AzGT9EEiknWJU^naNdz7Logo%#OFV!eyCIQuzgpZDDN-1F}JJTdGXiLN85p|GT! zGOfNd8^RD;MsK*^3gatg2#W0J<8j)UCkUYoZRR|R*UibOm-G)S#|(`$hPA7UmH+fT ziZxTgeiR_yzvNS1s+T!xw)QgNSH(_?B@O?uTBwMj`G)2c^8%g8zu zxMu5SrQ^J+K91tkPrP%*nTpyZor#4`)}(T-Y8eLd(|sv8xcIoHnicKyAlQfm1YPyI z!$zimjMlEcmJu?M6z|RtdouAN1U5lKmEWY3gajkPuUHYRvTVeM05CE@`@VZ%dNoZN z>=Y3~f$~Gosud$AN{}!DwV<6CHm3TPU^qcR!_0$cY#S5a+GJU-2I2Dv;ktonSLRRH zALlc(lvX9rm-b5`09uNu904c}sU(hlJZMp@%nvkcgwkT;Kd7-=Z_z9rYH@8V6Assf zKpXju&hT<=x4+tCZ{elYtH+_F$V=tq@-`oC%vdO>0Wmu#w*&?_=LEWRJpW|spYc8V z=$)u#r}Pu7kvjSuM{FSyy9_&851CO^B zTm$`pF+lBWU!q>X#;AO1&=tOt=i!=9BVPC#kPJU}K$pO&8Ads)XOFr336_Iyn z$d{MTGYQLX9;@mdO;_%2Ayw3hv}_$UT00*e{hWxS?r=KT^ymEwBo429b5i}LFmSk` zo)-*bF1g;y@&o=34TW|6jCjUx{55EH&DZ?7wB_EmUg*B4zc6l7x-}qYLQR@^7o6rrgkoujRNym9O)K>wNfvY+uy+4Om{XgRHi#Hpg*bZ36_X%pP`m7FIF z?n?G*g&>kt$>J_PiXIDzgw3IupL3QZbysSzP&}?JQ-6TN-aEYbA$X>=(Zm}0{hm6J zJnqQnEFCZGmT06LAdJ^T#o`&)CA*eIYu?zzDJi#c$1H9zX}hdATSA|zX0Vb^q$mgg z&6kAJ=~gIARct>}4z&kzWWvaD9#1WK=P>A_aQxe#+4cpJtcRvd)TCu! z>eqrt)r(`qYw6JPKRXSU#;zYNB7a@MYoGuAT0Nzxr`>$=vk`uEq2t@k9?jYqg)MXl z67MA3^5_}Ig*mycsGeH0_VtK3bNo;8#0fFQ&qDAj=;lMU9%G)&HL>NO|lWU3z+m4t7 zfV*3gSuZ++rIWsinX@QaT>dsbD>Xp8%8c`HLamm~(i{7L&S0uZ;`W-tqU4XAgQclM$PxE76OH(PSjHjR$(nh({vsNnawhP!!HcP!l)5 zG;C=k0xL<^q+4rpbp{sGzcc~ZfGv9J*k~PPl}e~t$>WPSxzi0}05(D6d<=5+E}Y4e z@_QZtDcC7qh4#dQFYb6Pulf_8iAYYE z1SWJfNe5@auBbE5O=oeO@o*H5mS(pm%$!5yz-71~lEN5=x0eN|V`xAeP;eTje?eC= z53WneK;6n35{OaIH2Oh6Hx)kV-jL-wMzFlynGI8Wk_A<~_|06rKB#Pi_QY2XtIGW_ zYr)RECK_JRzR1tMd(pM(L=F98y~7wd4QBKAmFF(AF(e~+80$GLZpFc;a{kj1h}g4l z3SxIRlV=h%Pl1yRacl^g>9q%>U+`P(J`oh-w8i82mFCn|NJ5oX*^VKODX2>~HLUky z3D(ak0Sj=Kv^&8dUhU(3Ab!U5TIy97PKQ))&`Ml~hik%cHNspUpCn24cqH@dq6ZVo zO9xz!cEMm;NL;#z-tThlFF%=^ukE8S0;hDMR_`rv#eTYg7io1w9n_vJpK+6%=c#Y?wjAs_(#RQA0gr&Va2BQTq` zUc8)wHEDl&Uyo<>-PHksM;b-y(`E_t8Rez@Iw+eogcEI*FDg@Bc;;?3j3&kPsq(mx z+Yr_J#?G6D?t2G%O9o&e7Gbf&>#(-)|8)GIbG_a${TU26cVrIQSt=% zQ~XY-b1VQVc>IV=7um0^Li>dF z`zSm_o*i@ra4B+Tw5jdguVqx`O(f4?_USIMJzLvS$*kvBfEuToq-VR%K*%1VHu=++ zQ`=cG3cCnEv{ZbP-h9qbkF}%qT$j|Z7ZB2?s7nK@gM{bAD=eoDKCCMlm4LG~yre!- zzPP#Rn9ZDUgb4++M78-V&VX<1ah(DN z(4O5b`Fif%*k?L|t%!WY`W$C_C`tzC`tI7XC`->oJs_Ezs=K*O_{*#SgNcvYdmBbG zHd8!UTzGApZC}n7LUp1fe0L<3|B5GdLbxX@{ETeUB2vymJgWP0q2E<&!Dtg4>v`aa zw(QcLoA&eK{6?Rb&6P0kY+YszBLXK49i~F!jr)7|xcnA*mOe1aZgkdmt4{Nq2!!SL z`aD{6M>c00muqJt4$P+RAj*cV^vn99UtJ*s${&agQ;C>;SEM|l%KoH_^kAcmX=%)* zHpByMU_F12iGE#68rHGAHO_ReJ#<2ijo|T7`{PSG)V-bKw}mpTJwtCl%cq2zxB__m zM_p2k8pDmwA*$v@cmm>I)TW|7a7ng*X7afyR1dcuVGl|BQzy$MM+zD{d~n#)9?1qW zdk(th4Ljb-vpv5VUt&9iuQBnQ$JicZ)+HoL`&)B^Jr9F1wvf=*1and~v}3u{+7u7F zf0U`l4Qx-ANfaB3bD1uIeT^zeXerps8nIW(tmIxYSL;5~!&&ZOLVug2j4t7G=zzK+ zmPy5<4h%vq$Fw)i1)ya{D;GyEm3fybsc8$=$`y^bRdmO{XU#95EZ$I$bBg)FW#=}s z@@&c?xwLF3|C7$%>}T7xl0toBc6N^C{!>a8vWc=G!bAFKmn{AKS6RxOWIJBZXP&0CyXAiHd?7R#S46K6UXYXl#c_#APL5SfW<<-|rcfX&B6e*isa|L^RK=0}D`4q-T0VAs0 zToyrF6`_k$UFGAGhY^&gg)(Fq0p%J{h?E)WQ(h@Gy=f6oxUSAuT4ir}jI)36|NnmnI|vtij;t!jT?6Jf-E19}9Lf9(+N+ z)+0)I5mST_?3diP*n2=ZONTYdXkjKsZ%E$jjU@0w_lL+UHJOz|K{{Uh%Zy0dhiqyh zofWXzgRyFzY>zpMC8-L^43>u#+-zlaTMOS(uS!p{Jw#u3_9s)(s)L6j-+`M5sq?f+ zIIcjq$}~j9b`0_hIz~?4?b(Sqdpi(;1=8~wkIABU+APWQdf5v@g=1c{c{d*J(X5+cfEdG?qxq z{GKkF;)8^H&Xdi~fb~hwtJRsfg#tdExEuDRY^x9l6=E+|fxczIW4Z29NS~-oLa$Iq z93;5$(M0N8ba%8&q>vFc=1}a8T?P~_nrL5tYe~X>G=3QoFlBae8vVt-K!^@vusN<8gQJ!WD7H%{*YgY0#(tXxXy##C@o^U7ysxe zLmUWN@4)JBjjZ3G-_)mrA`|NPCc8Oe!%Ios4$HWpBmJse7q?)@Xk%$x&lIY>vX$7L zpfNWlXxy2p7TqW`Wq22}Q3OC2OWTP_X(*#kRx1WPe%}$C!Qn^FvdYmvqgk>^nyk;6 zXv*S#P~NVx1n6pdbXuX9x_}h1SY#3ZyvLZ&VnWVva4)9D|i7kjGY{>am&^ z-_x1UYM1RU#z17=AruK~{BK$A65Sajj_OW|cpYQBGWO*xfGJXSn4E&VMWchq%>0yP z{M2q=zx!VnO71gb8}Al2i+uxb=ffIyx@oso@8Jb88ld6M#wgXd=WcX$q$91o(94Ek zjeBqQ+CZ64hI>sZ@#tjdL}JeJu?GS7N^s$WCIzO`cvj60*d&#&-BQ>+qK#7l+!u1t zBuyL-Cqups?2>)ek2Z|QnAqs_`u1#y8=~Hvsn^2Jtx-O`limc*w;byk^2D-!*zqRi zVcX+4lzwcCgb+(lROWJ~qi;q2!t6;?%qjGcIza=C6{T7q6_?A@qrK#+)+?drrs3U}4Fov+Y}`>M z#40OUPpwpaC-8&q8yW0XWGw`RcSpBX+7hZ@xarfCNnrl-{k@`@Vv> zYWB*T=4hLJ1SObSF_)2AaX*g(#(88~bVG9w)ZE91eIQWflNecYC zzUt}ov<&)S&i$}?LlbIi9i&-g=UUgjWTq*v$!0$;8u&hwL*S^V!GPSpM3PR3Ra5*d z7d77UC4M{#587NcZS4+JN=m#i)7T0`jWQ{HK3rIIlr3cDFt4odV25yu9H1!}BVW-& zrqM5DjDzbd^pE^Q<-$1^_tX)dX8;97ILK{ z!{kF{!h`(`6__+1UD5=8sS&#!R>*KqN9_?(Z$4cY#B)pG8>2pZqI;RiYW6aUt7kk*s^D~Rml_fg$m+4+O5?J&p1)wE zp5L-X(6og1s(?d7X#l-RWO+5Jj(pAS{nz1abM^O;8hb^X4pC7ADpzUlS{F~RUoZp^ zuJCU_fq}V!9;knx^uYD2S9E`RnEsyF^ZO$;`8uWNI%hZzKq=t`q12cKEvQjJ9dww9 zCerpM3n@Ag+XZJztlqHRs!9X(Dv&P;_}zz$N&xwA@~Kfnd3}YiABK*T)Ar2E?OG6V z<;mFs`D?U7>Rradv7(?3oCZZS_0Xr#3NNkpM1@qn-X$;aNLYL;yIMX4uubh^Xb?HloImt$=^s8vm)3g!{H1D|k zmbg_Rr-ypQokGREIcG<8u(=W^+oxelI&t0U`dT=bBMe1fl+9!l&vEPFFu~yAu!XIv4@S{;| z8?%<1@hJp%7AfZPYRARF1hf`cq_VFQ-y74;EdMob{z&qec2hiQJOQa>f-?Iz^VXOr z-wnfu*uT$(5WmLsGsVkHULPBvTRy0H(}S0SQ18W0kp_U}8Phc3gz!Hj#*VYh$AiDE245!YA0M$Q@rM zT;}1DQ}MxV<)*j{hknSHyihgMPCK=H)b-iz9N~KT%<&Qmjf39L@&7b;;>9nQkDax- zk%7ZMA%o41l#(G5K=k{D{80E@P|I;aufYpOlIJXv!dS+T^plIVpPeZ)Gp`vo+?BWt z8U8u=C51u%>yDCWt>`VGkE5~2dD4y_8+n_+I9mFN(4jHJ&x!+l*>%}b4Z>z#(tb~< z+<+X~GIi`sDb=SI-7m>*krlqE3aQD?D5WiYX;#8m|ENYKw}H^95u!=n=xr3jxhCB&InJ7>zgLJg;i?Sjjd`YW!2; z%+y=LwB+MMnSGF@iu#I%!mvt)aXzQ*NW$cHNHwjoaLtqKCHqB}LW^ozBX?`D4&h%# zeMZ3ZumBn}5y9&odo3=hN$Q&SRte*^-SNZg2<}6>OzRpF91oy0{RuZU(Q0I zvx%|9>;)-Ca9#L)HQt~axu0q{745Ac;s1XQKV ze3D9I5gV5SP-J>&3U!lg1`HN>n5B6XxYpwhL^t0Z)4$`YK93vTd^7BD%<)cIm|4e!;*%9}B-3NX+J*Nr@;5(27Zmf(TmfHsej^Bz+J1 zXKIjJ)H{thL4WOuro|6&aPw=-JW8G=2 z|L4YL)^rYf7J7DOKXpTX$4$Y{-2B!jT4y^w8yh3LKRKO3-4DOshFk}N^^Q{r(0K0+ z?7w}x>(s{Diq6K)8sy)>%*g&{u>)l+-Lg~=gteW?pE`B@FE`N!F-+aE;XhjF+2|RV z8vV2((yeA-VDO;3=^E;fhW~b=Wd5r8otQrO{Vu)M1{j(+?+^q%xpYCojc6rmQ<&ytZ2ly?bw*X)WB8(n^B4Gmxr^1bQ&=m;I4O$g{ z3m|M{tmkOyAPnMHu(Z}Q1X1GM|A+)VDP3Fz934zSl)z>N|D^`G-+>Mej|VcK+?iew zQ3=DH4zz;i>z{Yv_l@j*?{936kxM{c7eK$1cf8wxL>>O#`+vsu*KR)te$adfTD*w( zAStXnZk<6N3V-Vs#GB%vXZat+(EFWbkbky#{yGY`rOvN)?{5qUuFv=r=dyYZrULf%MppWuNRUWc z8|YaIn}P0DGkwSZ(njAO$Zhr3Yw`3O1A+&F*2UjO{0`P%kK(qL;kEkfjRC=lxPRjL z{{4PO3-*5RZ_B3LUB&?ZpJ4nk1E4L&eT~HX0Jo(|uGQCW3utB@p)rF@W*n$==TlS zKiTfzhrLbAeRqru%D;fUwXOUcHud{pw@Ib1xxQ}<2)?KC&%y5PVef<7rcu2l!8dsy z?lvdaHJ#s$0m18y{x#fB$o=l)-sV?Qya5GWf#8Vd{~Grn@qgX#!EI`Y>++l%1A;eL z{_7t6jMeEr@a+oxyCL^+_}9Qc;i0&Xd%LXp?to*R|26LKHG(m0)*QF4*h;5%YG5<9)c> z1vq!7bIJSv1^27i-mcH!zX>ep3Iw0^{nx<1jOy)N_UoFD8v}x~2mEWapI3m~kMQkR z#&@4FuEGBn`mgtSx6jeY7vUQNf=^}sTZErIEpH!cy|@7Z zU4h_Oxxd2s=f{}$XXy4}%JqTSjRC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/etc/checkstyle/java-header.txt b/etc/checkstyle/java-header.txt new file mode 100644 index 000000000..ef8f05fad --- /dev/null +++ b/etc/checkstyle/java-header.txt @@ -0,0 +1,20 @@ +^\Q/*\E$ +^\Q * Copyright (c) 2019 "Neo4j,"\E$ +^\Q * Neo4j Sweden AB [http://neo4j.com]\E$ +^\Q *\E$ +^\Q * This file is part of Neo4j.\E$ +^\Q *\E$ +^\Q * Licensed under the Apache License, Version 2.0 (the "License");\E$ +^\Q * you may not use this file except in compliance with the License.\E$ +^\Q * You may obtain a copy of the License at\E$ +^\Q *\E$ +^\Q * http://www.apache.org/licenses/LICENSE-2.0\E$ +^\Q *\E$ +^\Q * Unless required by applicable law or agreed to in writing, software\E$ +^\Q * distributed under the License is distributed on an "AS IS" BASIS,\E$ +^\Q * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\E$ +^\Q * See the License for the specific language governing permissions and\E$ +^\Q * limitations under the License.\E$ +^\Q */\E$ +^\Qpackage\E .+;$ +^.*$ diff --git a/etc/checkstyle/suppressions.xml b/etc/checkstyle/suppressions.xml new file mode 100644 index 000000000..829c1a269 --- /dev/null +++ b/etc/checkstyle/suppressions.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/lombok.config b/lombok.config new file mode 100644 index 000000000..e50c7ea43 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +lombok.nonNull.exceptionType = IllegalArgumentException +lombok.log.fieldName = LOG diff --git a/mvnw b/mvnw new file mode 100755 index 000000000..5551fde8e --- /dev/null +++ b/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100755 index 000000000..48363fa60 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..c5abba6f5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,261 @@ + + + + 4.0.0 + + + org.springframework.data.build + spring-data-parent + 2.2.0.BUILD-SNAPSHOT + + + org.springframework.data + spring-data-neo4j-rx-parent + 1.0.0.BUILD-SNAPSHOT + pom + + 2019 + + + + gmeier + Gerrit Meier + gerrit.meier at neo4j.com + Neo Technology + http://www.neotechnology.com + + Project Lead + + +1 + + + msimons + Michael Simons + michael.simons at neo4j.com + Neo Technology + http://www.neotechnology.com + + Project Lead + + +1 + + + + + spring-data-neo4j-rx + spring-data-neo4j-rx-spring-boot-autoconfigure + spring-data-neo4j-rx-spring-boot-starter + + + + UTF-8 + + 3.11.1 + 8.16 + 1.8 + 0.8.2 + 5.3.2 + 3.0.0 + 3.0.0-M1 + 3.0.0-M2 + 3.0.0-M1 + 3.0.0-M1 + 3.0.1 + 3.7.1 + 3.0.1 + 3.0.0-M2 + 2.2.0.BUILD-SNAPSHOT + 3.5.2 + 1.7.2 + 1.2.1 + Californium-SR4 + 1.3.8 + 2.2.5 + + ${java.version} + ${java.version} + + + + + + org.springframework.data + spring-data-commons + ${springdata-commons.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + + + org.assertj + assertj-core + ${assertj.version} + + + org.neo4j.test + neo4j-harness + ${neo4j.version} + + + io.projectreactor + reactor-bom + ${reactor-bom.version} + pom + import + + + io.reactivex + rxjava + ${rxjava.version} + + + io.reactivex + rxjava-reactive-streams + ${reactive-streams.version} + + + io.reactivex.rxjava2 + rxjava + ${rxjava2.version} + + + + + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + + + + spring-plugins-snapshot + https://repo.spring.io/plugins-snapshot + + + + + + + + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + + etc/checkstyle/config.xml + etc/checkstyle/suppressions.xml + etc/checkstyle/java-header.txt + ${project.build.sourceEncoding} + true + true + true + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + maven-deploy-plugin + ${maven-deploy-plugin.version} + + + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + maven-install-plugin + ${maven-install-plugin.version} + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + + + maven-checkstyle-plugin + + + validate + validate + + check + + + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + + + maven-enforcer-plugin + + + enforce + validate + + enforce + + + + + + 3.6.0 + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + false + + **/*Test.java + **/*Tests.java + + + + + + diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml new file mode 100644 index 000000000..fdccc8a0d --- /dev/null +++ b/spring-data-neo4j/pom.xml @@ -0,0 +1,87 @@ + + + + 4.0.0 + + + org.springframework.data + spring-data-neo4j-rx-parent + 1.0.0.BUILD-SNAPSHOT + + + spring-data-neo4j-rx + + + + org.springframework.data + spring-data-commons + + + org.springframework + spring-core + + + org.springframework + spring-context + + + org.springframework + spring-beans + + + + io.projectreactor + reactor-core + true + + + io.reactivex + rxjava + true + + + io.reactivex + rxjava-reactive-streams + true + + + io.reactivex.rxjava2 + rxjava + true + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + io.projectreactor + reactor-test + true + test + + + diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java new file mode 100644 index 000000000..2282782d0 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import org.springframework.data.mapping.context.AbstractMappingContext; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.TypeInformation; + +/** + * {@link org.springframework.data.mapping.context.MappingContext} implementation for Neo4j. + * + * @author Michael J. Simons + */ +public class Neo4jMappingContext extends AbstractMappingContext, Neo4jPersistentProperty> { + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) + */ + @Override + protected Neo4jPersistentEntity createPersistentEntity(TypeInformation typeInformation) { + + throw new UnsupportedOperationException("Not yet implemented."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(org.springframework.data.mapping.model.Property, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder) + */ + @Override + protected Neo4jPersistentProperty createPersistentProperty(Property property, + Neo4jPersistentEntity neo4jPersistentProperties, SimpleTypeHolder simpleTypeHolder) { + + throw new UnsupportedOperationException("Not yet implemented."); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java new file mode 100644 index 000000000..dadc200b2 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import org.springframework.data.mapping.model.MutablePersistentEntity; + +/** + * A {@link org.springframework.data.mapping.PersistentEntity} interface with additional methods for metadata related to Neo4j. + * + * @author Michael J. Simons + */ +public interface Neo4jPersistentEntity extends MutablePersistentEntity { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java new file mode 100644 index 000000000..356c7d496 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import org.springframework.data.mapping.PersistentProperty; + +/** + * A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related to Neo4j. + * + * @author Michael J. Simons + */ +public interface Neo4jPersistentProperty extends PersistentProperty { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Node.java new file mode 100644 index 000000000..63c3fb822 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Node.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * The annotation to configure the mapping from a node with a given set of labels to a class and vice versa. + * + * @author Michael J. Simons + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Inherited +public @interface Node { + + @AliasFor("labels") + String[] value() default {}; + + /** + * The labels to identify a node with that is supposed to be mapped to the class annotated with {@link Node @Node}. + */ + @AliasFor("value") + String[] labels() default {}; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Property.java new file mode 100644 index 000000000..bc9fcb04f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Property.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotation to configure the mapping from a property to an attribute and vice versa. + * + * @author Michael J. Simons + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Inherited +public @interface Property { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java new file mode 100644 index 000000000..efbbe5b90 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/package-info.java @@ -0,0 +1,9 @@ +/** + * The main mapping framework. This package contains all the public facing annotations necessary to mark Spring Data Neo4j entities. + * + * @author Michael J. Simons + */ +@NonNullApi +package org.springframework.data.neo4j.core.mapping; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/Neo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/Neo4jRepository.java new file mode 100644 index 000000000..0364330dc --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/Neo4jRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository; + +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.QueryByExampleExecutor; + +/** + * Neo4j specific {@link org.springframework.data.repository.Repository} interface. + * + * @author Michael J. Simons + */ +@NoRepositoryBean +public interface Neo4jRepository extends PagingAndSortingRepository, QueryByExampleExecutor { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/ReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/ReactiveNeo4jRepository.java new file mode 100644 index 000000000..9d23f6ab3 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/ReactiveNeo4jRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository; + +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; +import org.springframework.data.repository.reactive.ReactiveSortingRepository; + +/** + * Neo4j specific {@link org.springframework.data.repository.Repository} interface with reactive support. + * + * @author Michael J. Simons + */ +@NoRepositoryBean +public interface ReactiveNeo4jRepository + extends ReactiveSortingRepository, ReactiveQueryByExampleExecutor { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java new file mode 100644 index 000000000..2e9aa1f54 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java @@ -0,0 +1,7 @@ +/** + * This package contains the public repository API. + */ +@NonNullApi +package org.springframework.data.neo4j.repository; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java new file mode 100644 index 000000000..4db2b0827 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class Neo4jMappingContextTest { + + @Test + void shouldCreatePersistentEntity() { + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + new Neo4jMappingContext().createPersistentEntity(null)); + } +} From 24a9dbc8f95d9b7570aedca85f7c0e73e0b963e8 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 6 Feb 2019 09:22:07 +0100 Subject: [PATCH 003/342] Add some structure for a Neo4j client. --- pom.xml | 86 ++++++++++++++++++++++++--------------- spring-data-neo4j/pom.xml | 55 ++++++++++++++----------- 2 files changed, 85 insertions(+), 56 deletions(-) diff --git a/pom.xml b/pom.xml index c5abba6f5..a9c59976e 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,8 @@ | See the License for the specific language governing permissions and | limitations under the License. --> - + 4.0.0 @@ -31,7 +32,20 @@ 1.0.0.BUILD-SNAPSHOT pom + Spring Data Neo4j.rx + Next generation Object-Graph-Mapping for Spring Data. 2019 + + Neo4j, Neo4j Sweden AB + http://neo4j.com + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + @@ -59,14 +73,13 @@ + neo4j-java-client-parent spring-data-neo4j-rx - spring-data-neo4j-rx-spring-boot-autoconfigure - spring-data-neo4j-rx-spring-boot-starter + spring-data-neo4j-rx-spring-boot-starter-parent UTF-8 - 3.11.1 8.16 1.8 @@ -88,40 +101,12 @@ Californium-SR4 1.3.8 2.2.5 - ${java.version} ${java.version} - - org.springframework.data - spring-data-commons - ${springdata-commons.version} - - - org.junit.jupiter - junit-jupiter-engine - ${junit-jupiter.version} - - - org.assertj - assertj-core - ${assertj.version} - - - org.neo4j.test - neo4j-harness - ${neo4j.version} - - - io.projectreactor - reactor-bom - ${reactor-bom.version} - pom - import - io.reactivex rxjava @@ -137,6 +122,43 @@ rxjava ${rxjava2.version} + + org.assertj + assertj-core + ${assertj.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + + + org.neo4j + neo4j + ${neo4j.version} + + + org.neo4j.driver + neo4j-java-driver + ${neo4j-java-driver.version} + + + org.neo4j.test + neo4j-harness + ${neo4j.version} + + + org.springframework.data + spring-data-commons + ${springdata-commons.version} + + + io.projectreactor + reactor-bom + ${reactor-bom.version} + pom + import + diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index fdccc8a0d..5e46b302d 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -17,7 +17,8 @@ | See the License for the specific language governing permissions and | limitations under the License. --> - + 4.0.0 @@ -28,24 +29,10 @@ spring-data-neo4j-rx - - - org.springframework.data - spring-data-commons - - - org.springframework - spring-core - - - org.springframework - spring-context - - - org.springframework - spring-beans - + SDN.rx + Core module of SDN.rx. + io.projectreactor reactor-core @@ -66,11 +53,32 @@ rxjava true - - org.junit.jupiter - junit-jupiter-engine + org.neo4j.client + neo4j-client-api + 1.0.0.BUILD-SNAPSHOT + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + org.springframework + spring-core + + + org.springframework.data + spring-data-commons + + + io.projectreactor + reactor-test test + true org.assertj @@ -78,9 +86,8 @@ test - io.projectreactor - reactor-test - true + org.junit.jupiter + junit-jupiter-engine test From 122c363720092528e98369385d2965a4f8e1ba2a Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 6 Feb 2019 17:14:57 +0100 Subject: [PATCH 004/342] Remove drivers abstraction layer, add some architectural thoughts. After some discussion we decided to move the support for embedded connections down into the drivers layer where it actually belongs. Thus the additional modules ("transports") like we currently have in OGM can go away. --- README.adoc | 46 +++++++++++++++++++ pom.xml | 3 +- spring-data-neo4j/pom.xml | 7 ++- .../session/DefaultNeo4jSessionFactory.java | 36 +++++++++++++++ .../core/session/Neo4jSessionFactory.java | 31 +++++++++++++ .../session/Neo4jSessionFactorySupport.java | 30 ++++++++++++ .../data/neo4j/core/session/package-info.java | 7 +++ 7 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 README.adoc create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java diff --git a/README.adoc b/README.adoc new file mode 100644 index 000000000..ff522f75e --- /dev/null +++ b/README.adoc @@ -0,0 +1,46 @@ += Spring Data Neo4j⚡️RX + +[abstract] +-- +This is a work in progress project determining a possible future form of Spring Data Neo4j. +Expect the README and even more the project to change quite a lot in the future weeks. +-- + +== Architectural guidelines and principles + +The next version of Spring Data Neo4j should be designed with the following principles in mind: + +* Rely completely on the https://github.com/neo4j/neo4j-java-driver[Neo4j Java Driver], without introducing another "driver" or "transport" layer between the mapping framework and the driver. +* Immutable entities and thus full support for Kotlin's data classes right from the start. +* Work result item / record and not result set oriented, thus not reading the complete result set before the mapping starts, but make a "row" the foundation for any mapping. +This encourages generation of optimized queries, which should greatly reduce the object graph impedance mismatch we see in some projects using Neo4j-OGM. +* Follow up on the reactive story for database access in general. Being immutable and row oriented are two main requirements for making things possible. + +=== Modules + +So far we have identified the following modules: + +* Schema: Should read a set of given classes and create a schema (Metagraph) from it +* Mapping: Should take care of hydrating from results to domain objects and dehydrating vice versa. It can depend on schema, but only as a provider for property matching +* Lifecycle: Lifecycle must not depend directly on mapping, but should only care whether an Object and its Relations are managed or not +* Querying: Generates cypher queries, depends on schema + +Those will be reassembled as packages inside Spring Data Neo4j RX. +There are no short-term planes to create additional artifacts from those. + +NOTE: However, we should discuss if you could try a JDK 11 only project. + +=== The embedded "problem" + +Supporting the embedded usecase will be solved on the drivers level. + +== Configuration + +Spring Data Neo4j RX takes a "ready to use" drivers instance and uses that. +We won't provide any additional configuration for aspects that are configurable through the driver. +We will however provide support to configure the drivers instance in Spring Boot. +The current Spring Boot Starter only configures the Neo4j-OGM transport and not the "real" driver. + +Closing the driver is not the the concern of Spring Data Neo4j RX. +The lifecycle of that bean should be managed by the application. +Therefore, the starter need to take care of register the drivers instance with the application. diff --git a/pom.xml b/pom.xml index a9c59976e..3cb3ab63b 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 1.0.0.BUILD-SNAPSHOT pom - Spring Data Neo4j.rx + Spring Data Neo4j RX Next generation Object-Graph-Mapping for Spring Data. 2019 @@ -73,7 +73,6 @@ - neo4j-java-client-parent spring-data-neo4j-rx spring-data-neo4j-rx-spring-boot-starter-parent diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 5e46b302d..43f945f29 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -29,7 +29,7 @@ spring-data-neo4j-rx - SDN.rx + SDN⚡️RX Core module of SDN.rx. @@ -54,9 +54,8 @@ true - org.neo4j.client - neo4j-client-api - 1.0.0.BUILD-SNAPSHOT + org.neo4j.driver + neo4j-java-driver org.springframework diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java new file mode 100644 index 000000000..aacd35ef7 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.session; + +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.Session; + +/** + * Default implementation of a {@link Neo4jSessionFactory}. + * + * @author Michael J. Simons + */ +class DefaultNeo4jSessionFactory extends Neo4jSessionFactorySupport { + + @Override + public Session getSession() { + + throw new UnsupportedOperationException("Not there yet."); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java new file mode 100644 index 000000000..0d79c36ea --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.session; + +import org.neo4j.driver.v1.Session; + +/** + * Interface for factories creating {@link org.neo4j.driver.v1.Session Neo4j driver sessions}. + * + * @author Michael J. Simons + */ +public interface Neo4jSessionFactory { + + Session getSession(); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java new file mode 100644 index 000000000..7c5f3d4eb --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.session; + +import org.neo4j.driver.v1.Driver; + +/** + * Support infrastructure for dealing with the Neo4j Driver. + * + * @param Type of the driver. + * @author Michael J. Simons + */ +abstract class Neo4jSessionFactorySupport implements Neo4jSessionFactory { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java new file mode 100644 index 000000000..7340af78b --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java @@ -0,0 +1,7 @@ +/** + * Core infrastructure for providing Neo4j sessions to Spring Data Neo4j. + */ +@NonNullApi +package org.springframework.data.neo4j.core.session; + +import org.springframework.lang.NonNullApi; From 3210593a573f8864eb309bfb47c89a1bf70b7bc2 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 13 Feb 2019 13:04:04 +0100 Subject: [PATCH 005/342] Add example module. --- README.adoc | 5 +++++ pom.xml | 4 ++-- spring-data-neo4j/pom.xml | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.adoc b/README.adoc index ff522f75e..ce2431fa4 100644 --- a/README.adoc +++ b/README.adoc @@ -30,6 +30,11 @@ There are no short-term planes to create additional artifacts from those. NOTE: However, we should discuss if you could try a JDK 11 only project. +=== Other principles + +* Ensure that the underlying store (Neo4j) leaks as little as possible into the mapping. + I.e. reuse `@Id` etc. and avoid adding custom variants of annoation whenever possible. + === The embedded "problem" Supporting the embedded usecase will be solved on the drivers level. diff --git a/pom.xml b/pom.xml index 3cb3ab63b..a7703090d 100644 --- a/pom.xml +++ b/pom.xml @@ -17,8 +17,7 @@ | See the License for the specific language governing permissions and | limitations under the License. --> - + 4.0.0 @@ -75,6 +74,7 @@ spring-data-neo4j-rx spring-data-neo4j-rx-spring-boot-starter-parent + spring-data-neo4j-rx-examples diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 43f945f29..e450e0433 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -17,8 +17,7 @@ | See the License for the specific language governing permissions and | limitations under the License. --> - + 4.0.0 From c9012b808529c858133b257503985b90f530e1d1 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 13 Feb 2019 14:40:05 +0100 Subject: [PATCH 006/342] Typo fixing in readmes. --- README.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index ce2431fa4..80d653095 100644 --- a/README.adoc +++ b/README.adoc @@ -33,11 +33,11 @@ NOTE: However, we should discuss if you could try a JDK 11 only project. === Other principles * Ensure that the underlying store (Neo4j) leaks as little as possible into the mapping. - I.e. reuse `@Id` etc. and avoid adding custom variants of annoation whenever possible. + I.e. reuse `@Id` etc. and avoid adding custom variants of annotation whenever possible. === The embedded "problem" -Supporting the embedded usecase will be solved on the drivers level. +Supporting the embedded use case will be solved on the drivers level. == Configuration From f97f79ef2ca38e43c7c440c641c14848467ab6e4 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 13 Feb 2019 20:35:44 +0100 Subject: [PATCH 007/342] Add EnableNeo4jRepositories annotation. This is mostly a stub because it does nothing yet besides providing the basic configuration attributes for package scanning. --- .../config/EnableNeo4jRepositories.java | 66 +++++++++++++++++++ .../config/Neo4jRepositoriesRegistrar.java | 50 ++++++++++++++ .../neo4j/repository/config/package-info.java | 7 ++ .../config/EnableNeo4jRepositoriesTests.java | 63 ++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositoriesTests.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java new file mode 100644 index 000000000..e0950d9ad --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()}, + * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated + * configuration class. + * + * @author Gerrit Meier + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(Neo4jRepositoriesRegistrar.class) +public @interface EnableNeo4jRepositories { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: + * {@code @EnableNeo4jRepositories("org.my.pkg")} instead of + * {@code @EnableNeo4jRepositories(basePackages="org.my.pkg")}. + */ + @AliasFor("basePackages") + String[] value() default {}; + + /** + * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this + * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + */ + @AliasFor("value") + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The + * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in + * each package that serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java new file mode 100644 index 000000000..79f4118f5 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.config; + +import java.lang.annotation.Annotation; + +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +/** + * {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableNeo4jRepositories} annotation. + * + * @author Gerrit Meier + */ +class Neo4jRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableNeo4jRepositories.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension() + */ + @Override + protected RepositoryConfigurationExtension getExtension() { + return null; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java new file mode 100644 index 000000000..0cd9cd83a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java @@ -0,0 +1,7 @@ +/** + * Configuration infrastructure for Neo4j specific repositories. + */ +@NonNullApi +package org.springframework.data.neo4j.repository.config; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositoriesTests.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositoriesTests.java new file mode 100644 index 000000000..ebbb2f3c0 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositoriesTests.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.config; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.core.annotation.AnnotationUtils; + +/** + * @author Gerrit Meier + */ +class EnableNeo4jRepositoriesTests { + + private static final String BASE_PACKAGES_VALUE = "basePackages"; + + @Test + void valueIsAliasForBasePackages() { + + EnableNeo4jRepositories annotation = AnnotationUtils.findAnnotation(EnableRepositoryConfigWithValue.class, + EnableNeo4jRepositories.class); + + assertThat(annotation).isNotNull(); + assertThat(annotation.value()).containsExactly(BASE_PACKAGES_VALUE); + assertThat(annotation.value()).containsExactly(annotation.basePackages()); + } + + @Test + void basePackagesIsAliasForValue() { + + EnableNeo4jRepositories annotation = AnnotationUtils.findAnnotation(EnableRepositoryConfigWithBasePackages.class, + EnableNeo4jRepositories.class); + + assertThat(annotation).isNotNull(); + assertThat(annotation.basePackages()).containsExactly(BASE_PACKAGES_VALUE); + assertThat(annotation.basePackages()).containsExactly(annotation.value()); + } + + @EnableNeo4jRepositories(BASE_PACKAGES_VALUE) + private class EnableRepositoryConfigWithValue { + } + + @EnableNeo4jRepositories(basePackages = BASE_PACKAGES_VALUE) + private class EnableRepositoryConfigWithBasePackages { + } + +} From b2df65eaa448b69280f07ebf18a23ed0b85b4030 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 13 Feb 2019 23:01:09 +0100 Subject: [PATCH 008/342] First architecture bits with package structure focus. --- README.adoc | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/README.adoc b/README.adoc index 80d653095..d0d31490f 100644 --- a/README.adoc +++ b/README.adoc @@ -49,3 +49,76 @@ The current Spring Boot Starter only configures the Neo4j-OGM transport and not Closing the driver is not the the concern of Spring Data Neo4j RX. The lifecycle of that bean should be managed by the application. Therefore, the starter need to take care of register the drivers instance with the application. + +== Architecture + +This is definitely not the last version of the architecture. +It is only meant to be a basic for discussions. + +=== Package structure + +.A rough outline of the current and maybe future package structure +[plantuml, width=800] +---- +@startuml +note "currently only in Spring Data JDBC" as JDBC_note +package "org.springframework.data.neo4j" { +package "core" { +JDBC_note..convert + package "mapping" { + note "split into Spring Data and Neo4j mapping" as mappingNote + annotation Node + annotation Property + interface Neo4jPersistentEntity + interface Neo4jPersistentProperty + } + package "session" { + note "do we need/want a Neo4jTemplate" as templateNote + interface Neo4jSessionFactory + } + package "convert" { + note "conversion support" as conversionNote + } +} + +package "repository" { +JDBC_note..query +JDBC_note..support + package "config" { + class EnableNeo4jRepository + class Neo4jRepositoryRegistrar + class Neo4jRepositoryConfigExtension + } + package "query" { + class Query + } + package "support" { + class RepositoryFactoryBean + class SimpleNeo4jRepository + class Neo4jQueryLookupStrategy + } +} + +core-[hidden]--->repository +} + +@enduml +---- + +[options="header"] +|=== +|Package|Comment +|core.mapping +|Should get split to separate the Spring related mapping information like `Neo4jPersistentEntity` from incoming data mapping +|core.session +|Connection to the `Driver` instance. Maybe also a place to consider abstraction by introducing a `Neo4jTemplate`? +|core.convert +|_not used yet_ place for conversion related classes. +| | +|repository.config +|General setup: `@EnableNeo4jRepository` and related `Registrar` and `Config` classes. +|repository.query +|Place where `@Query` and other query method related annotations go in. +|repository.support +|"Glue code" like `Neo4jRepositoryFactoryBean`, `SimpleNeo4jRepository` etc. +|=== From 6a2de8c95d4e1b6767d476d5c75c6bd73b98072b Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 14 Feb 2019 10:10:19 +0100 Subject: [PATCH 009/342] Polishing package architecture document section. --- README.adoc | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index d0d31490f..d5f900b79 100644 --- a/README.adoc +++ b/README.adoc @@ -62,6 +62,7 @@ It is only meant to be a basic for discussions. ---- @startuml note "currently only in Spring Data JDBC" as JDBC_note +note "Implementation of Spring Data Commons SPI" as SDC_note package "org.springframework.data.neo4j" { package "core" { JDBC_note..convert @@ -84,6 +85,7 @@ JDBC_note..convert package "repository" { JDBC_note..query JDBC_note..support +SDC_note..config package "config" { class EnableNeo4jRepository class Neo4jRepositoryRegistrar @@ -108,15 +110,19 @@ core-[hidden]--->repository [options="header"] |=== |Package|Comment +|core +|`Neo4jTemplate` and related classes. |core.mapping -|Should get split to separate the Spring related mapping information like `Neo4jPersistentEntity` from incoming data mapping +|Spring mapping information. +|core.mapping.internal +|Neo4j data mapping. |core.session -|Connection to the `Driver` instance. Maybe also a place to consider abstraction by introducing a `Neo4jTemplate`? +|Connection to the `Driver` instance. |core.convert |_not used yet_ place for conversion related classes. | | |repository.config -|General setup: `@EnableNeo4jRepository` and related `Registrar` and `Config` classes. +|Register all needed beans for Spring context. |repository.query |Place where `@Query` and other query method related annotations go in. |repository.support From 25101eb1793b47ecee690716f65d37b84f338d9e Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 14 Feb 2019 10:23:29 +0100 Subject: [PATCH 010/342] Add repository configuration and factory beans. --- .../config/Neo4jRepositoriesRegistrar.java | 6 ++- ...Neo4jRepositoryConfigurationExtension.java | 52 +++++++++++++++++++ .../support/Neo4jRepositoryFactoryBean.java | 50 ++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java index 79f4118f5..4bc9d8a1b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java @@ -25,8 +25,12 @@ import org.springframework.data.repository.config.RepositoryConfigurationExtensi /** * {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableNeo4jRepositories} annotation. + * The {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of Spring's + * {@code org.springframework.context.annotation.ImportBeanDefinitionRegistrar}, a dedicated SPI to register beans + * during processing of configuration classes. * * @author Gerrit Meier + * @author Michael J. Simons */ class Neo4jRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { @@ -45,6 +49,6 @@ class Neo4jRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSuppor */ @Override protected RepositoryConfigurationExtension getExtension() { - return null; + return new Neo4jRepositoryConfigurationExtension(); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java new file mode 100644 index 000000000..dfe7a5812 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.config; + +import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; + +/** + * This dedicated Neo4j repository extension will be registered via {@link Neo4jRepositoriesRegistrar} and then provide + * all necessary beans to be registered in the application's context before the user's "business" beans gets registered. + * + * @author Michael J. Simons + * @author Gerrit Meier + */ +class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + private static final String MODULE_PREFIX = "neo4j"; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config14.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() + */ + @Override + public String getRepositoryFactoryBeanClassName() { + return Neo4jRepositoryFactoryBean.class.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config14.RepositoryConfigurationExtensionSupport#getModulePrefix() + */ + @Override + protected String getModulePrefix() { + return MODULE_PREFIX; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java new file mode 100644 index 000000000..38c00afef --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.support; + +import java.io.Serializable; + +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; + +/** + * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of + * repository factories via Spring configuration. + * + * @author Michael J. Simons + * @author Gerrit Meier + */ +public class Neo4jRepositoryFactoryBean, S, ID extends Serializable> + extends TransactionalRepositoryFactoryBeanSupport { + + /** + * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + protected Neo4jRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + @Override + protected RepositoryFactorySupport doCreateRepositoryFactory() { + return null; + } +} From 9846a46abc3545c01a8a03189d74f3e625198396 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 18 Feb 2019 12:40:51 +0100 Subject: [PATCH 011/342] Add suggestion for @Query-infrastructure. --- .../data/neo4j/repository/package-info.java | 2 +- .../neo4j/repository/query/CountQuery.java | 37 ++++++++++++++ .../neo4j/repository/query/ExistsQuery.java | 37 ++++++++++++++ .../data/neo4j/repository/query/Query.java | 51 +++++++++++++++++++ .../neo4j/repository/query/package-info.java | 7 +++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java index 2e9aa1f54..d3fc29f62 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java @@ -1,5 +1,5 @@ /** - * This package contains the public repository API. + * This package contains the repository infrastructure. */ @NonNullApi package org.springframework.data.neo4j.repository; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java new file mode 100644 index 000000000..b5664b223 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.query; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specialized version of {@link Query} whose values is always used as count projection. + * + * @author Michael J. Simons + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@Query(count = true) +public @interface CountQuery { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java new file mode 100644 index 000000000..6a8ef9c3c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.query; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specialized version of {@link Query} whose values is always used as exists projection. + * + * @author Michael J. Simons + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@Query(exists = true) +public @interface ExistsQuery { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java new file mode 100644 index 000000000..afd287515 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.query; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; + +/** + * Annotation to provide Cypher statements that will be used for executing the method. The Cypher statement may contain named + * parameters as supported by the >Neo4j Java Driver Date: Mon, 18 Feb 2019 13:50:01 +0100 Subject: [PATCH 012/342] Add jQassistant. --- README.adoc | 11 ++++++++++ etc/jqassistant/index.adoc | 16 +++++++++++++++ etc/jqassistant/naming.adoc | 16 +++++++++++++++ etc/jqassistant/structure.adoc | 15 ++++++++++++++ pom.xml | 10 +++++++++ spring-data-neo4j/pom.xml | 37 ++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+) create mode 100644 etc/jqassistant/index.adoc create mode 100644 etc/jqassistant/naming.adoc create mode 100644 etc/jqassistant/structure.adoc diff --git a/README.adoc b/README.adoc index d5f900b79..491d85709 100644 --- a/README.adoc +++ b/README.adoc @@ -128,3 +128,14 @@ core-[hidden]--->repository |repository.support |"Glue code" like `Neo4jRepositoryFactoryBean`, `SimpleNeo4jRepository` etc. |=== + +The structure of this project can be explored as a Graph. +We use https://jqassistant.org[jQAssistant] to verify our architecture during the build. +Run the following two commands + +``` +./mvnw clean compile jqassistant:scan +./mvnw jqassistant:server +``` + +and point your browser to http://localhost:7474. diff --git a/etc/jqassistant/index.adoc b/etc/jqassistant/index.adoc new file mode 100644 index 000000000..fcd328d58 --- /dev/null +++ b/etc/jqassistant/index.adoc @@ -0,0 +1,16 @@ += Coding Rules + +The following rules are checked during a build: + +[[default]] +[role=group,includesGroups="naming:Default,structure:Default"] +- <> +- <> + +== Naming things + +include::naming.adoc[] + +== Structuring things + +include::structure.adoc[] diff --git a/etc/jqassistant/naming.adoc b/etc/jqassistant/naming.adoc new file mode 100644 index 000000000..b8b760c84 --- /dev/null +++ b/etc/jqassistant/naming.adoc @@ -0,0 +1,16 @@ +[[naming:Default]] +[role=group,includesConstraints="naming:TypeNameMustBeginWithGroupId"] + +The following naming conventions are used throughout the project: + +[[naming:TypeNameMustBeginWithGroupId]] +[source,cypher,role=constraint] +.All Java types must be located in packages that start with `org.springframework.data.neo4j`. +---- +MATCH + (project:Maven:Project)-[:CREATES]->(:Artifact)-[:CONTAINS]->(type:Type) +WHERE + NOT type.fqn starts with 'org.springframework.data.neo4j' +RETURN + project as Project, collect(type) as TypeWithWrongName +---- diff --git a/etc/jqassistant/structure.adoc b/etc/jqassistant/structure.adoc new file mode 100644 index 000000000..18dd3dc66 --- /dev/null +++ b/etc/jqassistant/structure.adoc @@ -0,0 +1,15 @@ +[[structure:Default]] +[role=group,includesConstraints="structure:mapping"] + +Most of the time, the package structure under `org.springframework.data.neo4j` should reflect the main building parts. + +[[structure:mapping]] +[source,cypher,role=constraint,requiresConcepts="dependency:Package"] +.The mapping package must not depend on other SDN-RX packages. +---- +MATCH (a:Main:Artifact) +MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a) +WHERE p1.fqn = 'org.springframework.data.neo4j.core.mapping' + AND NOT (p1) -[:CONTAINS]-> (p2) +return p1,p2 +---- diff --git a/pom.xml b/pom.xml index a7703090d..a374e8382 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,8 @@ 8.16 1.8 0.8.2 + 1.6.0 + 1.6.0 5.3.2 3.0.0 3.0.0-M1 @@ -197,6 +199,14 @@ true + + com.buschmais.jqassistant + jqassistant-maven-plugin + ${jqassistant.version} + + etc/jqassistant + + org.jacoco jacoco-maven-plugin diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index e450e0433..8b9988d54 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -89,4 +89,41 @@ test + + + + + com.buschmais.jqassistant + jqassistant-maven-plugin + + + + scan + analyze + + + MINOR + + + + + + + + + + + com.buschmais.jqassistant + jqassistant-maven-plugin + ${jqassistant.version} + + + + report + + + + + + From 82af4a49e12d2bcfb52177ece165ebf792e5c9da Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 20 Feb 2019 16:39:02 +0100 Subject: [PATCH 013/342] Update and refine packaging documentation. --- README.adoc | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.adoc b/README.adoc index 491d85709..3d0e219b7 100644 --- a/README.adoc +++ b/README.adoc @@ -58,23 +58,23 @@ It is only meant to be a basic for discussions. === Package structure .A rough outline of the current and maybe future package structure -[plantuml, width=800] +[plantuml, width=1200] ---- @startuml -note "currently only in Spring Data JDBC" as JDBC_note note "Implementation of Spring Data Commons SPI" as SDC_note package "org.springframework.data.neo4j" { package "core" { -JDBC_note..convert + interface Neo4jTemplate package "mapping" { - note "split into Spring Data and Neo4j mapping" as mappingNote + package "internal" { + note "GraphToObjectMapping" as graphMapping + } annotation Node annotation Property interface Neo4jPersistentEntity interface Neo4jPersistentProperty } package "session" { - note "do we need/want a Neo4jTemplate" as templateNote interface Neo4jSessionFactory } package "convert" { @@ -83,8 +83,6 @@ JDBC_note..convert } package "repository" { -JDBC_note..query -JDBC_note..support SDC_note..config package "config" { class EnableNeo4jRepository @@ -92,13 +90,15 @@ SDC_note..config class Neo4jRepositoryConfigExtension } package "query" { - class Query + annotation Query } package "support" { - class RepositoryFactoryBean + class Neo4jRepositoryFactoryBean class SimpleNeo4jRepository class Neo4jQueryLookupStrategy } + interface Neo4jRepository + interface ReactiveNeo4jRepository } core-[hidden]--->repository @@ -121,6 +121,8 @@ core-[hidden]--->repository |core.convert |_not used yet_ place for conversion related classes. | | +|repository +|Repository interfaces like `Neo4jRepository`. |repository.config |Register all needed beans for Spring context. |repository.query @@ -129,6 +131,7 @@ core-[hidden]--->repository |"Glue code" like `Neo4jRepositoryFactoryBean`, `SimpleNeo4jRepository` etc. |=== +=== Architecture validation The structure of this project can be explored as a Graph. We use https://jqassistant.org[jQAssistant] to verify our architecture during the build. Run the following two commands From 6659f8fc2621df57df87599fe4699c20f8417a07 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 26 Feb 2019 14:01:04 +0100 Subject: [PATCH 014/342] Infrastructure to enable Neo4jRepositories. This will introduce the infrastructure needed to initialize the basic `SimpleNeo4jRepository` and autowire a given `Neo4jTemplate` to it when `@EnableNeo4jRepositories` is set. --- spring-data-neo4j/pom.xml | 4 + .../data/neo4j/core/Neo4jOperations.java | 27 ++++ .../data/neo4j/core/Neo4jTemplate.java | 29 ++++ .../config/EnableNeo4jRepositories.java | 39 +++++ ...Neo4jRepositoryConfigurationExtension.java | 15 ++ .../support/Neo4jRepositoryFactory.java | 55 +++++++ .../support/Neo4jRepositoryFactoryBean.java | 10 +- .../support/SimpleNeo4jRepository.java | 137 ++++++++++++++++++ .../data/neo4j/integration/Person.java | 29 ++++ .../neo4j/integration/PersonRepository.java | 25 ++++ .../integration/RepositoryCreationTest.java | 60 ++++++++ 11 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/Person.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 8b9988d54..2715f5809 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -72,6 +72,10 @@ org.springframework.data spring-data-commons + + org.springframework + spring-tx + io.projectreactor reactor-test diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java new file mode 100644 index 000000000..625c08245 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core; + +/** + * Definition of Neo4j specific operations. Implemented by {@link Neo4jTemplate}. + * + * @author Gerrit Meier + */ +public interface Neo4jOperations { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java new file mode 100644 index 000000000..43c323005 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core; + +/** + * Default implementation of {@link Neo4jOperations}. Uses the Neo4j Java driver to connect to and interact with the + * database. + * + * @author Gerrit Meier + */ +public class Neo4jTemplate implements Neo4jOperations { + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java index e0950d9ad..2a2bdef7d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java @@ -25,8 +25,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; /** * Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()}, @@ -63,4 +67,39 @@ public @interface EnableNeo4jRepositories { * each package that serves no purpose other than being referenced by this attribute. */ Class[] basePackageClasses() default {}; + + /** + * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to + * {@link Neo4jRepositoryFactoryBean}. + */ + Class repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class; + + /** + * Configures the name of the {@link Neo4jTemplate} bean to be used with the repositories detected. + */ + String neo4jTemplateRef() default "neo4jTemplate"; + + /** + * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from + * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters. + */ + ComponentScan.Filter[] includeFilters() default {}; + + /** + * Specifies which types are not eligible for component scanning. + */ + ComponentScan.Filter[] excludeFilters() default {}; + + /** + * Configures the location of where to find the Spring Data named queries properties file. Will default to + * {@code META-INFO/neo4j-named-queries.properties}. + */ + String namedQueriesLocation() default ""; + + /** + * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So + * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning + * for {@code PersonRepositoryImpl}. + */ + String repositoryImplementationPostfix() default "Impl"; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index dfe7a5812..a43f39e6f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -18,7 +18,10 @@ */ package org.springframework.data.neo4j.repository.config; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; /** @@ -49,4 +52,16 @@ class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExten protected String getModulePrefix() { return MODULE_PREFIX; } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource) + */ + @Override + public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { + + AnnotationAttributes attributes = config.getAttributes(); + + builder.addPropertyReference("neo4jOperations", attributes.getString("neo4jTemplateRef")); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java new file mode 100644 index 000000000..0e63e0210 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.support; + +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +/** + * Factory to create {@link Neo4jRepository} instances. + * + * @author Gerrit Meier + */ +public class Neo4jRepositoryFactory extends RepositoryFactorySupport { + + private final Neo4jOperations neo4jOperations; + + public Neo4jRepositoryFactory(Neo4jOperations neo4jOperations) { + this.neo4jOperations = neo4jOperations; + } + + @Override + public EntityInformation getEntityInformation(Class domainClass) { + return null; + } + + @Override + protected Object getTargetRepository(RepositoryInformation metadata) { + return getTargetRepositoryViaReflection(metadata, neo4jOperations); + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return SimpleNeo4jRepository.class; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java index 38c00afef..7a6a90080 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.repository.support; import java.io.Serializable; +import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; @@ -34,6 +35,8 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF public class Neo4jRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport { + private Neo4jOperations neo4jOperations; + /** * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface. * @@ -43,8 +46,13 @@ public class Neo4jRepositoryFactoryBean, S, ID exten super(repositoryInterface); } + public void setNeo4jOperations(Neo4jOperations neo4jOperations) { + this.neo4jOperations = neo4jOperations; + } + @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { - return null; + return new Neo4jRepositoryFactory(neo4jOperations); } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java new file mode 100644 index 000000000..18c79b759 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.support; + +import java.util.Optional; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.repository.Neo4jRepository; + +/** + * Repository base implementation for Neo4j. + * + * @author Gerrit Meier + */ +public class SimpleNeo4jRepository implements Neo4jRepository { + + private final Neo4jOperations neo4jOperations; + + public SimpleNeo4jRepository(Neo4jOperations neo4jOperations) { + this.neo4jOperations = neo4jOperations; + } + + @Override + public Iterable findAll(Sort sort) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Page findAll(Pageable pageable) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public S save(S entity) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Iterable saveAll(Iterable entities) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Optional findById(ID id) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public boolean existsById(ID id) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Iterable findAll() { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Iterable findAllById(Iterable ids) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public long count() { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public void deleteById(ID id) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public void delete(T entity) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public void deleteAll(Iterable entities) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public void deleteAll() { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Optional findOne(Example example) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Iterable findAll(Example example) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Iterable findAll(Example example, Sort sort) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public Page findAll(Example example, Pageable pageable) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public long count(Example example) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public boolean exists(Example example) { + throw new UnsupportedOperationException("Not there yet."); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/Person.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/Person.java new file mode 100644 index 000000000..da799596d --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/Person.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.integration; + +import org.springframework.data.annotation.Id; + +public class Person { + + @Id + private Long id; + + private String name; +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java new file mode 100644 index 000000000..814ca5c4b --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.integration; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +public interface PersonRepository extends Neo4jRepository { + +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java new file mode 100644 index 000000000..4e1efc12f --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.integration; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +@ContextConfiguration +@RunWith(SpringRunner.class) +public class RepositoryCreationTest { + + @Autowired private PersonRepository repository; + + @Test + public void repositoryGetsCreated() { + assertThat(repository).isNotNull(); + } + + @Test + public void repositoryCallFailsBecauseOfUnsupportedOperationException() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll()); + } + + @Configuration + @EnableNeo4jRepositories + static class Config { + + @Bean + public Neo4jOperations neo4jTemplate() { + return new Neo4jTemplate(); + } + + } +} From 0109d7787b0cfeae0548411f316e378f6681877e Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 26 Feb 2019 22:24:52 +0100 Subject: [PATCH 015/342] Add condensed initialization description for SimpleNeo4jRepository`. --- README.adoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.adoc b/README.adoc index 3d0e219b7..e2fa378c4 100644 --- a/README.adoc +++ b/README.adoc @@ -142,3 +142,18 @@ Run the following two commands ``` and point your browser to http://localhost:7474. + +== `SimpleNeo4jRepository` initialization +. `@EnableNeo4jRepositories` defines + ** the `repositoryFactoryBeanClass` that defaults to `Neo4jRepositoryFactoryBean.class`. (I) + ** `Neo4jRepositoriesRegistrar` as a configuration via the `@Import` annotation. +. `Neo4jRepositoriesRegistrar` connects `@EnableNeo4jRepositories` with `Neo4jRepositoryConfigurationExtension`. +. `Neo4jRepositoryConfigurationExtension` creates `Neo4jRepositoryFactoryBean` (the class defined (I)). +** Adds manually created `Neo4jTemplate` (as an implementation of `Neo4jOperations`) bean by setting it (`setNeo4jOperations`) in the `Neo4jRepositoryFactoryBean`. (II) +** Defines the default/fallback `RepositoryFactoryBeanClassName` as `Neo4jRepositoryFactoryBean.class.getName()` in `getRepositoryFactoryBeanClassName`. +. `Neo4jRepositoryFactoryBean` has a super constructor that gets called from the infrastructure code. +As a consequence the `neo4jOperations` property has to get set in (II) after initialization. +** Creates a new instance of `Neo4jRepositoryFactory` with the in (II) provided `Neo4jOperations` in `doCreateRepositoryFactory`. +. `Neo4jRepositoryFactory` will then create a `SimpleNeo4jRepository`. +** It does this by calling `getTargetRepositoryViaReflection` in `getTargetRepository` and providing the `neo4jOperations`. +. `SimpleNeo4jRepository` (the repository behind every user defined repository) is initialized. From 16d2c5cfe35644493eb93b9db0ca954da8d28907 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 27 Feb 2019 14:36:02 +0100 Subject: [PATCH 016/342] Introduce testcontainers for integration testing. --- pom.xml | 23 ++++++++++++++++++++--- spring-data-neo4j/pom.xml | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index a374e8382..786974347 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,8 @@ UTF-8 + ${java.version} + ${java.version} 3.11.1 8.16 1.8 @@ -95,15 +97,15 @@ 3.7.1 3.0.1 3.0.0-M2 - 2.2.0.BUILD-SNAPSHOT 3.5.2 1.7.2 1.2.1 Californium-SR4 1.3.8 2.2.5 - ${java.version} - ${java.version} + 1.7.25 + 2.2.0.BUILD-SNAPSHOT + 1.10.6 @@ -133,6 +135,16 @@ junit-jupiter-engine ${junit-jupiter.version} + + org.testcontainers + junit-jupiter + ${testcontainers.version} + + + org.testcontainers + neo4j + ${testcontainers.version} + org.neo4j neo4j @@ -153,6 +165,11 @@ spring-data-commons ${springdata-commons.version} + + org.slf4j + slf4j-api + ${slf4j.version} + io.projectreactor reactor-bom diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 2715f5809..737b3630f 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -92,6 +92,21 @@ junit-jupiter-engine test + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + neo4j + test + + + org.slf4j + slf4j-api + test + From c9989409791727a164c37376523dbce33d4eace9 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 27 Feb 2019 14:43:22 +0100 Subject: [PATCH 017/342] Enable basic custom cypher query support. --- .../data/neo4j/core/Neo4jOperations.java | 1 + .../data/neo4j/core/Neo4jTemplate.java | 27 ++++++++ .../repository/query/AbstractNeo4jQuery.java | 29 ++++++++ .../repository/query/Neo4jQueryMethod.java | 66 +++++++++++++++++++ .../data/neo4j/repository/query/Query.java | 5 ++ .../query/StringBasedNeo4jQuery.java | 49 ++++++++++++++ .../support/Neo4jRepositoryFactory.java | 47 +++++++++++++ .../neo4j/integration/PersonRepository.java | 6 ++ .../integration/RepositoryCreationTest.java | 37 ++++++++--- 9 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java index 625c08245..e4b88cb67 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java @@ -24,4 +24,5 @@ package org.springframework.data.neo4j.core; * @author Gerrit Meier */ public interface Neo4jOperations { + Object executeQuery(String query); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 43c323005..779fe7a16 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -18,6 +18,11 @@ */ package org.springframework.data.neo4j.core; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.Transaction; + /** * Default implementation of {@link Neo4jOperations}. Uses the Neo4j Java driver to connect to and interact with the * database. @@ -26,4 +31,26 @@ package org.springframework.data.neo4j.core; */ public class Neo4jTemplate implements Neo4jOperations { + private final Driver driver; + + public Neo4jTemplate(Driver driver) { + this.driver = driver; + } + + @Override + public Object executeQuery(String query) { + Session session = driver.session(); + Transaction transaction = session.beginTransaction(); + try { + StatementResult result = transaction.run(query); + transaction.success(); + return result.list(); + } catch (Exception e) { + transaction.failure(); + } finally { + transaction.close(); + } + + return null; + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java new file mode 100644 index 000000000..e43326efc --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.query; + +import org.springframework.data.repository.query.RepositoryQuery; + +/** + * Base class for {@link RepositoryQuery} implementations for Neo4j. + * + * @author Gerrit Meier + **/ +abstract class AbstractNeo4jQuery implements RepositoryQuery { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java new file mode 100644 index 000000000..f42c7bc76 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.query; + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.Nullable; + +/** + * Neo4j specific implementation of {@link QueryMethod}. + * + * @author Gerrit Meier + **/ +public class Neo4jQueryMethod extends QueryMethod { + + private final Method method; + + private Neo4jQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + super(method, metadata, factory); + + this.method = method; + } + + /** + * Creates a new {@link Neo4jQueryMethod} from the given parameters. Looks up the correct query to use for following + * invocations of the method given. + * + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param factory must not be {@literal null}. + */ + public static Neo4jQueryMethod of(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + return new Neo4jQueryMethod(method, metadata, factory); + } + + @Nullable + String getAnnotatedQuery() { + return getQueryAnnotation().map(Query::value).orElse(null); + } + + private Optional getQueryAnnotation() { + return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)); + } + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java index afd287515..fa0d8eee5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java @@ -39,6 +39,11 @@ import org.springframework.data.annotation.QueryAnnotation; @Documented public @interface Query { + /** + * The custom Cypher query to get executed and mapped back, if any return type is defined. + */ + String value() default ""; + /** * @return whether the query defined should be executed as count projection. */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java new file mode 100644 index 000000000..2a781b5a9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.query; + +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.repository.query.QueryMethod; + +/** + * Represents a String based custom Cypher query. + * + * @author Gerrit Meier + */ +public class StringBasedNeo4jQuery extends AbstractNeo4jQuery { + + private final Neo4jQueryMethod queryMethod; + private final Neo4jOperations neo4jOperations; + + public StringBasedNeo4jQuery(Neo4jQueryMethod queryMethod, Neo4jOperations neo4jOperations) { + + this.queryMethod = queryMethod; + this.neo4jOperations = neo4jOperations; + } + + @Override + public Object execute(Object[] parameters) { + return neo4jOperations.executeQuery(queryMethod.getAnnotatedQuery()); + } + + @Override + public QueryMethod getQueryMethod() { + return null; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 0e63e0210..76c07985a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -18,12 +18,23 @@ */ package org.springframework.data.neo4j.repository.support; +import java.lang.reflect.Method; +import java.util.Optional; + import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod; +import org.springframework.data.neo4j.repository.query.StringBasedNeo4jQuery; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; /** * Factory to create {@link Neo4jRepository} instances. @@ -52,4 +63,40 @@ public class Neo4jRepositoryFactory extends RepositoryFactorySupport { protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleNeo4jRepository.class; } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) + */ + @Override + protected Optional getQueryLookupStrategy(Key key, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + + return Optional.of(new Neo4jQueryLookupStrategy(neo4jOperations, evaluationContextProvider)); + } + + private class Neo4jQueryLookupStrategy implements QueryLookupStrategy { + + private final Neo4jOperations neo4jOperations; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + private Neo4jQueryLookupStrategy(Neo4jOperations neo4jOperations, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + + this.neo4jOperations = neo4jOperations; + this.evaluationContextProvider = evaluationContextProvider; + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) + */ + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + NamedQueries namedQueries) { + + Neo4jQueryMethod queryMethod = Neo4jQueryMethod.of(method, metadata, factory); + + return new StringBasedNeo4jQuery(queryMethod, neo4jOperations); + } + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 814ca5c4b..54285ed10 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -18,8 +18,14 @@ */ package org.springframework.data.neo4j.integration; +import java.util.List; + +import org.neo4j.driver.v1.Record; import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; public interface PersonRepository extends Neo4jRepository { + @Query("MATCH (n) return n") + List customQuery(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java index 4e1efc12f..e37e9a2f4 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java @@ -20,8 +20,14 @@ package org.springframework.data.neo4j.integration; import static org.assertj.core.api.Assertions.*; -import org.junit.Test; -import org.junit.runner.RunWith; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.neo4j.driver.v1.AuthTokens; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.GraphDatabase; +import org.neo4j.driver.v1.Record; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,31 +35,46 @@ import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; @ContextConfiguration -@RunWith(SpringRunner.class) -public class RepositoryCreationTest { +@ExtendWith(SpringExtension.class) +@Testcontainers +class RepositoryCreationTest { + + @Container private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null); @Autowired private PersonRepository repository; @Test - public void repositoryGetsCreated() { + void repositoryGetsCreated() { assertThat(repository).isNotNull(); } @Test - public void repositoryCallFailsBecauseOfUnsupportedOperationException() { + void repositoryCallFailsBecauseOfUnsupportedOperationException() { assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll()); } + @Test + void callCustomCypher() { + List actual = repository.customQuery(); + assertThat(actual).isNotNull(); + } + @Configuration @EnableNeo4jRepositories static class Config { @Bean public Neo4jOperations neo4jTemplate() { - return new Neo4jTemplate(); + String boltUrl = neo4jContainer.getBoltUrl(); + Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.none()); + + return new Neo4jTemplate(driver); } } From 9c173d66f82a7f79fde17d2aa4761130083c2548 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 27 Feb 2019 15:14:06 +0100 Subject: [PATCH 018/342] Set log level in test to warn. --- .../src/test/resources/logback-test.xml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 spring-data-neo4j/src/test/resources/logback-test.xml diff --git a/spring-data-neo4j/src/test/resources/logback-test.xml b/spring-data-neo4j/src/test/resources/logback-test.xml new file mode 100644 index 000000000..f53376a05 --- /dev/null +++ b/spring-data-neo4j/src/test/resources/logback-test.xml @@ -0,0 +1,35 @@ + + + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + From b7db1342d31cbb07508ae13437518792152a716b Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 28 Feb 2019 11:41:26 +0100 Subject: [PATCH 019/342] Briefly explain query execution for custom queries without params. --- README.adoc | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index e2fa378c4..b0a7ec3ed 100644 --- a/README.adoc +++ b/README.adoc @@ -143,7 +143,7 @@ Run the following two commands and point your browser to http://localhost:7474. -== `SimpleNeo4jRepository` initialization +=== `SimpleNeo4jRepository` initialization . `@EnableNeo4jRepositories` defines ** the `repositoryFactoryBeanClass` that defaults to `Neo4jRepositoryFactoryBean.class`. (I) ** `Neo4jRepositoriesRegistrar` as a configuration via the `@Import` annotation. @@ -157,3 +157,23 @@ As a consequence the `neo4jOperations` property has to get set in (II) after ini . `Neo4jRepositoryFactory` will then create a `SimpleNeo4jRepository`. ** It does this by calling `getTargetRepositoryViaReflection` in `getTargetRepository` and providing the `neo4jOperations`. . `SimpleNeo4jRepository` (the repository behind every user defined repository) is initialized. + +=== Query execution + +NOTE: This section contains the already straight-forward implemented support for custom queries via `@Query`. +The other execution paths are only drafts right now and marked with a `*`. + +`Neo4jRepositoryFactory` overrides the `getQueryLookupStrategy` method to provide the `Neo4jQueryLookupStrategy`. +From our previous experience and handling in other Spring Data stores this would branch off in two (technical three) directions: + +. `StringBasedNeo4jQuery` for custom Cypher queries that are provided with the `@Query` annotation. +. `*` `StringBasedNeo4jQuery` for named queries that are outsourced in property files. +. `*` `PartTreeNeo4jQuery` for derived finder methods. + +All three of them will get a custom `Neo4jQueryMethod` besides `Neo4jOperations` and `QueryMethodEvaluationContextProvider` (not used yet) provided. +This is a wrapper around the `java.lang.reflect.Method` passed into the `resolveQuery` method of the `Neo4jQueryLookupStrategy` to provide additional metadata. + +==== `StringBasedNeo4jQuery` execution + +At the moment the implementation just takes the value of the provided `@Query` annotation by calling `getAnnotatedQuery` on the `Neo4jQueryMethod` +and executes it through the `neo4jOperations` (`Neo4jTemplate`) class. From d42eb0fb20c27f5c1debc70fdd1a094a2183d92b Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 28 Feb 2019 11:41:47 +0100 Subject: [PATCH 020/342] Change test for custom query to also evaluate the result. --- .../data/neo4j/integration/PersonRepository.java | 2 +- .../data/neo4j/integration/RepositoryCreationTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 54285ed10..1296954dc 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -26,6 +26,6 @@ import org.springframework.data.neo4j.repository.query.Query; public interface PersonRepository extends Neo4jRepository { - @Query("MATCH (n) return n") + @Query("RETURN 1") List customQuery(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java index e37e9a2f4..8da0db01a 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java @@ -63,6 +63,7 @@ class RepositoryCreationTest { void callCustomCypher() { List actual = repository.customQuery(); assertThat(actual).isNotNull(); + assertThat(actual.get(0).get("1").asInt()).isEqualTo(1); } @Configuration From 70f69874c2a0737b2fecc7d374108c5d68db35e2 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 28 Feb 2019 16:32:21 +0100 Subject: [PATCH 021/342] Rename integration test. After a few failed tries to get a cleaner test structure and avoid starting the container multiple times, just rename the test to point out that this is *the* integration test right now. --- ...ositoryCreationTest.java => RepositoryIntegrationTest.java} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{RepositoryCreationTest.java => RepositoryIntegrationTest.java} (98%) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java index 8da0db01a..0024c58ba 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryCreationTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java @@ -43,7 +43,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; @ContextConfiguration @ExtendWith(SpringExtension.class) @Testcontainers -class RepositoryCreationTest { +class RepositoryIntegrationTest { @Container private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null); @@ -79,4 +79,5 @@ class RepositoryCreationTest { } } + } From 5640ed1ed922835e6ffc139efa39e90bd86ae450 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 28 Feb 2019 17:09:49 +0100 Subject: [PATCH 022/342] Add support for derived finder methods. This is just a stub that will throw an exception on execution but makes sure the infrastructure code will lead towards the right method calls. --- .../repository/query/Neo4jQueryMethod.java | 9 ++++ .../repository/query/PartTreeNeo4jQuery.java | 44 +++++++++++++++++++ .../query/StringBasedNeo4jQuery.java | 3 +- .../support/Neo4jRepositoryFactory.java | 6 ++- .../neo4j/integration/PersonRepository.java | 2 + .../RepositoryIntegrationTest.java | 5 +++ 6 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java index f42c7bc76..ffa641a4b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java @@ -54,6 +54,15 @@ public class Neo4jQueryMethod extends QueryMethod { return new Neo4jQueryMethod(method, metadata, factory); } + /** + * Determines if this is a custom Cypher query method. + * + * @return true, if method has {@link Query} annotation. Otherwise false. + */ + public boolean hasAnnotatedQuery() { + return getQueryAnnotation().isPresent(); + } + @Nullable String getAnnotatedQuery() { return getQueryAnnotation().map(Query::value).orElse(null); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java new file mode 100644 index 000000000..3fdc80d15 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.query; + +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; + +/** + * Implementation of {@link RepositoryQuery} for derived finder methods. + * + * @author Gerrit Meier + **/ +public class PartTreeNeo4jQuery extends AbstractNeo4jQuery { + + public PartTreeNeo4jQuery(Neo4jQueryMethod queryMethod, Neo4jOperations neo4jOperations) { + } + + @Override + public Object execute(Object[] parameters) { + throw new UnsupportedOperationException("Not there yet."); + } + + @Override + public QueryMethod getQueryMethod() { + return null; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index 2a781b5a9..2e6222516 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -20,9 +20,10 @@ package org.springframework.data.neo4j.repository.query; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; /** - * Represents a String based custom Cypher query. + * Implementation of {@link RepositoryQuery} for String based custom Cypher query. * * @author Gerrit Meier */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 76c07985a..0d9fb836a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -24,6 +24,7 @@ import java.util.Optional; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod; +import org.springframework.data.neo4j.repository.query.PartTreeNeo4jQuery; import org.springframework.data.neo4j.repository.query.StringBasedNeo4jQuery; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.EntityInformation; @@ -95,8 +96,11 @@ public class Neo4jRepositoryFactory extends RepositoryFactorySupport { NamedQueries namedQueries) { Neo4jQueryMethod queryMethod = Neo4jQueryMethod.of(method, metadata, factory); + if (queryMethod.hasAnnotatedQuery()) { + return new StringBasedNeo4jQuery(queryMethod, neo4jOperations); + } - return new StringBasedNeo4jQuery(queryMethod, neo4jOperations); + return new PartTreeNeo4jQuery(queryMethod, neo4jOperations); } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 1296954dc..cef268283 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -28,4 +28,6 @@ public interface PersonRepository extends Neo4jRepository { @Query("RETURN 1") List customQuery(); + + List findByName(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java index 0024c58ba..a7ce67770 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java @@ -66,6 +66,11 @@ class RepositoryIntegrationTest { assertThat(actual.get(0).get("1").asInt()).isEqualTo(1); } + @Test + void findBySimpleProperty() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findByName()); + } + @Configuration @EnableNeo4jRepositories static class Config { From eec7fade6e759c10057f7c06b9818b97769cbe54 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 1 Mar 2019 12:02:52 +0100 Subject: [PATCH 023/342] Change visibility of previous public classes. --- .../data/neo4j/repository/support/Neo4jRepositoryFactory.java | 4 ++-- .../data/neo4j/repository/support/SimpleNeo4jRepository.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 0d9fb836a..9dc0af6b9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -42,11 +42,11 @@ import org.springframework.data.repository.query.RepositoryQuery; * * @author Gerrit Meier */ -public class Neo4jRepositoryFactory extends RepositoryFactorySupport { +class Neo4jRepositoryFactory extends RepositoryFactorySupport { private final Neo4jOperations neo4jOperations; - public Neo4jRepositoryFactory(Neo4jOperations neo4jOperations) { + Neo4jRepositoryFactory(Neo4jOperations neo4jOperations) { this.neo4jOperations = neo4jOperations; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 18c79b759..5db19abb3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -32,11 +32,11 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; * * @author Gerrit Meier */ -public class SimpleNeo4jRepository implements Neo4jRepository { +class SimpleNeo4jRepository implements Neo4jRepository { private final Neo4jOperations neo4jOperations; - public SimpleNeo4jRepository(Neo4jOperations neo4jOperations) { + SimpleNeo4jRepository(Neo4jOperations neo4jOperations) { this.neo4jOperations = neo4jOperations; } From b788818ec7d44466f46d24e2302c2867761655a3 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 1 Mar 2019 12:55:59 +0100 Subject: [PATCH 024/342] Introduce API Guardian. --- pom.xml | 6 ++++++ spring-data-neo4j/pom.xml | 4 ++++ .../springframework/data/neo4j/core/Neo4jOperations.java | 4 ++++ .../org/springframework/data/neo4j/core/Neo4jTemplate.java | 2 ++ 4 files changed, 16 insertions(+) diff --git a/pom.xml b/pom.xml index 786974347..337d6d37d 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,7 @@ UTF-8 ${java.version} ${java.version} + 1.0.0 3.11.1 8.16 1.8 @@ -170,6 +171,11 @@ slf4j-api ${slf4j.version} + + org.apiguardian + apiguardian-api + ${apiguardian.version} + io.projectreactor reactor-bom diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 737b3630f..08c44f524 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -76,6 +76,10 @@ org.springframework spring-tx + + org.apiguardian + apiguardian-api + io.projectreactor reactor-test diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java index e4b88cb67..c37d1bcc4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java @@ -18,11 +18,15 @@ */ package org.springframework.data.neo4j.core; +import org.apiguardian.api.API; + /** * Definition of Neo4j specific operations. Implemented by {@link Neo4jTemplate}. * * @author Gerrit Meier */ public interface Neo4jOperations { + + @API(status = API.Status.EXPERIMENTAL) Object executeQuery(String query); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 779fe7a16..3f1d42c50 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core; +import org.apiguardian.api.API; import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; @@ -33,6 +34,7 @@ public class Neo4jTemplate implements Neo4jOperations { private final Driver driver; + @API(status = API.Status.STABLE, since = "1.0") public Neo4jTemplate(Driver driver) { this.driver = driver; } From 649c8fff2b3a0c9592a2b4a8e572e27e31a05cb8 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 1 Mar 2019 17:03:30 +0100 Subject: [PATCH 025/342] More testing. --- .../query/Neo4jQueryMethodTest.java | 68 +++++++++ .../support/SimpleNeo4jRepositoryTest.java | 136 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java new file mode 100644 index 000000000..1e62a1b24 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * @author Gerrit Meier + **/ +class Neo4jQueryMethodTest { + + private static final String CUSTOM_CYPHER_QUERY = "MATCH (n) return n"; + + @Test + void shouldFindAnnotatedQuery() throws Exception { + Neo4jQueryMethod queryMethod = queryMethod("annotatedQuery"); + assertThat(queryMethod.getAnnotatedQuery()).isEqualTo(CUSTOM_CYPHER_QUERY); + } + + @Test + void findQueryAnnotation() throws Exception { + Neo4jQueryMethod queryMethod = queryMethod("annotatedQuery"); + assertThat(queryMethod.hasAnnotatedQuery()).isTrue(); + } + + private Neo4jQueryMethod queryMethod(String name, Class... parameters) throws Exception { + Class repositoryClass = PersonRepository.class; + + Method method = repositoryClass.getMethod(name, parameters); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + return Neo4jQueryMethod.of(method, new DefaultRepositoryMetadata(repositoryClass), factory); + } + + interface PersonRepository extends Repository { + + @Query(CUSTOM_CYPHER_QUERY) + List annotatedQuery(); + } + + class Person { + } + +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java new file mode 100644 index 000000000..c8113e9a7 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.support; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +/** + * @author Gerrit Meier + **/ +class SimpleNeo4jRepositoryTest { + + private SimpleNeo4jRepository repository = new SimpleNeo4jRepository(null); + + @Test + void saveNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.save(null)); + } + + @Test + void saveAllNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.saveAll(null)); + } + + @Test + void findByIdNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findById(null)); + } + + @Test + void existsByIdNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.existsById(null)); + } + + @Test + void findAllByIdNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAllById(null)); + } + + @Test + void deleteByIdNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteById(null)); + } + + @Test + void deleteNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.delete(null)); + } + + @Test + void deleteAllNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteAll()); + } + + @Test + void deleteAll1NotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteAll(null)); + } + + @Test + void findOneNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findOne(null)); + } + + @Test + void findAllNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll()); + } + + @Test + void findAllExampleNotImplemented() { + Example example = Example.of(""); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll(example)); + } + + @Test + void findAllPageableNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> repository.findAll(Pageable.unpaged())); + } + + @Test + void findAllSortByNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> repository.findAll(Sort.by("property"))); + } + + @Test + void findAllExampleAndSortNotImplemented() { + Example example = Example.of(""); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> repository.findAll(example, Sort.by("property"))); + } + + @Test + void findAllExampleAndPageableNotImplemented() { + Example example = Example.of(""); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> repository.findAll(example, Pageable.unpaged())); + } + + @Test + void countNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.count()); + } + + @Test + void countWithExampleNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.count(null)); + } + + @Test + void existsNotImplemented() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll()); + } +} From 93bddc1d5324943e0a683f2f9bbf99896c24c588 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 1 Mar 2019 17:03:41 +0100 Subject: [PATCH 026/342] Close session after query execution. --- .../java/org/springframework/data/neo4j/core/Neo4jTemplate.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 3f1d42c50..5ee20ab38 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -51,6 +51,7 @@ public class Neo4jTemplate implements Neo4jOperations { transaction.failure(); } finally { transaction.close(); + session.close(); } return null; From c9185f262e62bd965f96fbfd4b41e329dc899a57 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 5 Mar 2019 11:56:55 +0100 Subject: [PATCH 027/342] Update to JUnit 5.4.0 and AssertJ 3.12.0. This is needed to keep up with Spring Boot 2.1.3 for the starter. This also adds JUnit Jupiter BOM as imported dependencies, forcing it downwards onto all child projects and effectivly overwriting Boots settings in the starter. --- pom.xml | 10 ++++++---- spring-data-neo4j/pom.xml | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 337d6d37d..fc29fa07e 100644 --- a/pom.xml +++ b/pom.xml @@ -82,13 +82,13 @@ ${java.version} ${java.version} 1.0.0 - 3.11.1 + 3.12.0 8.16 1.8 0.8.2 1.6.0 1.6.0 - 5.3.2 + 5.4.0 3.0.0 3.0.0-M1 3.0.0-M2 @@ -132,9 +132,11 @@ ${assertj.version} - org.junit.jupiter - junit-jupiter-engine + org.junit + junit-bom ${junit-jupiter.version} + import + pom org.testcontainers diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 08c44f524..48ff2cbba 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -93,7 +93,7 @@ org.junit.jupiter - junit-jupiter-engine + junit-jupiter test From 321946ac5b91fa9a02efc0a6d4fc3bd544a40cbb Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 4 Mar 2019 11:58:37 +0100 Subject: [PATCH 028/342] Rework integration test. --- .../integration/RepositoryIntegrationTest.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java index a7ce67770..ef8cc16f4 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java @@ -40,18 +40,17 @@ import org.testcontainers.containers.Neo4jContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -@ContextConfiguration @ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = RepositoryIntegrationTest.Config.class) @Testcontainers class RepositoryIntegrationTest { @Container private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null); - @Autowired private PersonRepository repository; + private final PersonRepository repository; - @Test - void repositoryGetsCreated() { - assertThat(repository).isNotNull(); + RepositoryIntegrationTest(@Autowired PersonRepository repository) { + this.repository = repository; } @Test @@ -62,8 +61,11 @@ class RepositoryIntegrationTest { @Test void callCustomCypher() { List actual = repository.customQuery(); - assertThat(actual).isNotNull(); - assertThat(actual.get(0).get("1").asInt()).isEqualTo(1); + assertThat(actual) + .isNotNull() + .isNotEmpty() + .first().extracting(record -> record.get("1").asInt()) + .isEqualTo(1); } @Test From c995b24a5aac8be2ba631cf2fb14b4adf1387e9c Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 8 Mar 2019 13:23:51 +0100 Subject: [PATCH 029/342] Remove internal API builder method. We came to the conclusion that we do not want to have builder style access for our internal APIs but use explicit constructors. This commit removes builder API and introduces public constructors. --- .../data/neo4j/repository/query/Neo4jQueryMethod.java | 11 +++-------- .../repository/support/Neo4jRepositoryFactory.java | 2 +- .../neo4j/repository/query/Neo4jQueryMethodTest.java | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java index ffa641a4b..4a19d6f29 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java @@ -36,12 +36,6 @@ public class Neo4jQueryMethod extends QueryMethod { private final Method method; - private Neo4jQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { - super(method, metadata, factory); - - this.method = method; - } - /** * Creates a new {@link Neo4jQueryMethod} from the given parameters. Looks up the correct query to use for following * invocations of the method given. @@ -50,8 +44,9 @@ public class Neo4jQueryMethod extends QueryMethod { * @param metadata must not be {@literal null}. * @param factory must not be {@literal null}. */ - public static Neo4jQueryMethod of(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { - return new Neo4jQueryMethod(method, metadata, factory); + public Neo4jQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + super(method, metadata, factory); + this.method = method; } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 9dc0af6b9..c2f8943fd 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -95,7 +95,7 @@ class Neo4jRepositoryFactory extends RepositoryFactorySupport { public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { - Neo4jQueryMethod queryMethod = Neo4jQueryMethod.of(method, metadata, factory); + Neo4jQueryMethod queryMethod = new Neo4jQueryMethod(method, metadata, factory); if (queryMethod.hasAnnotatedQuery()) { return new StringBasedNeo4jQuery(queryMethod, neo4jOperations); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java index 1e62a1b24..4d592d11f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java @@ -53,7 +53,7 @@ class Neo4jQueryMethodTest { Method method = repositoryClass.getMethod(name, parameters); ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); - return Neo4jQueryMethod.of(method, new DefaultRepositoryMetadata(repositoryClass), factory); + return new Neo4jQueryMethod(method, new DefaultRepositoryMetadata(repositoryClass), factory); } interface PersonRepository extends Repository { From f6a07a84fb821423202385e20b8069fe29b07e3b Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 8 Mar 2019 13:48:53 +0100 Subject: [PATCH 030/342] Split unit and integration tests. --- README.adoc | 7 +++++++ pom.xml | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/README.adoc b/README.adoc index b0a7ec3ed..bd1366719 100644 --- a/README.adoc +++ b/README.adoc @@ -39,6 +39,13 @@ NOTE: However, we should discuss if you could try a JDK 11 only project. Supporting the embedded use case will be solved on the drivers level. +=== Integration tests + +Integration tests take more time by their very nature. +To get fast feedback we have split up the tests in unit and integration tests. +Unit tests will run when the `test` goal is issued and should have a name ending with `Test` or `Tests`. +Integration tests will get executed withing the `verify` goal and their class name have to end with `IntegrationTest` to get picked up. + == Configuration Spring Data Neo4j RX takes a "ready to use" drivers instance and uses that. diff --git a/pom.xml b/pom.xml index fc29fa07e..374f036e7 100644 --- a/pom.xml +++ b/pom.xml @@ -310,6 +310,27 @@ **/*Test.java **/*Tests.java + + **/*IntegrationTest.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M3 + + + + integration-test + verify + + + + + + **/*IntegrationTest.java + From 04dfff3264f56e29ff68421f2eec20d86612ffe3 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 8 Mar 2019 14:00:46 +0100 Subject: [PATCH 031/342] Introduce additional parameter in pom to skip unit tests. --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index 374f036e7..98cf7584c 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,7 @@ UTF-8 + false ${java.version} ${java.version} 1.0.0 @@ -313,6 +314,7 @@ **/*IntegrationTest.java + ${skipUnitTests} From aeffb1e941275a0c4be26933057a759e8b6f613e Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 5 Mar 2019 15:08:43 +0100 Subject: [PATCH 032/342] Provide autoconfiguration for an instance of Driver. This commit adds Spring Boot autoconfiguration for an instance of Neo4j Java-Driver. It exposes all non-deprecated properties as Spring Boot configuration properties along with an excerpt of their docs. The configuration supports both uri and uris, but highly recommends using only one single URI. In the process of making the examples more accessible, a separate project hast been added for demoing Spring Boot integration and the existing example project has been moved into a common parent. --- README.adoc | 13 ++++++++++++- pom.xml | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index bd1366719..417364742 100644 --- a/README.adoc +++ b/README.adoc @@ -51,7 +51,8 @@ Integration tests will get executed withing the `verify` goal and their class na Spring Data Neo4j RX takes a "ready to use" drivers instance and uses that. We won't provide any additional configuration for aspects that are configurable through the driver. We will however provide support to configure the drivers instance in Spring Boot. -The current Spring Boot Starter only configures the Neo4j-OGM transport and not the "real" driver. +The current SDN Spring Boot Starter only configures the Neo4j-OGM transport and not the "real" driver. +Our plans for a future starter a have been <>. Closing the driver is not the the concern of Spring Data Neo4j RX. The lifecycle of that bean should be managed by the application. @@ -184,3 +185,13 @@ This is a wrapper around the `java.lang.reflect.Method` passed into the `resolve At the moment the implementation just takes the value of the provided `@Query` annotation by calling `getAnnotatedQuery` on the `Neo4jQueryMethod` and executes it through the `neo4jOperations` (`Neo4jTemplate`) class. + +[[starter]] +== Spring Boot Starter + +The Spring Data Neo4j RX Spring Boot Starter provides automatic configuration to + +* Create an instance of the https://github.com/neo4j/neo4j-java-driver[neo4j-java-driver] +* Configure Spring Data Neo4j RX itself inside a Spring Boot application and enabling Spring Data repositories + +Read me about it link:spring-data-neo4j-rx-spring-boot-starter-parent/README.adoc[here]. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 98cf7584c..d55e73a22 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ spring-data-neo4j-rx spring-data-neo4j-rx-spring-boot-starter-parent - spring-data-neo4j-rx-examples + spring-data-neo4j-rx-examples-parent @@ -136,8 +136,8 @@ org.junit junit-bom ${junit-jupiter.version} - import pom + import org.testcontainers From e0a76fe780f3f0c63d3b1dc2bab4e91a08226d93 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 8 Mar 2019 16:38:02 +0100 Subject: [PATCH 033/342] Add sortpom-maven-plugin. --- pom.xml | 83 ++++++++++++++++++++++++--------------- spring-data-neo4j/pom.xml | 54 ++++++++++++------------- 2 files changed, 79 insertions(+), 58 deletions(-) diff --git a/pom.xml b/pom.xml index d55e73a22..59941c82b 100644 --- a/pom.xml +++ b/pom.xml @@ -78,33 +78,33 @@ - UTF-8 - false - ${java.version} - ${java.version} 1.0.0 3.12.0 8.16 - 1.8 0.8.2 - 1.6.0 + 1.8 1.6.0 + 1.6.0 5.4.0 3.0.0 3.0.0-M1 - 3.0.0-M2 3.0.0-M1 + 3.0.0-M2 3.0.0-M1 3.0.1 3.7.1 3.0.1 3.0.0-M2 - 3.5.2 + ${java.version} + ${java.version} 1.7.2 + 3.5.2 + UTF-8 1.2.1 Californium-SR4 1.3.8 2.2.5 + false 1.7.25 2.2.0.BUILD-SNAPSHOT 1.10.6 @@ -112,6 +112,13 @@ + + io.projectreactor + reactor-bom + ${reactor-bom.version} + pom + import + io.reactivex rxjava @@ -127,6 +134,11 @@ rxjava ${rxjava2.version} + + org.apiguardian + apiguardian-api + ${apiguardian.version} + org.assertj assertj-core @@ -139,16 +151,6 @@ pom import - - org.testcontainers - junit-jupiter - ${testcontainers.version} - - - org.testcontainers - neo4j - ${testcontainers.version} - org.neo4j neo4j @@ -164,27 +166,25 @@ neo4j-harness ${neo4j.version} - - org.springframework.data - spring-data-commons - ${springdata-commons.version} - org.slf4j slf4j-api ${slf4j.version} - org.apiguardian - apiguardian-api - ${apiguardian.version} + org.springframework.data + spring-data-commons + ${springdata-commons.version} - io.projectreactor - reactor-bom - ${reactor-bom.version} - pom - import + org.testcontainers + junit-jupiter + ${testcontainers.version} + + + org.testcontainers + neo4j + ${testcontainers.version} @@ -205,6 +205,27 @@ + + com.github.ekryd.sortpom + sortpom-maven-plugin + 2.8.0 + + + verify + + sort + + + + + ${project.build.sourceEncoding} + true + -1 + true + groupId,artifactId + false + + maven-checkstyle-plugin ${maven-checkstyle-plugin.version} diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 48ff2cbba..6464c1960 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -37,6 +37,12 @@ reactor-core true + + io.projectreactor + reactor-test + test + true + io.reactivex rxjava @@ -52,10 +58,29 @@ rxjava true + + org.apiguardian + apiguardian-api + + + org.assertj + assertj-core + test + + + org.junit.jupiter + junit-jupiter + test + org.neo4j.driver neo4j-java-driver + + org.slf4j + slf4j-api + test + org.springframework spring-beans @@ -68,33 +93,13 @@ org.springframework spring-core - - org.springframework.data - spring-data-commons - org.springframework spring-tx - org.apiguardian - apiguardian-api - - - io.projectreactor - reactor-test - test - true - - - org.assertj - assertj-core - test - - - org.junit.jupiter - junit-jupiter - test + org.springframework.data + spring-data-commons org.testcontainers @@ -106,11 +111,6 @@ neo4j test - - org.slf4j - slf4j-api - test - From bb48337187183a003afae4911e6523b395ebe07a Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 8 Mar 2019 17:02:27 +0100 Subject: [PATCH 034/342] Rename integration tests to end with IT. --- pom.xml | 8 -------- .../{RepositoryIntegrationTest.java => RepositoryIT.java} | 6 +++--- 2 files changed, 3 insertions(+), 11 deletions(-) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{RepositoryIntegrationTest.java => RepositoryIT.java} (94%) diff --git a/pom.xml b/pom.xml index 59941c82b..bcba9e1f8 100644 --- a/pom.xml +++ b/pom.xml @@ -332,9 +332,6 @@ **/*Test.java **/*Tests.java - - **/*IntegrationTest.java - ${skipUnitTests} @@ -350,11 +347,6 @@ - - - **/*IntegrationTest.java - - diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java similarity index 94% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index ef8cc16f4..c51f7736c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIntegrationTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -41,15 +41,15 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = RepositoryIntegrationTest.Config.class) +@ContextConfiguration(classes = RepositoryIT.Config.class) @Testcontainers -class RepositoryIntegrationTest { +class RepositoryIT { @Container private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null); private final PersonRepository repository; - RepositoryIntegrationTest(@Autowired PersonRepository repository) { + RepositoryIT(@Autowired PersonRepository repository) { this.repository = repository; } From 0197ef4cb64732e34086b4e559d2cc08158d4ab9 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 11 Mar 2019 18:45:31 +0100 Subject: [PATCH 035/342] Implement Neo4jTransactionManager. --- lombok.config | 2 +- pom.xml | 13 +- spring-data-neo4j/pom.xml | 22 +- .../data/neo4j/core/Neo4jTemplate.java | 29 +-- .../session/DefaultNeo4jSessionFactory.java | 36 --- .../session/Neo4jSessionFactorySupport.java | 30 --- .../DefaultNeo4jStatementRunnerSupplier.java | 64 +++++ .../core/transaction/Neo4jResourceHolder.java | 102 ++++++++ .../Neo4jSessionSynchronization.java | 88 +++++++ .../transaction/Neo4jTransactionManager.java | 227 ++++++++++++++++++ .../StatementRunnerSupplier.java} | 13 +- .../package-info.java | 2 +- .../config/EnableNeo4jRepositories.java | 11 +- ...Neo4jRepositoryConfigurationExtension.java | 22 +- .../support/SimpleNeo4jRepository.java | 17 +- ...faultNeo4jStatementRunnerSupplierTest.java | 221 +++++++++++++++++ .../Neo4jTransactionManagerTest.java | 82 +++++++ .../data/neo4j/integration/RepositoryIT.java | 26 +- .../support/SimpleNeo4jRepositoryTest.java | 5 +- 19 files changed, 902 insertions(+), 110 deletions(-) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/{session/Neo4jSessionFactory.java => transaction/StatementRunnerSupplier.java} (65%) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/{session => transaction}/package-info.java (70%) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java diff --git a/lombok.config b/lombok.config index e50c7ea43..b94b2470c 100644 --- a/lombok.config +++ b/lombok.config @@ -1,2 +1,2 @@ lombok.nonNull.exceptionType = IllegalArgumentException -lombok.log.fieldName = LOG + diff --git a/pom.xml b/pom.xml index bcba9e1f8..3dc0d26ec 100644 --- a/pom.xml +++ b/pom.xml @@ -21,8 +21,8 @@ 4.0.0 - org.springframework.data.build spring-data-parent + org.springframework.data.build 2.2.0.BUILD-SNAPSHOT @@ -97,6 +97,7 @@ 3.0.0-M2 ${java.version} ${java.version} + 2.23.4 1.7.2 3.5.2 UTF-8 @@ -151,6 +152,16 @@ pom import + + org.mockito + mockito-core + ${mockito.version} + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + org.neo4j neo4j diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 6464c1960..ecff24d53 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -21,8 +21,8 @@ 4.0.0 - org.springframework.data spring-data-neo4j-rx-parent + org.springframework.data 1.0.0.BUILD-SNAPSHOT @@ -58,6 +58,12 @@ rxjava true + + javax.transaction + jta + 1.1 + test + org.apiguardian apiguardian-api @@ -72,6 +78,16 @@ junit-jupiter test + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + org.neo4j.driver neo4j-java-driver @@ -81,6 +97,10 @@ slf4j-api test + + org.slf4j + slf4j-api + org.springframework spring-beans diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 5ee20ab38..8a7ffbc0f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -18,42 +18,33 @@ */ package org.springframework.data.neo4j.core; -import org.apiguardian.api.API; import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; -import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.StatementRunner; +import org.springframework.data.neo4j.core.transaction.DefaultNeo4jStatementRunnerSupplier; +import org.springframework.data.neo4j.core.transaction.StatementRunnerSupplier; /** * Default implementation of {@link Neo4jOperations}. Uses the Neo4j Java driver to connect to and interact with the * database. * * @author Gerrit Meier + * @author Michael J. Simons */ public class Neo4jTemplate implements Neo4jOperations { - private final Driver driver; + private StatementRunnerSupplier statementRunnerSupplier; - @API(status = API.Status.STABLE, since = "1.0") public Neo4jTemplate(Driver driver) { - this.driver = driver; + this.statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(driver); } @Override public Object executeQuery(String query) { - Session session = driver.session(); - Transaction transaction = session.beginTransaction(); - try { - StatementResult result = transaction.run(query); - transaction.success(); - return result.list(); - } catch (Exception e) { - transaction.failure(); - } finally { - transaction.close(); - session.close(); - } - return null; + // TODO Let's see whether we can stick with the statementrunner or if we need to differentiate between reactive runner and default. Current 2.0 react version has two complete separate interfaces + StatementRunner statementRunner = statementRunnerSupplier.get(); + StatementResult result = statementRunner.run(query); + return result.list(); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java deleted file mode 100644 index aacd35ef7..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 org.springframework.data.neo4j.core.session; - -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.Session; - -/** - * Default implementation of a {@link Neo4jSessionFactory}. - * - * @author Michael J. Simons - */ -class DefaultNeo4jSessionFactory extends Neo4jSessionFactorySupport { - - @Override - public Session getSession() { - - throw new UnsupportedOperationException("Not there yet."); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java deleted file mode 100644 index 7c5f3d4eb..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 org.springframework.data.neo4j.core.session; - -import org.neo4j.driver.v1.Driver; - -/** - * Support infrastructure for dealing with the Neo4j Driver. - * - * @param Type of the driver. - * @author Michael J. Simons - */ -abstract class Neo4jSessionFactorySupport implements Neo4jSessionFactory { -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java new file mode 100644 index 000000000..d18445f6a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.transaction; + +import lombok.RequiredArgsConstructor; + +import java.util.Collections; + +import org.neo4j.driver.v1.AccessMode; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.StatementRunner; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * @author Michael J. Simons + */ +@RequiredArgsConstructor +public class DefaultNeo4jStatementRunnerSupplier implements StatementRunnerSupplier { + + private final Driver driver; + + @Override + public StatementRunner get() { + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + return driver.session(); + } + + // Try existing transaction + Neo4jResourceHolder resourceHolder = (Neo4jResourceHolder) TransactionSynchronizationManager + .getResource(driver); + + if (resourceHolder != null) { + + return resourceHolder.getTransaction(); + } + + // Manually create a new synchronization + resourceHolder = new Neo4jResourceHolder(driver.session(AccessMode.WRITE, Collections.emptyList())); + + TransactionSynchronizationManager.registerSynchronization( + new Neo4jSessionSynchronization(resourceHolder, driver)); + resourceHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.bindResource(driver, resourceHolder); + + return resourceHolder.getTransaction(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java new file mode 100644 index 000000000..73b99b3a1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.transaction; + +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.TransactionConfig; +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Neo4j specific {@link ResourceHolderSupport resource holder}, wrapping a {@link org.neo4j.driver.v1.Transaction}. + * {@link Neo4jTransactionManager} binds instances of this class to the thread. + *

+ * Note: Intended for internal usage only. + * + * @author Michael J. Simons + */ +public class Neo4jResourceHolder extends ResourceHolderSupport { + + private final Session session; + + private final Transaction transaction; + + public Neo4jResourceHolder(Session session) { + this(session, TransactionConfig.empty()); + } + + public Neo4jResourceHolder(Session session, TransactionConfig transactionConfig) { + this.session = session; + + this.transaction = this.session.beginTransaction(transactionConfig); + } + + Transaction getTransaction() { + return transaction; + } + + public void commit() { + + Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed."); + Assert.state(!isRollbackOnly(), "Resource msut not be marked as rollback only."); + + transaction.success(); + transaction.close(); + } + + public void rollback() { + + Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed."); + + transaction.failure(); + transaction.close(); + } + + public void close() { + + Assert.state(hasActiveSession(), "Session must be open, but has already been closed."); + + if (hasActiveTransaction()) { + transaction.close(); + } + session.close(); + } + + @Override + public void setRollbackOnly() { + super.setRollbackOnly(); + } + + @Override + public void resetRollbackOnly() { + + throw new UnsupportedOperationException(); + } + + public boolean hasActiveSession() { + + return session.isOpen(); + } + + public boolean hasActiveTransaction() { + + return transaction.isOpen(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java new file mode 100644 index 000000000..6b6022756 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.transaction; + +import org.neo4j.driver.v1.Driver; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.transaction.support.TransactionSynchronization; + +/** + * Neo4j specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when + * participating in a non-native Neoj4 transaction, such as a Jta transaction. + */ +class Neo4jSessionSynchronization + extends ResourceHolderSynchronization { + + private final Neo4jResourceHolder localResourceHolder; + + Neo4jSessionSynchronization(Neo4jResourceHolder resourceHolder, Driver driver) { + + super(resourceHolder, driver); + this.localResourceHolder = resourceHolder; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSynchronization#shouldReleaseBeforeCompletion() + */ + @Override + protected boolean shouldReleaseBeforeCompletion() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object) + */ + @Override + protected void processResourceAfterCommit(Neo4jResourceHolder resourceHolder) { + + super.processResourceAfterCommit(resourceHolder); + + if (resourceHolder.hasActiveTransaction()) { + resourceHolder.commit(); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSynchronization#afterCompletion(int) + */ + @Override + public void afterCompletion(int status) { + + if (status == TransactionSynchronization.STATUS_ROLLED_BACK && localResourceHolder.hasActiveTransaction()) { + localResourceHolder.rollback(); + } + + super.afterCompletion(status); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object) + */ + @Override + protected void releaseResource(Neo4jResourceHolder resourceHolder, Object resourceKey) { + + if (resourceHolder.hasActiveSession()) { + resourceHolder.close(); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java new file mode 100644 index 000000000..f956b7153 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.transaction; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import org.apiguardian.api.API; +import org.neo4j.driver.v1.AccessMode; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.TransactionConfig; +import org.springframework.lang.Nullable; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; + +/** + * Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. + * + * @author Michael J. Simons + */ +public class Neo4jTransactionManager extends AbstractPlatformTransactionManager { + + private final Driver driver; + + @API(status = API.Status.STABLE, since = "1.0") + public Neo4jTransactionManager(Driver driver) { + this.driver = driver; + } + + @Override + protected Object doGetTransaction() throws TransactionException { + + Neo4jResourceHolder resourceHolder = (Neo4jResourceHolder) TransactionSynchronizationManager + .getResource(driver); + return new Neo4jTransactionObject(resourceHolder); + } + + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + + return extractNeo4jTransaction(transaction).hasResourceHolder(); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + + Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); + + TransactionConfig transactionConfig = createTransactionConfigFrom(definition); + boolean readOnly = definition.isReadOnly(); + AccessMode accessMode = readOnly ? AccessMode.READ : AccessMode.WRITE; + List bookmarks = Collections.emptyList(); // TODO Bookmarksupport + + TransactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); + + try { + Session session = this.driver.session(accessMode, bookmarks); + + Neo4jResourceHolder resourceHolder = new Neo4jResourceHolder(session, transactionConfig); + transactionObject.setResourceHolder(resourceHolder); + + resourceHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.bindResource(driver, resourceHolder); + } catch (Exception ex) { + ex.printStackTrace(); + throw new TransactionSystemException(String.format("Could not open a new Neo4j session:", ex)); + } + } + + @Override + protected Object doSuspend(Object transaction) throws TransactionException { + + Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); + transactionObject.setResourceHolder(null); + + return TransactionSynchronizationManager.unbindResource(driver); + } + + @Override + protected void doResume(@Nullable Object transaction, Object suspendedResources) { + + TransactionSynchronizationManager.bindResource(driver, suspendedResources); + } + + @Override + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + + Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status); + transactionObject.getRequiredResourceHolder().commit(); + } + + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + + Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status); + transactionObject.getRequiredResourceHolder().rollback(); + } + + @Override + protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { + + Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status); + transactionObject.setRollbackOnly(); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + + Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); + transactionObject.getRequiredResourceHolder().close(); + + TransactionSynchronizationManager.unbindResource(driver); + } + + private static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) { + + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + throw new InvalidIsolationLevelException( + "Neo4jTransactionManager is not allowed to support custom isolation levels."); + } + + if (definition.getPropagationBehavior() != TransactionDefinition.PROPAGATION_REQUIRED) { + throw new IllegalTransactionStateException("Neo4jTransactionManager only supports 'required' propagation."); + } + + TransactionConfig.Builder builder = TransactionConfig.builder(); + if (definition.getTimeout() > 0) { + builder = builder.withTimeout(Duration.ofSeconds(definition.getTimeout())); + } + + return builder.build(); + } + + private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) { + + Assert.isInstanceOf(Neo4jTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", Neo4jTransactionObject.class, + transaction.getClass())); + + return (Neo4jTransactionObject) transaction; + } + + private static Neo4jTransactionObject extractNeo4jTransaction(DefaultTransactionStatus status) { + + return extractNeo4jTransaction(status.getTransaction()); + } + + static class Neo4jTransactionObject implements SmartTransactionObject { + + private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jResourceHolder is required but not present. o_O"; + + // The resource holder is null when the call to TransactionSynchronizationManager.getResource + // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder. + // If it is null, there's no existing session / transaction. + private @Nullable Neo4jResourceHolder resourceHolder; + + Neo4jTransactionObject(@Nullable Neo4jResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * Usually called in {@link #doBegin(Object, TransactionDefinition)} which is called when there's + * no existing transaction. + * + * @param resourceHolder A newly created resource holder with a fresh drivers session, + */ + void setResourceHolder(@Nullable Neo4jResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * @return {@literal true} if a {@link Neo4jResourceHolder} is set. + */ + boolean hasResourceHolder() { + return resourceHolder != null; + } + + Neo4jResourceHolder getRequiredResourceHolder() { + + Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); + return resourceHolder; + } + + void setRollbackOnly() { + + getRequiredResourceHolder().setRollbackOnly(); + } + + @Override + public boolean isRollbackOnly() { + return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly(); + } + + @Override + public void flush() { + + TransactionSynchronizationUtils.triggerFlush(); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java similarity index 65% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java index 0d79c36ea..4d2a2934c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java @@ -16,16 +16,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.session; +package org.springframework.data.neo4j.core.transaction; -import org.neo4j.driver.v1.Session; +import java.util.function.Supplier; /** - * Interface for factories creating {@link org.neo4j.driver.v1.Session Neo4j driver sessions}. + * Used for obtaining {@link org.neo4j.driver.v1.Session session bound (standard or reactive)} bound statement runners. + *

+ * Note: Intended for internal usage only. * * @author Michael J. Simons */ -public interface Neo4jSessionFactory { - - Session getSession(); +@FunctionalInterface +public interface StatementRunnerSupplier extends Supplier { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java similarity index 70% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java index 7340af78b..f75ba3547 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java @@ -2,6 +2,6 @@ * Core infrastructure for providing Neo4j sessions to Spring Data Neo4j. */ @NonNullApi -package org.springframework.data.neo4j.core.session; +package org.springframework.data.neo4j.core.transaction; import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java index 2a2bdef7d..90cdedc3b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.repository.config; +import static org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension.*; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -31,6 +33,7 @@ import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; /** * Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()}, @@ -77,7 +80,13 @@ public @interface EnableNeo4jRepositories { /** * Configures the name of the {@link Neo4jTemplate} bean to be used with the repositories detected. */ - String neo4jTemplateRef() default "neo4jTemplate"; + String neo4jTemplateRef() default DEFAULT_NEO4J_TEMPLATE_BEAN_NAME; + + /** + * Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories + * discovered through this annotation. Defaults to {@code transactionManager}. + */ + String transactionManagerRef() default DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; /** * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index a43f39e6f..e05e15a09 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -18,11 +18,11 @@ */ package org.springframework.data.neo4j.repository.config; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; -import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.config.RepositoryConfigurationSource; /** * This dedicated Neo4j repository extension will be registered via {@link Neo4jRepositoriesRegistrar} and then provide @@ -35,6 +35,14 @@ class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExten private static final String MODULE_PREFIX = "neo4j"; + /** + * See {@link AbstractBeanDefinition#INFER_METHOD}. + */ + static final String GENERATE_BEAN_NAME = "(generated)"; + + static final String DEFAULT_NEO4J_TEMPLATE_BEAN_NAME = "neo4jTemplate"; + static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; + /* * (non-Javadoc) * @see org.springframework.data.repository.config14.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() @@ -55,13 +63,15 @@ class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExten /* * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) */ @Override - public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { + public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { - AnnotationAttributes attributes = config.getAttributes(); + builder.addPropertyValue("transactionManager", + source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); - builder.addPropertyReference("neo4jOperations", attributes.getString("neo4jTemplateRef")); + builder.addPropertyReference("neo4jOperations", + source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 5db19abb3..ceebc17b0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -26,12 +26,17 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; /** * Repository base implementation for Neo4j. * * @author Gerrit Meier + * @author Michael J. Simons */ +@Repository +@Transactional(readOnly = true) class SimpleNeo4jRepository implements Neo4jRepository { private final Neo4jOperations neo4jOperations; @@ -51,11 +56,13 @@ class SimpleNeo4jRepository implements Neo4jRepository { } @Override + @Transactional public S save(S entity) { throw new UnsupportedOperationException("Not there yet."); } @Override + @Transactional public Iterable saveAll(Iterable entities) { throw new UnsupportedOperationException("Not there yet."); } @@ -86,23 +93,31 @@ class SimpleNeo4jRepository implements Neo4jRepository { } @Override + @Transactional public void deleteById(ID id) { throw new UnsupportedOperationException("Not there yet."); } @Override + @Transactional public void delete(T entity) { throw new UnsupportedOperationException("Not there yet."); } @Override + @Transactional public void deleteAll(Iterable entities) { + throw new UnsupportedOperationException("Not there yet."); } @Override + @Transactional public void deleteAll() { - throw new UnsupportedOperationException("Not there yet."); + + for (T element : findAll()) { + delete(element); + } } @Override diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java new file mode 100644 index 000000000..7efe3a9e9 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.transaction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.v1.AccessMode; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.StatementRunner; +import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.TransactionConfig; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class DefaultNeo4jStatementRunnerSupplierTest { + + @Mock + private Driver driver; + @Mock + private Session session; + @Mock + private Transaction transaction; + @Mock + UserTransaction userTransaction; + + @BeforeEach + void setUp() { + + AtomicBoolean sessionIsOpen = new AtomicBoolean(true); + AtomicBoolean transactionIsOpen = new AtomicBoolean(true); + + when(driver.session(AccessMode.WRITE, Collections.emptyList())).thenReturn(session); + + when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); + doAnswer(invocation -> { + sessionIsOpen.set(false); + return null; + }).when(session).close(); + when(session.isOpen()).thenAnswer(invocation -> sessionIsOpen.get()); + + doAnswer(invocation -> { + transactionIsOpen.set(false); + return null; + }).when(transaction).close(); + when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get()); + } + + @Nested + class BasedOnJtaTransactions { + + @Test + void shouldParticipateInOngoingTransactionWithCommit() throws Exception { + + when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( + driver); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); + + StatementRunner statementRunner = statementRunnerSupplier.get(); + + assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + } + }); + + verify(userTransaction).begin(); + + verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + + verify(session, times(2)).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(3)).isOpen(); + verify(transaction).success(); + verify(transaction).close(); + } + + @Test + public void shouldParticipateInOngoingTransactionWithRollback() throws Exception { + + when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( + driver); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); + + @SuppressWarnings({ "unused" }) + StatementRunner statementRunner = statementRunnerSupplier.get(); + + assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(userTransaction).begin(); + verify(userTransaction).rollback(); + + verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + + verify(session, times(2)).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(3)).isOpen(); + verify(transaction).failure(); + verify(transaction).close(); + } + } + + @Nested + class BasedOnNeo4jTransactions { + @Test + public void shouldParticipateInOngoingTransaction() { + + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( + driver); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + + @SuppressWarnings({ "unused" }) + StatementRunner statementRunner = statementRunnerSupplier.get(); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + + verify(session).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(2)).isOpen(); + verify(transaction).failure(); + verify(transaction).close(); + } + } + + @AfterEach + void verifyTransactionSynchronizationManagerState() { + + assertTrue(TransactionSynchronizationManager.getResourceMap().isEmpty()); + assertFalse(TransactionSynchronizationManager.isSynchronizationActive()); + assertNull(TransactionSynchronizationManager.getCurrentTransactionName()); + assertFalse(TransactionSynchronizationManager.isCurrentTransactionReadOnly()); + assertNull(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel()); + assertFalse(TransactionSynchronizationManager.isActualTransactionActive()); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java new file mode 100644 index 000000000..7b4ac3a84 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.transaction; + +import static org.mockito.Mockito.*; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.v1.AccessMode; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.TransactionConfig; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class Neo4jTransactionManagerTest { + + @Mock + private Driver driver; + @Mock + private Session session; + @Mock + private Transaction transaction; + @Mock + private StatementResult statementResult; + + @Test + public void triggerCommitCorrectly() { + + when(driver.session(AccessMode.WRITE, Collections.emptyList())).thenReturn(session); + when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); + when(transaction.run(anyString())).thenReturn(statementResult); + when(session.isOpen()).thenReturn(true); + when(transaction.isOpen()).thenReturn(true, false); + + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); + TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); + + Neo4jTemplate template = new Neo4jTemplate(driver); + template.executeQuery("RETURN 1"); + + txManager.commit(txStatus); + + verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + + verify(session).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + + verify(transaction, times(2)).isOpen(); + verify(transaction).success(); + verify(transaction).close(); + + verify(session).close(); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index c51f7736c..dd5e2ca94 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -33,9 +33,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; import org.testcontainers.containers.Neo4jContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -45,7 +48,8 @@ import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers class RepositoryIT { - @Container private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null); + @Container + private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null); private final PersonRepository repository; @@ -75,16 +79,26 @@ class RepositoryIT { @Configuration @EnableNeo4jRepositories + @EnableTransactionManagement static class Config { @Bean - public Neo4jOperations neo4jTemplate() { - String boltUrl = neo4jContainer.getBoltUrl(); - Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.none()); + public Driver driver() { - return new Neo4jTemplate(driver); + String boltUrl = neo4jContainer.getBoltUrl(); + return GraphDatabase.driver(boltUrl, AuthTokens.none()); } - } + @Bean + public PlatformTransactionManager transactionManager() { + return new Neo4jTransactionManager(driver()); + } + + @Bean + public Neo4jOperations neo4jTemplate() { + + return new Neo4jTemplate(driver()); + } + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java index c8113e9a7..8267394bd 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java @@ -20,6 +20,8 @@ package org.springframework.data.neo4j.repository.support; import static org.assertj.core.api.Assertions.*; +import java.util.Collections; + import org.junit.jupiter.api.Test; import org.springframework.data.domain.Example; import org.springframework.data.domain.Pageable; @@ -74,7 +76,8 @@ class SimpleNeo4jRepositoryTest { @Test void deleteAll1NotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteAll(null)); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> repository.deleteAll(Collections.emptyList())); } @Test From f5384dd6caea849f3b0dfc020bc469bb06f0335c Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 12 Mar 2019 12:11:19 +0100 Subject: [PATCH 036/342] Provide automatic configuration for SDN RX repositories. --- .../config/Neo4jRepositoryConfigurationExtension.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index e05e15a09..cc8b1a8b5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -27,11 +27,13 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; /** * This dedicated Neo4j repository extension will be registered via {@link Neo4jRepositoriesRegistrar} and then provide * all necessary beans to be registered in the application's context before the user's "business" beans gets registered. + *

+ * While it is public, it is mainly used for internal API respectively for Spring Boots automatic configuration. * * @author Michael J. Simons * @author Gerrit Meier */ -class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { +public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { private static final String MODULE_PREFIX = "neo4j"; From 53262adc96b6d48ac6b7f420ac15f8ed394dd22b Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 14 Mar 2019 14:16:09 +0100 Subject: [PATCH 037/342] Move annotations to independent schema package. --- README.adoc | 38 +++++++-- .../neo4j/core/{mapping => schema}/Node.java | 4 +- .../core/{mapping => schema}/Property.java | 5 +- .../data/neo4j/core/schema/Relationship.java | 83 +++++++++++++++++++ .../data/neo4j/core/schema/package-info.java | 10 +++ 5 files changed, 129 insertions(+), 11 deletions(-) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/{mapping => schema}/Node.java (91%) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/{mapping => schema}/Property.java (89%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java diff --git a/README.adoc b/README.adoc index 417364742..783865c8a 100644 --- a/README.adoc +++ b/README.adoc @@ -30,6 +30,22 @@ There are no short-term planes to create additional artifacts from those. NOTE: However, we should discuss if you could try a JDK 11 only project. +==== Schema + +We used the new Spring Data JDBC project as blueprint for some ideas now. +Spring Data JDBC doesn't build up the schema upfront. +Each time, a persistent entity is requested from the mapping context, that entity is read and fully described, including properties and all associations. +We can implement it in the same way. +The mapping context would return instances of `Neo4jPersistentEntity` which implements a Spring Data interface. +To fulfill the contract however, we would read the classes and store them in a schema that is free of Spring dependencies. +That way we we can avoid a compile time dependency to Spring Data and have an independent schema module, in which the mapping context is the connecting adapter. + +Spring Data JDBC doesn't restrict the supported or scanned classes from Spring Data sides. +Our schema should also support non-annotated classes and be smart about naming things, but we will require at least the `@Node` or `@Relationship` annotation to the outside world. + +The schema will life independend from Spring classes in `org.springframework.data.neo4j.core.schema`. +Each property of a class that is not identified as a simple type by `org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes` will be considered describing a relationship and thus required to be part of the schema as well. + === Other principles * Ensure that the underlying store (Neo4j) leaks as little as possible into the mapping. @@ -73,15 +89,17 @@ note "Implementation of Spring Data Commons SPI" as SDC_note package "org.springframework.data.neo4j" { package "core" { interface Neo4jTemplate - package "mapping" { - package "internal" { - note "GraphToObjectMapping" as graphMapping + package "schema" { + package "internal" { + note "Schema description" as schemaDescription + } + annotation Node + annotation Property + } + package "mapping" { + interface Neo4jPersistentEntity + interface Neo4jPersistentProperty } - annotation Node - annotation Property - interface Neo4jPersistentEntity - interface Neo4jPersistentProperty - } package "session" { interface Neo4jSessionFactory } @@ -120,6 +138,8 @@ core-[hidden]--->repository |Package|Comment |core |`Neo4jTemplate` and related classes. +|core.schema +|Annotations for marking classes as nodes to be saved as well as internal schema description. |core.mapping |Spring mapping information. |core.mapping.internal @@ -128,7 +148,7 @@ core-[hidden]--->repository |Connection to the `Driver` instance. |core.convert |_not used yet_ place for conversion related classes. -| | + |repository |Repository interfaces like `Neo4jRepository`. |repository.config diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Node.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Node.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Node.java index 63c3fb822..aae15b4e6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Node.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Node.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.springframework.data.neo4j.core.schema; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -25,6 +25,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; import org.springframework.core.annotation.AliasFor; /** @@ -36,6 +37,7 @@ import org.springframework.core.annotation.AliasFor; @Target(ElementType.TYPE) @Documented @Inherited +@API(status = API.Status.STABLE, since = "1.0") public @interface Node { @AliasFor("labels") diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Property.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java index bc9fcb04f..3c5e81861 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.springframework.data.neo4j.core.schema; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -25,6 +25,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; + /** * The annotation to configure the mapping from a property to an attribute and vice versa. * @@ -34,5 +36,6 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) @Documented @Inherited +@API(status = API.Status.STABLE, since = "1.0") public @interface Property { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java new file mode 100644 index 000000000..fe008577a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to configure mappings of relationship. + * + * @author Michael J. Simons + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Inherited +@API(status = API.Status.STABLE, since = "1.0") +public @interface Relationship { + + enum Direction { + + /** + * Describes an outgoing relationship. + */ + OUTGOING, + + /** + * Describes an incoming relationship. + */ + INCOMING, + + /** + * Describes an undirected relationship. + */ + UNDIRECTED + } + + @AliasFor("type") + String value() default ""; + + /** + * @return The type of the relationship. + */ + @AliasFor("value") + String type() default ""; + + /** + * If {@code direction} is {@link Direction#OUTGOING}, than the attribute annotated with {@link Relationship} will be + * the target node of the relationship and the class containing the annotated attribute will be the start node. + *

+ * If {@code direction} is {@link Direction#INCOMING}, than the attribute annotated with {@link Relationship} will be + * the start node of the relationship and the class containing the annotated attribute will be the end node. + *

+ * In case of {@code direction} being {@link Direction#UNDIRECTED}, an equally undirected relationships will be used + * between the annotated attribute and the class containing the attribute. + * + * @return The direction of the relationship. + */ + Direction direction() default Direction.OUTGOING; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java new file mode 100644 index 000000000..3a10722a2 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains the schema that is defined by a set of classes, representing nodes and relationships and their + * properties. It provides Neo4js main annotations to mark classes as persistable nodes. + * + * @author Michael J. Simons + */ +@NonNullApi +package org.springframework.data.neo4j.core.schema; + +import org.springframework.lang.NonNullApi; From ed76a9028e74d1deda56165945b79b646b6caac2 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 22 Mar 2019 17:20:35 +0100 Subject: [PATCH 038/342] Add some thoughts about a persistence context. --- README.adoc | 18 ++++++++- .../core/context/PersistenceContext.java | 37 +++++++++++++++++++ .../core/context/PersistenceContextImpl.java | 25 +++++++++++++ .../data/neo4j/core/context/package-info.java | 9 +++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContextImpl.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java diff --git a/README.adoc b/README.adoc index 783865c8a..155309e42 100644 --- a/README.adoc +++ b/README.adoc @@ -30,6 +30,7 @@ There are no short-term planes to create additional artifacts from those. NOTE: However, we should discuss if you could try a JDK 11 only project. +[[schema]] ==== Schema We used the new Spring Data JDBC project as blueprint for some ideas now. @@ -43,9 +44,22 @@ That way we we can avoid a compile time dependency to Spring Data and have an in Spring Data JDBC doesn't restrict the supported or scanned classes from Spring Data sides. Our schema should also support non-annotated classes and be smart about naming things, but we will require at least the `@Node` or `@Relationship` annotation to the outside world. -The schema will life independend from Spring classes in `org.springframework.data.neo4j.core.schema`. +The schema will life independent from Spring classes in `org.springframework.data.neo4j.core.schema`. Each property of a class that is not identified as a simple type by `org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes` will be considered describing a relationship and thus required to be part of the schema as well. +==== Context + +NOTE: Context in this sections refers especially to dirty tracking and dealing with state of entities. + +It is unlikely that we can do completely without context. +While it's easy enough to update on entity as is with the <>, it's unlikely that we can deal this way with collections of relationships. +Spring Data JDBC's approach is as follow +____ +If the aggregate root is not new, all referenced entities get deleted, the aggregate root gets updated, and all referenced entities get inserted again. +____ + +That sounds good in theory, but the Graph itself being more optimized for reads than ongoing large chunks of writes probably won't like that. + === Other principles * Ensure that the underlying store (Neo4j) leaks as little as possible into the mapping. @@ -140,6 +154,8 @@ core-[hidden]--->repository |`Neo4jTemplate` and related classes. |core.schema |Annotations for marking classes as nodes to be saved as well as internal schema description. +|context +|Infrastructure for dirty tracking etc. |core.mapping |Spring mapping information. |core.mapping.internal diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java new file mode 100644 index 000000000..1afa01ec0 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.context; + +import org.apiguardian.api.API; + +/** + * Represents the state of varies instances being tracked. Those include: + * + *

    + *
  • All nodes
  • + *
  • All relationships
  • + *
+ * + * including properties of them. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public interface PersistenceContext { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContextImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContextImpl.java new file mode 100644 index 000000000..37a47c3bd --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContextImpl.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.context; + +/** + * @author Michael J. Simons + */ +class PersistenceContextImpl implements PersistenceContext { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java new file mode 100644 index 000000000..7def88a09 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java @@ -0,0 +1,9 @@ +/** + * Contains all necessary infrastructur for the context of a session, i.e. dirty tracking and related. + * + * @author Michael J. Simons + */ +@NonNullApi +package org.springframework.data.neo4j.core.context; + +import org.springframework.lang.NonNullApi; From 059a9d8f8d2be18933bd37c0658667d16bb90f64 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 25 Mar 2019 13:16:26 +0000 Subject: [PATCH 039/342] Add thoughts about relationships and labels. --- README.adoc | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/README.adoc b/README.adoc index 155309e42..f06d3199c 100644 --- a/README.adoc +++ b/README.adoc @@ -69,6 +69,88 @@ That sounds good in theory, but the Graph itself being more optimized for reads Supporting the embedded use case will be solved on the drivers level. + +=== Relationships + +=== Simple relationships + +We provide `@Relationship` for mapping relationships without properties. +This annotation shall be used for 1:1 and 1:n mappings. +It provides an attribute to specifiy the name of the relationship. +When a relationship is mapped on both sides (that is a circular mapping in the domain model from start to endnode and back) the annotation provides the `inverse` attribute to cater the following use case: + +[source,java] +---- +@Node("User") +static class UserNode { + + @Relationship(type = "OWNS", inverse = "owner") + List bikes; +} + +static class BikeNode { + + UserNode owner; + + UserNode renter; +} +---- + +Without the inverse that mapping would lead to three relationships: + +1. `(user) - [:OWNS] -> (bike)` +2. `(user) <- [:OWNER] - (bike)` +3. `(user) <- [:RENTER] - (bike)` + +Which is probably not the intended graph. +Specifying `inverse` removes ambiguity here. + +=== "Rich" relationships + +There should be no means of using a relationship as aggregate root in SDN RX (like it is today the case with `@RelationshipEntity`). +Instead we suggest that properties of relationships are mapped to POJOs. +This has the following requirements: +On the node representing the start node, the relationship (either 1:1 or 1:n) has to be annotated with `@Relationship` specifying the type of the end node like this: + +[source,java] +---- +@Relationship("HAS_MEMBER", endNodeType = SoloArtistEntity.class) +private List member = new ArrayList<>(); +---- + +The POJO, in this case `Member` is required to have the following structure : + +[source,java] +---- +public static class Member { + + private SoloArtistEntity artist; + + private Year joinedIn; + + private Year leftIn; +} +---- + +It must have exactly _one_ attribute of `endNodeType`. +All other attributes are mapped from the properties of the relationship. + +The motivation behind this is that a relationship needs to be manifested in the domain model, +but as the domain model usually isn't a graph, it manifests itself as a thing, not a relationship as is. +We prefer that people use the mapping framework domain centric, not database centric. +In the relational world it is an anti pattern to map out n:m (intersection) tables. +If they have attributes, a schema is usually refactored into a 1:n and a n:1 table and an entity structure. +We don't need another entity, though. + + +=== Labels + +NOTE: Do we need support for dynamic labels? +We propose a new `@Node` annotation that takes in an array of strings as labels for that object. +We like to get rid of `@Label` annotation supporting dynamic labels for objects + + + === Integration tests Integration tests take more time by their very nature. From e0d637f801ae690a6614c2da713f935c37629309 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Mon, 25 Mar 2019 16:14:00 +0100 Subject: [PATCH 040/342] Add thoughts about dirty tracking. --- README.adoc | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index f06d3199c..5944d1ba7 100644 --- a/README.adoc +++ b/README.adoc @@ -304,6 +304,29 @@ This is a wrapper around the `java.lang.reflect.Method` passed into the `resolve At the moment the implementation just takes the value of the provided `@Query` annotation by calling `getAnnotatedQuery` on the `Neo4jQueryMethod` and executes it through the `neo4jOperations` (`Neo4jTemplate`) class. +=== Dirty tracking + +We considered several approaches of dirty tracking in SDN-rx: + +. No dirty tracking at all. + _Not an option when it comes to relationships._ +. Dirty tracking through hashes. + _Not on the level of detail (fields) we want to have it._ +. Using some kind of event / listener to track changes. +. Shallow copy of objects to get compared on save. + _A full copy of the objects will occupy twice the memory._ + +Picking the third option for now (_using some kind of event / listener_) would allow us to track single field / relationship changes per object. +An entity will get tracked after it gets initially saved into or loaded from the database. +As the technical solution we decided to use proxies to listen for the changes. +It will then be required for the user to work with the returned (proxy) object after calling `save` to set additional fields etc. + +The event tracking and event triggering through the proxies should communicate via an API to have the option of using another tracking approach in the future. + +==== Side-effect on "flush mode" +We can also track repository interactions like `save` and defer them depending on the flush mode. +This would make it possible to remove the need for calling `save` explicitly but be more JPA-ish. + [[starter]] == Spring Boot Starter @@ -312,4 +335,4 @@ The Spring Data Neo4j RX Spring Boot Starter provides automatic configuration to * Create an instance of the https://github.com/neo4j/neo4j-java-driver[neo4j-java-driver] * Configure Spring Data Neo4j RX itself inside a Spring Boot application and enabling Spring Data repositories -Read me about it link:spring-data-neo4j-rx-spring-boot-starter-parent/README.adoc[here]. \ No newline at end of file +Read me about it link:spring-data-neo4j-rx-spring-boot-starter-parent/README.adoc[here]. From 727b37b56d38839256fa35fe1f667491063cb56c Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 26 Mar 2019 23:20:45 +0000 Subject: [PATCH 041/342] Update testcontainers. --- pom.xml | 2 +- .../springframework/data/neo4j/integration/RepositoryIT.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3dc0d26ec..8a6d1c01b 100644 --- a/pom.xml +++ b/pom.xml @@ -108,7 +108,7 @@ false 1.7.25 2.2.0.BUILD-SNAPSHOT - 1.10.6 + 1.10.7 diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index dd5e2ca94..9dc6ef2b8 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -49,7 +49,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; class RepositoryIT { @Container - private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null); + private static Neo4jContainer neo4jContainer = new Neo4jContainer().withoutAuthentication(); private final PersonRepository repository; From 7aa029926daad070817f7f4ce48d1a3b7aad4490 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 26 Mar 2019 23:21:08 +0000 Subject: [PATCH 042/342] Add additional testscenarios for transaction management. --- .../core/transaction/Neo4jResourceHolder.java | 14 +- .../transaction/Neo4jTransactionManager.java | 3 +- ...faultNeo4jStatementRunnerSupplierTest.java | 139 +++++++++++++----- 3 files changed, 108 insertions(+), 48 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java index 73b99b3a1..f49a0925a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java @@ -38,11 +38,11 @@ public class Neo4jResourceHolder extends ResourceHolderSupport { private final Transaction transaction; - public Neo4jResourceHolder(Session session) { + Neo4jResourceHolder(Session session) { this(session, TransactionConfig.empty()); } - public Neo4jResourceHolder(Session session, TransactionConfig transactionConfig) { + Neo4jResourceHolder(Session session, TransactionConfig transactionConfig) { this.session = session; this.transaction = this.session.beginTransaction(transactionConfig); @@ -52,7 +52,7 @@ public class Neo4jResourceHolder extends ResourceHolderSupport { return transaction; } - public void commit() { + void commit() { Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed."); Assert.state(!isRollbackOnly(), "Resource msut not be marked as rollback only."); @@ -61,7 +61,7 @@ public class Neo4jResourceHolder extends ResourceHolderSupport { transaction.close(); } - public void rollback() { + void rollback() { Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed."); @@ -69,7 +69,7 @@ public class Neo4jResourceHolder extends ResourceHolderSupport { transaction.close(); } - public void close() { + void close() { Assert.state(hasActiveSession(), "Session must be open, but has already been closed."); @@ -90,12 +90,12 @@ public class Neo4jResourceHolder extends ResourceHolderSupport { throw new UnsupportedOperationException(); } - public boolean hasActiveSession() { + boolean hasActiveSession() { return session.isOpen(); } - public boolean hasActiveTransaction() { + boolean hasActiveTransaction() { return transaction.isOpen(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index f956b7153..486d40d79 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -89,8 +89,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager resourceHolder.setSynchronizedWithTransaction(true); TransactionSynchronizationManager.bindResource(driver, resourceHolder); } catch (Exception ex) { - ex.printStackTrace(); - throw new TransactionSystemException(String.format("Could not open a new Neo4j session:", ex)); + throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage())); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java index 7efe3a9e9..3a6266f50 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java @@ -35,6 +35,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.neo4j.driver.v1.AccessMode; import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.Session; @@ -51,6 +53,7 @@ import org.springframework.transaction.support.TransactionTemplate; * @author Michael J. Simons */ @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class DefaultNeo4jStatementRunnerSupplierTest { @Mock @@ -68,6 +71,7 @@ class DefaultNeo4jStatementRunnerSupplierTest { AtomicBoolean sessionIsOpen = new AtomicBoolean(true); AtomicBoolean transactionIsOpen = new AtomicBoolean(true); + when(driver.session()).thenReturn(session); when(driver.session(AccessMode.WRITE, Collections.emptyList())).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); @@ -84,6 +88,100 @@ class DefaultNeo4jStatementRunnerSupplierTest { when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get()); } + @Test + void shouldWorkWithoutSynchronizations() { + StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( + driver); + + @SuppressWarnings({ "unused" }) + StatementRunner statementRunner = statementRunnerSupplier.get(); + + verify(driver).session(); + verifyNoMoreInteractions(driver, session, transaction); + } + + @Nested + class BasedOnNeo4jTransactions { + @Test + void shouldOpenNewTransaction() { + + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( + driver); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + + @SuppressWarnings({ "unused" }) + StatementRunner statementRunner = statementRunnerSupplier.get(); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + + verify(session).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(2)).isOpen(); + verify(transaction).failure(); + verify(transaction).close(); + } + + @Test + void shouldParticipateInOngoingTransaction() { + + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( + driver); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { + + @SuppressWarnings({ "unused" }) + StatementRunner outerStatementRunner = statementRunnerSupplier.get(); + assertThat(outerStatus.isNewTransaction()).isTrue(); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus innerStatus) { + + assertThat(innerStatus.isNewTransaction()).isFalse(); + + @SuppressWarnings({ "unused" }) + StatementRunner innerStatementRunner = statementRunnerSupplier.get(); + } + }); + + outerStatus.setRollbackOnly(); + } + }); + + verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + + verify(session).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(2)).isOpen(); + verify(transaction).failure(); + verify(transaction).close(); + } + } + @Nested class BasedOnJtaTransactions { @@ -107,6 +205,7 @@ class DefaultNeo4jStatementRunnerSupplierTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); + @SuppressWarnings({ "unused" }) StatementRunner statementRunner = statementRunnerSupplier.get(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); @@ -127,7 +226,7 @@ class DefaultNeo4jStatementRunnerSupplierTest { } @Test - public void shouldParticipateInOngoingTransactionWithRollback() throws Exception { + void shouldParticipateInOngoingTransactionWithRollback() throws Exception { when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); @@ -170,44 +269,6 @@ class DefaultNeo4jStatementRunnerSupplierTest { } } - @Nested - class BasedOnNeo4jTransactions { - @Test - public void shouldParticipateInOngoingTransaction() { - - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); - TransactionTemplate txTemplate = new TransactionTemplate(txManager); - StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( - driver); - - txTemplate.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { - - assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); - assertThat(transactionStatus.isNewTransaction()).isTrue(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); - - @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = statementRunnerSupplier.get(); - - transactionStatus.setRollbackOnly(); - } - }); - - verify(driver).session(AccessMode.WRITE, Collections.emptyList()); - - verify(session).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - - verify(transaction, times(2)).isOpen(); - verify(transaction).failure(); - verify(transaction).close(); - } - } - @AfterEach void verifyTransactionSynchronizationManagerState() { From 33bf99dcc4f9e38683d25a4e318e7011ebb629d0 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 14 Mar 2019 21:53:31 +0100 Subject: [PATCH 043/342] Add a minimal, Spring independent schema. --- etc/jqassistant/structure.adoc | 4 +- .../core/mapping/Neo4jMappingContext.java | 76 +++++++++++++++- .../core/mapping/Neo4jPersistentEntity.java | 7 ++ .../mapping/Neo4jPersistentEntityImpl.java | 48 ++++++++++ .../core/mapping/Neo4jPersistentProperty.java | 2 + .../mapping/Neo4jPersistentPropertyImpl.java | 59 ++++++++++++ .../schema/IllegalSchemaChangeException.java | 34 +++++++ .../neo4j/core/schema/Neo4jSimpleTypes.java | 34 +++++++ .../neo4j/core/schema/NodeDescription.java | 91 +++++++++++++++++++ .../data/neo4j/core/schema/Property.java | 9 +- .../core/schema/PropertyDescription.java | 44 +++++++++ .../data/neo4j/core/schema/Relationship.java | 4 +- .../core/schema/RelationshipDescription.java | 43 +++++++++ .../data/neo4j/core/schema/Schema.java | 84 +++++++++++++++++ ...Neo4jRepositoryConfigurationExtension.java | 16 ++++ .../core/mapping/Neo4jMappingContextTest.java | 64 ++++++++++++- .../Neo4jPersistentEntityImplTest.java | 44 +++++++++ .../data/neo4j/core/schema/SchemaTest.java | 40 ++++++++ 18 files changed, 694 insertions(+), 9 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java diff --git a/etc/jqassistant/structure.adoc b/etc/jqassistant/structure.adoc index 18dd3dc66..afb12264c 100644 --- a/etc/jqassistant/structure.adoc +++ b/etc/jqassistant/structure.adoc @@ -5,11 +5,11 @@ Most of the time, the package structure under `org.springframework.data.neo4j` s [[structure:mapping]] [source,cypher,role=constraint,requiresConcepts="dependency:Package"] -.The mapping package must not depend on other SDN-RX packages. +.The mapping package must not depend on any other SDN-RX packages than `schema` ---- MATCH (a:Main:Artifact) MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a) WHERE p1.fqn = 'org.springframework.data.neo4j.core.mapping' - AND NOT (p1) -[:CONTAINS]-> (p2) + AND NOT ((p1) -[:CONTAINS]-> (p2) OR p2.fqn = 'org.springframework.data.neo4j.core.schema') return p1,p2 ---- diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 2282782d0..967b0a1ba 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -18,9 +18,21 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.PropertyDescription; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.neo4j.core.schema.RelationshipDescription; +import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.data.util.TypeInformation; /** @@ -30,6 +42,8 @@ import org.springframework.data.util.TypeInformation; */ public class Neo4jMappingContext extends AbstractMappingContext, Neo4jPersistentProperty> { + private final Schema schema = new Schema(); + /* * (non-Javadoc) * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) @@ -37,7 +51,7 @@ public class Neo4jMappingContext extends AbstractMappingContext Neo4jPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - throw new UnsupportedOperationException("Not yet implemented."); + return new Neo4jPersistentEntityImpl<>(typeInformation); } /* @@ -48,6 +62,64 @@ public class Neo4jMappingContext extends AbstractMappingContext neo4jPersistentProperties, SimpleTypeHolder simpleTypeHolder) { - throw new UnsupportedOperationException("Not yet implemented."); + return new Neo4jPersistentPropertyImpl(property, neo4jPersistentProperties, simpleTypeHolder); } + + @Override + public void initialize() { + super.initialize(); + + super.getPersistentEntities().forEach(m -> + schema.registerNodeDescription(describeAsNode(m)) + ); + } + + public Schema getSchema() { + return schema; + } + + private NodeDescription describeAsNode(Neo4jPersistentEntity entity) { + + List properties = new ArrayList<>(); + + // TODO break this up into separate methods. + entity.doWithProperties(new SimplePropertyHandler() { + @Override + public void doWithPersistentProperty(PersistentProperty persistentProperty) { + org.springframework.data.neo4j.core.schema.Property propertyAnnotation = + persistentProperty.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); + + String propertyName = persistentProperty.getName(); + if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty() + && propertyAnnotation.name().trim().length() != 0) { + propertyName = propertyAnnotation.name().trim(); + } + + properties.add(new PropertyDescription(persistentProperty.getName(), propertyName)); + } + }); + + List relationships = new ArrayList<>(); + entity.doWithAssociations(new SimpleAssociationHandler() { + @Override + public void doWithAssociation(Association> association) { + + Neo4jPersistentEntity obverseOwner = Neo4jMappingContext.this + .getPersistentEntity(association.getInverse().getAssociationTargetType()); + + Relationship outgoingRelationship = association.getInverse().findAnnotation(Relationship.class); + String type; + if (outgoingRelationship != null && outgoingRelationship.type() != null) { + type = outgoingRelationship.type(); + } else { + type = association.getInverse().getName(); + } + relationships.add(new RelationshipDescription(type, obverseOwner.getPrimaryLabel())); + } + }); + + return new NodeDescription(entity.getPrimaryLabel(), properties, relationships); + } + + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java index dadc200b2..ea6d461e0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.mapping; +import org.apiguardian.api.API; import org.springframework.data.mapping.model.MutablePersistentEntity; /** @@ -25,5 +26,11 @@ import org.springframework.data.mapping.model.MutablePersistentEntity; * * @author Michael J. Simons */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Neo4jPersistentEntity extends MutablePersistentEntity { + + /** + * @return The primary label of this entity inside Neo4j. + */ + String getPrimaryLabel(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java new file mode 100644 index 000000000..ac522e3c9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.util.TypeInformation; + +/** + * @author Michael J. Simons + */ +class Neo4jPersistentEntityImpl extends BasicPersistentEntity + implements Neo4jPersistentEntity { + + private final String primaryLabel; + + Neo4jPersistentEntityImpl(TypeInformation information) { + super(information); + + Node nodeAnnotation = this.findAnnotation(Node.class); + if (nodeAnnotation == null || nodeAnnotation.labels().length != 1) { + primaryLabel = this.getType().getSimpleName(); + } else { + primaryLabel = nodeAnnotation.labels()[0]; + } + } + + @Override + public String getPrimaryLabel() { + return primaryLabel; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java index 356c7d496..dbc1ef891 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.mapping; +import org.apiguardian.api.API; import org.springframework.data.mapping.PersistentProperty; /** @@ -25,5 +26,6 @@ import org.springframework.data.mapping.PersistentProperty; * * @author Michael J. Simons */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Neo4jPersistentProperty extends PersistentProperty { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java new file mode 100644 index 000000000..0678b8a51 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * @author Michael J. Simons + */ +class Neo4jPersistentPropertyImpl extends AnnotationBasedPersistentProperty + implements Neo4jPersistentProperty { + + /** + * Creates a new {@link AnnotationBasedPersistentProperty}. + * + * @param property must not be {@literal null}. + * @param owner must not be {@literal null}. + * @param simpleTypeHolder + */ + Neo4jPersistentPropertyImpl(Property property, + PersistentEntity owner, + SimpleTypeHolder simpleTypeHolder) { + + super(property, owner, simpleTypeHolder); + } + + @Override + protected Association createAssociation() { + + return new Association<>(this, null); + } + + @Override + public boolean isAssociation() { + return !SimpleTypeHolder.DEFAULT.isSimpleType(super.getType()); + } + + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java new file mode 100644 index 000000000..060337971 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import org.apiguardian.api.API; + +/** + * Exception to be thrown when any change to a {@link Schema} would lead to an inconsistent state. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public class IllegalSchemaChangeException extends IllegalArgumentException { + + IllegalSchemaChangeException(String s) { + super(s); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java new file mode 100644 index 000000000..155eef31d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import org.apiguardian.api.API; + +/** + * A list of Neo4j simple types: All attributes that can be mapped to a property. There is never a relationship + * established for attributes of a node that are simple types. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Neo4jSimpleTypes { + + private Neo4jSimpleTypes() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java new file mode 100644 index 000000000..0fe112b49 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apiguardian.api.API; + +/** + * Describes how a class is mapped to a node inside the database. It provides navigable links to relationships and + * access to the nodes properties. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class NodeDescription { + + /** + * The primary label of this node. + */ + private final String primaryLabel; + + private final Map propertiesByFieldName; + + private final List relationships; + + public NodeDescription(String primaryLabel, List properties, + List relationships) { + this.primaryLabel = primaryLabel; + this.propertiesByFieldName = properties.stream() + .collect(Collectors.toMap(PropertyDescription::getFieldName, Function + .identity())); + this.relationships = new ArrayList<>(relationships); + } + + /** + * @return The primary label of this node + */ + public String getPrimaryLabel() { + return primaryLabel; + } + + /** + * @return The properties of this node + */ + public Collection getProperties() { + return Collections.unmodifiableCollection(propertiesByFieldName.values()); + } + + /** + * Retrieves a properties description by its field name. + * + * @param fieldName The field name under which the node is described + * @return The description if any + */ + public Optional getPropertyDescription(String fieldName) { + return Optional.ofNullable(this.propertiesByFieldName.get(fieldName)); + } + + /** + * This returns the outgoing relationships this node has to other nodes directions. + * + * @return The relationships defined by instances of this node. + */ + public Collection getRelationships() { + return Collections.unmodifiableCollection(relationships); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java index 3c5e81861..28b07cde0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java @@ -26,6 +26,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.springframework.core.annotation.AliasFor; /** * The annotation to configure the mapping from a property to an attribute and vice versa. @@ -33,9 +34,15 @@ import org.apiguardian.api.API; * @author Michael J. Simons */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Target(ElementType.FIELD) @Documented @Inherited @API(status = API.Status.STABLE, since = "1.0") public @interface Property { + + @AliasFor("name") + String value() default ""; + + @AliasFor("value") + String name() default ""; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java new file mode 100644 index 000000000..732ac4049 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import lombok.Getter; + +import org.apiguardian.api.API; + +/** + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +@Getter +public final class PropertyDescription { + + private final String fieldName; + + private final String propertyName; + + // TODO basically all the properties from Springs PersistentProperty are needed. + // Two options: Turn that classes ending in XXXDescription into interfaces and let Neo4jPersistentProperty extend + // from it as well, or copy needed stuff into a format that fits our needs best. + + public PropertyDescription(String fieldName, String propertyName) { + this.fieldName = fieldName; + this.propertyName = propertyName; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java index fe008577a..f6da825ff 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java @@ -34,7 +34,7 @@ import org.springframework.core.annotation.AliasFor; * @author Michael J. Simons */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Target(ElementType.FIELD) @Documented @Inherited @API(status = API.Status.STABLE, since = "1.0") @@ -67,6 +67,8 @@ public @interface Relationship { @AliasFor("value") String type() default ""; + String inverse() default ""; + /** * If {@code direction} is {@link Direction#OUTGOING}, than the attribute annotated with {@link Relationship} will be * the target node of the relationship and the class containing the annotated attribute will be the start node. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java new file mode 100644 index 000000000..2f95b489c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import org.apiguardian.api.API; + +/** + * Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction + * is maybe defined on the {@link NodeDescription} reachable in the {@link Schema} via it's primary label defined by + * {@link #target}. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +@RequiredArgsConstructor +@ToString +@EqualsAndHashCode(of = { "type", "target" }) +public final class RelationshipDescription { + + private final String type; + + private final String target; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java new file mode 100644 index 000000000..2de859b2e --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apiguardian.api.API; + +/** + * Contains the descriptions of all nodes, their properties and relationships known to SDN-RX. + * + * The schema is currently designed to be mutual. + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Schema { + + private final Map nodeDescriptionsByPrimaryLabel = new HashMap<>(); + + // Just added as a reminder. Add adaquate locks + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock read = lock.readLock(); + private final Lock write = lock.writeLock(); + + /** + * Registers a node description under it's primary label. + * + * @param newDescription The new node description. + * @return This schema. + * @throws IllegalSchemaChangeException when the description is already registered (under any label) + */ + public Schema registerNodeDescription(NodeDescription newDescription) { + + String primaryLabel = newDescription.getPrimaryLabel(); + if (this.nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel)) { + throw new IllegalSchemaChangeException(String + .format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s", + primaryLabel)); + } + + if (this.nodeDescriptionsByPrimaryLabel.containsValue(newDescription)) { + Optional label = this.nodeDescriptionsByPrimaryLabel.entrySet().stream() + .filter(e -> e.getValue().equals(newDescription)).map( + Map.Entry::getKey).findFirst(); + + throw new IllegalSchemaChangeException(String + .format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s", + newDescription, label.orElse("n/a"))); + } + + this.nodeDescriptionsByPrimaryLabel.put(primaryLabel, newDescription); + return this; + } + + /** + * Retrieves a nodes description by its primary label. + * + * @param primaryLabel The primary label under which the node is described + * @return The description if any + */ + public Optional getNodeDescription(String primaryLabel) { + return Optional.ofNullable(this.nodeDescriptionsByPrimaryLabel.get(primaryLabel)); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index cc8b1a8b5..cecf03300 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -18,8 +18,14 @@ */ package org.springframework.data.neo4j.repository.config; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; @@ -63,6 +69,16 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati return MODULE_PREFIX; } + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.singleton(Node.class); + } + + @Override + protected Collection> getIdentifyingTypes() { + return Collections.singleton(Neo4jRepository.class); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index 4db2b0827..511177caf 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -20,7 +20,19 @@ package org.springframework.data.neo4j.core.mapping; import static org.assertj.core.api.Assertions.*; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.Property; +import org.springframework.data.neo4j.core.schema.PropertyDescription; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.neo4j.core.schema.RelationshipDescription; +import org.springframework.data.neo4j.core.schema.Schema; /** * @author Michael J. Simons @@ -28,9 +40,55 @@ import org.junit.jupiter.api.Test; class Neo4jMappingContextTest { @Test - void shouldCreatePersistentEntity() { + void initializationOfSchemaShouldWork() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - new Neo4jMappingContext().createPersistentEntity(null)); + Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); + neo4jMappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class))); + neo4jMappingContext.initialize(); + + Schema schema = neo4jMappingContext.getSchema(); + + Optional optionalUserNodeDescription = schema.getNodeDescription("User"); + assertThat(optionalUserNodeDescription) + .isPresent() + .hasValueSatisfying(userNodeDescription -> { + assertThat(userNodeDescription.getProperties()) + .extracting(PropertyDescription::getFieldName) + .containsExactlyInAnyOrder("name", "first_name"); + + assertThat(userNodeDescription.getProperties()) + .extracting(PropertyDescription::getPropertyName) + .containsExactlyInAnyOrder("name", "firstName"); + }); + + Optional optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); + assertThat(optionalBikeNodeDescription) + .isPresent() + .hasValueSatisfying(bikeNodeDescription -> + assertThat(bikeNodeDescription.getRelationships()) + .containsExactlyInAnyOrder( + new RelationshipDescription("owner", "User"), + new RelationshipDescription("renter", "User"))); } + + @Node("User") + static class UserNode { + + @Relationship(type = "OWNS", inverse = "owner") + List bikes; + + String name; + + @Property(name = "firstName") + String first_name; + } + + static class BikeNode { + + UserNode owner; + + List renter; + } + + } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java new file mode 100644 index 000000000..e3f6b6040 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.schema.Node; + +/** + * @author Michael J. Simons + */ +public class Neo4jPersistentEntityImplTest { + + private Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + + @Test + void shouldDiscoverAnnotatedPrimaryLabel() { + + Neo4jPersistentEntity entity = mappingContext.getPersistentEntity(DummySubEntity.class); + + assertThat(entity).isNotNull(); + } + + @Node("DummySubNode") + static class DummySubEntity { + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java new file mode 100644 index 000000000..1defe9f7e --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class SchemaTest { + + @Test + void shouldGetNodeDescription() { + NodeDescription description = new NodeDescription("aLabel", Collections.emptyList(), Collections.emptyList()); + + Schema schema = new Schema().registerNodeDescription(description); + assertThat(schema.getNodeDescription("aLabel")).isPresent().contains(description); + assertThat(schema.getNodeDescription("anotherLabel")).isNotPresent(); + } +} From d310d832791ea36ab94ceb0b9748458fbc7cac8b Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 27 Mar 2019 00:23:40 +0000 Subject: [PATCH 044/342] Add @Id and friends. --- etc/adr/adr-001.adoc | 26 ++++++++ .../core/mapping/Neo4jMappingContext.java | 18 +++++- .../data/neo4j/core/schema/Id.java | 64 +++++++++++++++++++ .../data/neo4j/core/schema/IdDescription.java | 59 +++++++++++++++++ .../data/neo4j/core/schema/IdGenerator.java | 39 +++++++++++ .../neo4j/core/schema/NodeDescription.java | 16 ++++- .../core/schema/PropertyDescription.java | 11 +++- .../core/mapping/Neo4jMappingContextTest.java | 34 ++++++---- .../data/neo4j/core/schema/SchemaTest.java | 3 +- 9 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 etc/adr/adr-001.adoc create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java diff --git a/etc/adr/adr-001.adoc b/etc/adr/adr-001.adoc new file mode 100644 index 000000000..83c2b3614 --- /dev/null +++ b/etc/adr/adr-001.adoc @@ -0,0 +1,26 @@ +== ADR 1: Configuration of Id-Mapping + +=== Status + +accepted + +=== Context + +SDN RX needs to provide a configuration of Ids mappings. +Ids can either be internal (native Neo4j) Ids or generated Ids. + +=== Decision + +The configuration should not be ambiguous. +`@org.springframework.data.annotation.Id` will be used as the marker for an Id. +A _strategy_ will be used to decide whether the annotated attribute will be mapped to `id(node)` or set from the external. +The strategy will either be `internal`, `assigned` or `generated`. +`generated` will require an additional attribute of `generator`. + +The internal strategy will be the default. + +To configure the Id strategy, a meta-annotated `@Id` annotation will be provided through `org.springframework.data.neo4j.core.schema.Id` + +=== Consequences + +We are still compatible with the OGM 3.1+ approach of recommending `@Id long id;` while providing a clear direction for the user. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 967b0a1ba..0fff776cb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -20,7 +20,9 @@ package org.springframework.data.neo4j.core.mapping; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.SimpleAssociationHandler; @@ -28,6 +30,8 @@ import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.PropertyDescription; import org.springframework.data.neo4j.core.schema.Relationship; @@ -118,7 +122,19 @@ public class Neo4jMappingContext extends AbstractMappingContext optionalIdAnnotation = Optional + .ofNullable(AnnotatedElementUtils.findMergedAnnotation(idProperty.getField(), Id.class)); + final IdDescription idDescription = optionalIdAnnotation + .map(idAnnotation -> new IdDescription(idAnnotation.strategy(), idAnnotation.generator())) + .orElseGet(() -> new IdDescription()); + + return NodeDescription.builder() + .primaryLabel(entity.getPrimaryLabel()) + .idDescription(idDescription) + .properties(properties) + .relationships(relationships) + .build(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java new file mode 100644 index 000000000..ae6bcf2b1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * Annotation to configure assigment of ids. + * + * @author Michael J. Simons + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +@Inherited +@org.springframework.data.annotation.Id +@API(status = API.Status.STABLE, since = "1.0") +public @interface Id { + + enum Strategy { + + /** + * The default, use Neo4js internal ids. + */ + INTERNAL, + + /** + * Use assigned values. + */ + ASSIGNED, + + /** + * Use generated values, generator class is required. + */ + GENERATED + } + + Strategy strategy() default Strategy.INTERNAL; + + Class generator() default IdDescription.NoopIdGenerator.class; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java new file mode 100644 index 000000000..f09c13fe6 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import org.apiguardian.api.API; + +/** + * Description howto generate Ids for entities. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class IdDescription { + + private final Id.Strategy idStrategy; + + private final Class idGeneratorClass; + + public IdDescription() { + this(Id.Strategy.INTERNAL, NoopIdGenerator.class); + } + + public IdDescription(Id.Strategy idStrategy, Class idGeneratorClass) { + this.idStrategy = idStrategy; + this.idGeneratorClass = idGeneratorClass; + } + + public Id.Strategy getIdStrategy() { + return idStrategy; + } + + public Class getIdGeneratorClass() { + return idGeneratorClass; + } + + static class NoopIdGenerator implements IdGenerator { + + @Override + public Object generateId(Object entity) { + return null; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java new file mode 100644 index 000000000..52d6232b1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import org.apiguardian.api.API; + +/** + * Interface for generating ids for entities. + * + * @author Michael J. Simons + */ +@FunctionalInterface +@API(status = API.Status.STABLE, since = "1.0") +public interface IdGenerator { + + /** + * Generates a new id for given entity + * + * @param entity the entity to be saved + * @return id to be assigned to the entity + */ + Object generateId(Object entity); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java index 0fe112b49..1545709fd 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.schema; +import lombok.Builder; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -43,16 +45,20 @@ public final class NodeDescription { */ private final String primaryLabel; + private final IdDescription idDescription; + private final Map propertiesByFieldName; private final List relationships; - public NodeDescription(String primaryLabel, List properties, + @Builder + private NodeDescription(String primaryLabel, IdDescription idDescription, + List properties, List relationships) { this.primaryLabel = primaryLabel; + this.idDescription = idDescription; this.propertiesByFieldName = properties.stream() - .collect(Collectors.toMap(PropertyDescription::getFieldName, Function - .identity())); + .collect(Collectors.toMap(PropertyDescription::getFieldName, Function.identity())); this.relationships = new ArrayList<>(relationships); } @@ -63,6 +69,10 @@ public final class NodeDescription { return primaryLabel; } + public IdDescription getIdDescription() { + return idDescription; + } + /** * @return The properties of this node */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java index 732ac4049..fd91a3822 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java @@ -18,15 +18,12 @@ */ package org.springframework.data.neo4j.core.schema; -import lombok.Getter; - import org.apiguardian.api.API; /** * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -@Getter public final class PropertyDescription { private final String fieldName; @@ -41,4 +38,12 @@ public final class PropertyDescription { this.fieldName = fieldName; this.propertyName = propertyName; } + + public String getFieldName() { + return fieldName; + } + + public String getPropertyName() { + return propertyName; + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index 511177caf..ea9993e4b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Property; @@ -51,29 +52,39 @@ class Neo4jMappingContextTest { Optional optionalUserNodeDescription = schema.getNodeDescription("User"); assertThat(optionalUserNodeDescription) .isPresent() - .hasValueSatisfying(userNodeDescription -> { - assertThat(userNodeDescription.getProperties()) - .extracting(PropertyDescription::getFieldName) - .containsExactlyInAnyOrder("name", "first_name"); + .hasValueSatisfying(description -> { + assertThat(description.getIdDescription().getIdStrategy()) + .isEqualTo(Id.Strategy.INTERNAL); - assertThat(userNodeDescription.getProperties()) + assertThat(description.getProperties()) + .extracting(PropertyDescription::getFieldName) + .containsExactlyInAnyOrder("id", "name", "first_name"); + + assertThat(description.getProperties()) .extracting(PropertyDescription::getPropertyName) - .containsExactlyInAnyOrder("name", "firstName"); + .containsExactlyInAnyOrder("id", "name", "firstName"); }); Optional optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); assertThat(optionalBikeNodeDescription) .isPresent() - .hasValueSatisfying(bikeNodeDescription -> - assertThat(bikeNodeDescription.getRelationships()) + .hasValueSatisfying(description -> { + assertThat(description.getIdDescription().getIdStrategy()) + .isEqualTo(Id.Strategy.ASSIGNED); + + assertThat(description.getRelationships()) .containsExactlyInAnyOrder( new RelationshipDescription("owner", "User"), - new RelationshipDescription("renter", "User"))); + new RelationshipDescription("renter", "User")); + }); } @Node("User") static class UserNode { + @org.springframework.data.annotation.Id + private long id; + @Relationship(type = "OWNS", inverse = "owner") List bikes; @@ -85,10 +96,11 @@ class Neo4jMappingContextTest { static class BikeNode { + @Id(strategy = Id.Strategy.ASSIGNED) + private String id; + UserNode owner; List renter; } - - } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java index 1defe9f7e..000353487 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java @@ -31,7 +31,8 @@ class SchemaTest { @Test void shouldGetNodeDescription() { - NodeDescription description = new NodeDescription("aLabel", Collections.emptyList(), Collections.emptyList()); + NodeDescription description = NodeDescription.builder().primaryLabel("aLabel") + .properties(Collections.emptyList()).relationships(Collections.emptyList()).build(); Schema schema = new Schema().registerNodeDescription(description); assertThat(schema.getNodeDescription("aLabel")).isPresent().contains(description); From 5ab8facba385bca3678ea9f42141c1bae3f3a723 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 27 Mar 2019 18:27:52 +0000 Subject: [PATCH 045/342] Decouple schema and mapping context. --- .../MappingContextBasedScannerImpl.java | 116 ++++++++++++++++++ .../core/mapping/Neo4jMappingContext.java | 88 ------------- .../data/neo4j/core/schema/Scanner.java | 38 ++++++ .../data/neo4j/core/schema/Schema.java | 9 +- ...> MappingContextBasedScannerImplTest.java} | 6 +- 5 files changed, 159 insertions(+), 98 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/{Neo4jMappingContextTest.java => MappingContextBasedScannerImplTest.java} (93%) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java new file mode 100644 index 000000000..0f11cfe20 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.mapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apiguardian.api.API; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdDescription; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.PropertyDescription; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.neo4j.core.schema.RelationshipDescription; +import org.springframework.data.neo4j.core.schema.Scanner; +import org.springframework.data.neo4j.core.schema.Schema; + +/** + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public class MappingContextBasedScannerImpl implements Scanner { + + private final Neo4jMappingContext neo4jMappingContext; + + public MappingContextBasedScannerImpl(Neo4jMappingContext neo4jMappingContext) { + this.neo4jMappingContext = neo4jMappingContext; + } + + @Override + public Schema scan() { + + final Schema schema = new Schema(); + neo4jMappingContext.getPersistentEntities().forEach(m -> + schema.registerNodeDescription(describeAsNode(m)) + ); + return schema; + } + + private NodeDescription describeAsNode(Neo4jPersistentEntity entity) { + + List properties = new ArrayList<>(); + + // TODO break this up into separate methods. + entity.doWithProperties(new SimplePropertyHandler() { + @Override + public void doWithPersistentProperty(PersistentProperty persistentProperty) { + org.springframework.data.neo4j.core.schema.Property propertyAnnotation = + persistentProperty.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); + + String propertyName = persistentProperty.getName(); + if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty() + && propertyAnnotation.name().trim().length() != 0) { + propertyName = propertyAnnotation.name().trim(); + } + + properties.add(new PropertyDescription(persistentProperty.getName(), propertyName)); + } + }); + + List relationships = new ArrayList<>(); + entity.doWithAssociations(new SimpleAssociationHandler() { + @Override + public void doWithAssociation(Association> association) { + + Neo4jPersistentEntity obverseOwner = neo4jMappingContext + .getPersistentEntity(association.getInverse().getAssociationTargetType()); + + Relationship outgoingRelationship = association.getInverse().findAnnotation(Relationship.class); + String type; + if (outgoingRelationship != null && outgoingRelationship.type() != null) { + type = outgoingRelationship.type(); + } else { + type = association.getInverse().getName(); + } + relationships.add(new RelationshipDescription(type, obverseOwner.getPrimaryLabel())); + } + }); + + final Neo4jPersistentProperty idProperty = entity.getRequiredIdProperty(); + final Optional optionalIdAnnotation = Optional + .ofNullable(AnnotatedElementUtils.findMergedAnnotation(idProperty.getField(), Id.class)); + final IdDescription idDescription = optionalIdAnnotation + .map(idAnnotation -> new IdDescription(idAnnotation.strategy(), idAnnotation.generator())) + .orElseGet(() -> new IdDescription()); + + return NodeDescription.builder() + .primaryLabel(entity.getPrimaryLabel()) + .idDescription(idDescription) + .properties(properties) + .relationships(relationships) + .build(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 0fff776cb..6d8f0ef1a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -18,25 +18,9 @@ */ package org.springframework.data.neo4j.core.mapping; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.SimpleAssociationHandler; -import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.IdDescription; -import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.PropertyDescription; -import org.springframework.data.neo4j.core.schema.Relationship; -import org.springframework.data.neo4j.core.schema.RelationshipDescription; -import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.data.util.TypeInformation; /** @@ -46,8 +30,6 @@ import org.springframework.data.util.TypeInformation; */ public class Neo4jMappingContext extends AbstractMappingContext, Neo4jPersistentProperty> { - private final Schema schema = new Schema(); - /* * (non-Javadoc) * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) @@ -68,74 +50,4 @@ public class Neo4jMappingContext extends AbstractMappingContext - schema.registerNodeDescription(describeAsNode(m)) - ); - } - - public Schema getSchema() { - return schema; - } - - private NodeDescription describeAsNode(Neo4jPersistentEntity entity) { - - List properties = new ArrayList<>(); - - // TODO break this up into separate methods. - entity.doWithProperties(new SimplePropertyHandler() { - @Override - public void doWithPersistentProperty(PersistentProperty persistentProperty) { - org.springframework.data.neo4j.core.schema.Property propertyAnnotation = - persistentProperty.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); - - String propertyName = persistentProperty.getName(); - if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty() - && propertyAnnotation.name().trim().length() != 0) { - propertyName = propertyAnnotation.name().trim(); - } - - properties.add(new PropertyDescription(persistentProperty.getName(), propertyName)); - } - }); - - List relationships = new ArrayList<>(); - entity.doWithAssociations(new SimpleAssociationHandler() { - @Override - public void doWithAssociation(Association> association) { - - Neo4jPersistentEntity obverseOwner = Neo4jMappingContext.this - .getPersistentEntity(association.getInverse().getAssociationTargetType()); - - Relationship outgoingRelationship = association.getInverse().findAnnotation(Relationship.class); - String type; - if (outgoingRelationship != null && outgoingRelationship.type() != null) { - type = outgoingRelationship.type(); - } else { - type = association.getInverse().getName(); - } - relationships.add(new RelationshipDescription(type, obverseOwner.getPrimaryLabel())); - } - }); - - final Neo4jPersistentProperty idProperty = entity.getRequiredIdProperty(); - final Optional optionalIdAnnotation = Optional - .ofNullable(AnnotatedElementUtils.findMergedAnnotation(idProperty.getField(), Id.class)); - final IdDescription idDescription = optionalIdAnnotation - .map(idAnnotation -> new IdDescription(idAnnotation.strategy(), idAnnotation.generator())) - .orElseGet(() -> new IdDescription()); - - return NodeDescription.builder() - .primaryLabel(entity.getPrimaryLabel()) - .idDescription(idDescription) - .properties(properties) - .relationships(relationships) - .build(); - } - - } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java new file mode 100644 index 000000000..c21a1aecb --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.schema; + +import org.apiguardian.api.API; + +/** + * A scanner produces an instance of schema. The default scanner is based on all the information we can get from + * a Spring Mapping context (in our case, the {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext}. + * + * @author Michael J. Simons + */ +@API(status = API.Status.STABLE, since = "1.0") +public interface Scanner { + + /** + * Scans the relevant classes and creates a schema. + * + * @return The new schema. + */ + Schema scan(); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java index 2de859b2e..c87fa0863 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -22,8 +22,6 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Optional; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apiguardian.api.API; @@ -32,16 +30,11 @@ import org.apiguardian.api.API; * * The schema is currently designed to be mutual. */ -@API(status = API.Status.INTERNAL, since = "1.0") +@API(status = API.Status.STABLE, since = "1.0") public final class Schema { private final Map nodeDescriptionsByPrimaryLabel = new HashMap<>(); - // Just added as a reminder. Add adaquate locks - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final Lock read = lock.readLock(); - private final Lock write = lock.writeLock(); - /** * Registers a node description under it's primary label. * diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java similarity index 93% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java index ea9993e4b..017f53111 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java @@ -33,12 +33,13 @@ import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.neo4j.core.schema.PropertyDescription; import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.core.schema.RelationshipDescription; +import org.springframework.data.neo4j.core.schema.Scanner; import org.springframework.data.neo4j.core.schema.Schema; /** * @author Michael J. Simons */ -class Neo4jMappingContextTest { +class MappingContextBasedScannerImplTest { @Test void initializationOfSchemaShouldWork() { @@ -47,7 +48,8 @@ class Neo4jMappingContextTest { neo4jMappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class))); neo4jMappingContext.initialize(); - Schema schema = neo4jMappingContext.getSchema(); + final Scanner scanner = new MappingContextBasedScannerImpl(neo4jMappingContext); + Schema schema = scanner.scan(); Optional optionalUserNodeDescription = schema.getNodeDescription("User"); assertThat(optionalUserNodeDescription) From c62cd13a19b0a2096c19cf21f948f96bfeb89cfb Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 28 Mar 2019 15:39:23 +0100 Subject: [PATCH 046/342] Cleanup readme. --- README.adoc | 2 -- etc/adr/adr-002.adoc | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 etc/adr/adr-002.adoc diff --git a/README.adoc b/README.adoc index 5944d1ba7..d69014e1d 100644 --- a/README.adoc +++ b/README.adoc @@ -28,8 +28,6 @@ So far we have identified the following modules: Those will be reassembled as packages inside Spring Data Neo4j RX. There are no short-term planes to create additional artifacts from those. -NOTE: However, we should discuss if you could try a JDK 11 only project. - [[schema]] ==== Schema diff --git a/etc/adr/adr-002.adoc b/etc/adr/adr-002.adoc new file mode 100644 index 000000000..156958ad2 --- /dev/null +++ b/etc/adr/adr-002.adoc @@ -0,0 +1,21 @@ +== ADR 2: Build on and compile for JDK11 + +=== Status + +proposed + +=== Context + +JDK 8 is deprecated and has left support. +Neo4j 4.0 is already build on JDK 11 and probably compiled to it as well. + +=== Decision + +Not yet made. + +=== Consequences + +* + We would target a modern platform +* + We would benefit from SDK enhancements +* 0 We could benefit from JPMS +* - Adoption rate could be lower. From f0b3172c858305ba37d365f0e21ddd495ad27b60 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 1 Apr 2019 17:03:27 +0200 Subject: [PATCH 047/342] Create NodeManagerFactory, NodeManager and related infrastructure. A NodeManager will be used to create, read, update, delete and query managed objects. It will use a short lived persistence context. This adds the basic skeletons for doing this but especially the infrastructure to allow the following usage: * User configures a driver bean * User provides a NodeManagerFactory using that driver and a given set of classes to be managed * The factory gets augmented to handle transactions and facilitate Spring Datas class scanning mechanism The Spring Boot starter will automatically provide a configured NodeManager instance. --- etc/adr/adr-003.adoc | 21 +++ .../data/neo4j/core/DefaultNodeManager.java | 60 ++++++++ .../data/neo4j/core/Neo4jTemplate.java | 13 +- .../data/neo4j/core/NodeManager.java | 46 ++++++ .../data/neo4j/core/NodeManagerFactory.java | 145 ++++++++++++++++++ ...pl.java => DefaultPersistenceContext.java} | 2 +- ...java => DefaultNeo4jPersistentEntity.java} | 4 +- ...va => DefaultNeo4jPersistentProperty.java} | 4 +- .../MappingContextBasedScannerImpl.java | 11 +- .../core/mapping/Neo4jMappingContext.java | 4 +- .../data/neo4j/core/schema/Scanner.java | 5 +- .../DefaultNeo4jStatementRunnerSupplier.java | 64 -------- ...Holder.java => Neo4jConnectionHolder.java} | 6 +- .../Neo4jSessionSynchronization.java | 20 +-- .../transaction/Neo4jTransactionManager.java | 82 ++++++++-- .../transaction/Neo4jTransactionUtils.java | 91 +++++++++++ ...erSupplier.java => NodeManagerHolder.java} | 23 ++- .../NodeManagerSynchronization.java | 44 ++++++ .../config/EnableNeo4jRepositories.java | 6 +- ...Neo4jRepositoryConfigurationExtension.java | 64 +++++++- .../repository/query/PartTreeNeo4jQuery.java | 7 +- .../query/StringBasedNeo4jQuery.java | 11 +- .../support/Neo4jRepositoryFactory.java | 25 +-- .../support/Neo4jRepositoryFactoryBean.java | 10 +- .../support/NodeManagerFactoryBean.java | 67 ++++++++ .../support/SharedNodeManagerCreator.java | 127 +++++++++++++++ .../support/SimpleNeo4jRepository.java | 8 +- ... => DefaultNeo4JPersistentEntityTest.java} | 2 +- .../MappingContextBasedScannerImplTest.java | 2 +- ...st.java => Neo4jTransactionUtilsTest.java} | 30 ++-- .../neo4j/integration/PersonRepository.java | 2 + .../data/neo4j/integration/RepositoryIT.java | 11 +- 32 files changed, 846 insertions(+), 171 deletions(-) create mode 100644 etc/adr/adr-003.adoc create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/{PersistenceContextImpl.java => DefaultPersistenceContext.java} (91%) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/{Neo4jPersistentEntityImpl.java => DefaultNeo4jPersistentEntity.java} (89%) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/{Neo4jPersistentPropertyImpl.java => DefaultNeo4jPersistentProperty.java} (91%) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/{Neo4jResourceHolder.java => Neo4jConnectionHolder.java} (92%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/{StatementRunnerSupplier.java => NodeManagerHolder.java} (56%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerSynchronization.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SharedNodeManagerCreator.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/{Neo4jPersistentEntityImplTest.java => DefaultNeo4JPersistentEntityTest.java} (96%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/{DefaultNeo4jStatementRunnerSupplierTest.java => Neo4jTransactionUtilsTest.java} (88%) diff --git a/etc/adr/adr-003.adoc b/etc/adr/adr-003.adoc new file mode 100644 index 000000000..45d5a09f8 --- /dev/null +++ b/etc/adr/adr-003.adoc @@ -0,0 +1,21 @@ +== ADR 3: Public classes that are part of internal API only must be final + +=== Status + +accepted, open + +=== Context + +Due to the fact that we are not yet on the module path, we need to have some classes public defined that are not meant +to be part of the public API. + +=== Decision + +Those classes should be marked `@API(status = API.Status.INTERNAL, since = "1.0")` as well as made final to at least +prevent people from inheriting from them. +We need to add architecture rules in jQAssistant. + +=== Consequences + +Potentially problems with some Spring proxies. +Need to be solved on a case by case incident. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java new file mode 100644 index 000000000..19ab19d24 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core; + +import org.apiguardian.api.API; +import org.neo4j.driver.v1.StatementRunner; +import org.neo4j.driver.v1.Transaction; +import org.springframework.data.neo4j.core.context.DefaultPersistenceContext; +import org.springframework.data.neo4j.core.context.PersistenceContext; +import org.springframework.data.neo4j.core.schema.Schema; +import org.springframework.lang.Nullable; + +/** + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +class DefaultNodeManager implements NodeManager { + + private final PersistenceContext persistenceContext = new DefaultPersistenceContext(); + + private final Schema schema; + + private final Neo4jTemplate neo4jTemplate; + + private final Transaction transaction; + + DefaultNodeManager(Schema schema, StatementRunner statementRunner) { + this.schema = schema; + this.neo4jTemplate = new Neo4jTemplate(() -> statementRunner); + this.transaction = statementRunner instanceof Transaction ? (Transaction) statementRunner : null; + } + + @Override + @Nullable + public Transaction getTransaction() { + return transaction; + } + + @Override + public Object executeQuery(String query) { + + return neo4jTemplate.executeQuery(query); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 8a7ffbc0f..fe2a417e7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -18,11 +18,12 @@ */ package org.springframework.data.neo4j.core; +import java.util.function.Supplier; + import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.StatementResult; import org.neo4j.driver.v1.StatementRunner; -import org.springframework.data.neo4j.core.transaction.DefaultNeo4jStatementRunnerSupplier; -import org.springframework.data.neo4j.core.transaction.StatementRunnerSupplier; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; /** * Default implementation of {@link Neo4jOperations}. Uses the Neo4j Java driver to connect to and interact with the @@ -33,10 +34,14 @@ import org.springframework.data.neo4j.core.transaction.StatementRunnerSupplier; */ public class Neo4jTemplate implements Neo4jOperations { - private StatementRunnerSupplier statementRunnerSupplier; + private Supplier statementRunnerSupplier; public Neo4jTemplate(Driver driver) { - this.statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(driver); + this(() -> Neo4jTransactionUtils.retrieveTransactionalStatementRunner(driver)); + } + + Neo4jTemplate(Supplier statementRunnerSupplier) { + this.statementRunnerSupplier = statementRunnerSupplier; } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java new file mode 100644 index 000000000..db7c8aa76 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core; + +import org.apiguardian.api.API; +import org.neo4j.driver.v1.Transaction; +import org.springframework.lang.Nullable; + +/** + * Entry point for creating queries that return managed Nodes. The node manager is not supposed to be kept around + * for longer than necessary. Try to keep your transactions short to avoid memory pressure due to keeping track of + * managed nodes. + * + * @author Michael J. Simons + */ +@API(status = API.Status.STABLE, since = "1.0") +public interface NodeManager { + + /** + * Clears all managed entities and flushes any open state to the underlying storage. + */ + default void flush() { + } + + @Nullable + Transaction getTransaction(); + + @API(status = API.Status.EXPERIMENTAL) + Object executeQuery(String query); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java new file mode 100644 index 000000000..324211dd0 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.neo4j.driver.v1.AccessMode; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.StatementRunner; +import org.springframework.data.neo4j.core.schema.Scanner; +import org.springframework.data.neo4j.core.schema.Schema; +import org.springframework.lang.Nullable; + +/** + * Creates ready to use instances of {@link NodeManager}. + * + * @author Michael J. Simons + */ +@API(status = API.Status.STABLE, since = "1.0") +@Slf4j +public final class NodeManagerFactory { + + /** + * Driver that is used to create new sessions, either by directly invoking it or through Springs transactional utils. + */ + private final Driver driver; + + /** + * The initial set of classes that will be scanned in {@link #initialize()} to build the schema for node managers + * belonging to this factory. + */ + private final Set> initialPersistentClasses; + + /** The scanner used to scan the initial set of persistent classes for creating a schema. */ + private Scanner scanner = new NoopScanner(); + + @Nullable + private Schema schema; + + private Function statementRunnerProvider = sourceDriver -> sourceDriver + .session(AccessMode.WRITE, Collections.emptyList()).beginTransaction(); + + /** + * Creates a new instance of a factory producing {@link NodeManager node managers}. When used in a transactional setup, + * i.e. with the {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}, make sure to use + * the same {@link Driver driver instance} for both the node and the transaction manager. + *

+ * Spring Boots autoconfiguration for SDN RX will make sure that the same driver is used for both concerns. + * + * @param driver The driver used to obtain statement runners from when creating instances of node managers. + * @param initialPersistentClasses The set of classes that should be initially scanned + */ + public NodeManagerFactory(Driver driver, Class... initialPersistentClasses) { + this.driver = driver; + + this.initialPersistentClasses = new HashSet<>(); + Arrays.stream(initialPersistentClasses).forEach(this.initialPersistentClasses::add); + } + + /** + * Creates a new node manager. The returned manager is supposed to have a short lifetime. When used in a Spring setup, + * this method should not called directly by client code. Instead the client code should use an injected instance of + * {@link NodeManager}, which will participate in Springs application based transaction or in JTA transactions. + * + * @return A new node manager + */ + public NodeManager createNodeManager() { + + Objects.requireNonNull(schema, "A schema is required. Did you call #initialize() before using this factory?"); + return new DefaultNodeManager(schema, statementRunnerProvider.apply(driver)); + } + + /** + * Configures a provider for extracting sessions/transactions from a Neo4j driver. This method is not to be called + * from application code and only used by internal API. + * + * @param statementRunnerProvider A required provider of statement runners + */ + @API(status = API.Status.INTERNAL, since = "1.0") + public void setStatementRunnerProvider(Function statementRunnerProvider) { + + Objects.requireNonNull(statementRunnerProvider, "A node manager factory requires a provider of statement runners."); + this.statementRunnerProvider = statementRunnerProvider; + } + + /** + * Configures the scanner used to build a schema for domain objects. This method is not to be called from application + * code and only used by internal API. + * + * @param scanner + */ + @API(status = API.Status.INTERNAL, since = "1.0") + public void setScanner(Scanner scanner) { + + Objects.requireNonNull(scanner, "A node manager factory requires a scanner."); + this.scanner = scanner; + } + + /** + * This initializes this factory and is usually called by Springs infrastructure and only useful as standalone call + * when the node manager factory is used without Spring. + */ + public void initialize() { + + log.info("Initializing schema with {} persistent classes", this.initialPersistentClasses.size()); + this.schema = scanner.scan(Collections.unmodifiableSet(this.initialPersistentClasses)); + } + + /** + * A noop implementation of a Schema scanner, only provided to create instances of a {@link NodeManagerFactory} that + * are in a valid state without booting up a whole context. + */ + private static class NoopScanner implements Scanner { + + @Override + public Schema scan(Collection> persistentClasses) { + return new Schema(); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContextImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContextImpl.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java index 37a47c3bd..3f46405e7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContextImpl.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java @@ -21,5 +21,5 @@ package org.springframework.data.neo4j.core.context; /** * @author Michael J. Simons */ -class PersistenceContextImpl implements PersistenceContext { +public class DefaultPersistenceContext implements PersistenceContext { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index ac522e3c9..f1e4c1b22 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -25,12 +25,12 @@ import org.springframework.data.util.TypeInformation; /** * @author Michael J. Simons */ -class Neo4jPersistentEntityImpl extends BasicPersistentEntity +class DefaultNeo4jPersistentEntity extends BasicPersistentEntity implements Neo4jPersistentEntity { private final String primaryLabel; - Neo4jPersistentEntityImpl(TypeInformation information) { + DefaultNeo4jPersistentEntity(TypeInformation information) { super(information); Node nodeAnnotation = this.findAnnotation(Node.class); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java index 0678b8a51..f772d0009 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java @@ -27,7 +27,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; /** * @author Michael J. Simons */ -class Neo4jPersistentPropertyImpl extends AnnotationBasedPersistentProperty +class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty implements Neo4jPersistentProperty { /** @@ -37,7 +37,7 @@ class Neo4jPersistentPropertyImpl extends AnnotationBasedPersistentProperty owner, SimpleTypeHolder simpleTypeHolder) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java index 0f11cfe20..84624335f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core.mapping; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -50,12 +51,14 @@ public class MappingContextBasedScannerImpl implements Scanner { } @Override - public Schema scan() { + public Schema scan(Collection> persistentClasses) { final Schema schema = new Schema(); - neo4jMappingContext.getPersistentEntities().forEach(m -> - schema.registerNodeDescription(describeAsNode(m)) - ); + persistentClasses.forEach(clazz -> { + Neo4jPersistentEntity m = neo4jMappingContext.getPersistentEntity(clazz); + schema.registerNodeDescription(describeAsNode(m)); + }); + return schema; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 6d8f0ef1a..fdbbcdc70 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -37,7 +37,7 @@ public class Neo4jMappingContext extends AbstractMappingContext Neo4jPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - return new Neo4jPersistentEntityImpl<>(typeInformation); + return new DefaultNeo4jPersistentEntity<>(typeInformation); } /* @@ -48,6 +48,6 @@ public class Neo4jMappingContext extends AbstractMappingContext neo4jPersistentProperties, SimpleTypeHolder simpleTypeHolder) { - return new Neo4jPersistentPropertyImpl(property, neo4jPersistentProperties, simpleTypeHolder); + return new DefaultNeo4jPersistentProperty(property, neo4jPersistentProperties, simpleTypeHolder); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java index c21a1aecb..d961faaf8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.schema; +import java.util.Collection; + import org.apiguardian.api.API; /** @@ -32,7 +34,8 @@ public interface Scanner { /** * Scans the relevant classes and creates a schema. * + * @param persistentClasses The classes to scan * @return The new schema. */ - Schema scan(); + Schema scan(Collection> persistentClasses); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java deleted file mode 100644 index d18445f6a..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 org.springframework.data.neo4j.core.transaction; - -import lombok.RequiredArgsConstructor; - -import java.util.Collections; - -import org.neo4j.driver.v1.AccessMode; -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.StatementRunner; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -/** - * @author Michael J. Simons - */ -@RequiredArgsConstructor -public class DefaultNeo4jStatementRunnerSupplier implements StatementRunnerSupplier { - - private final Driver driver; - - @Override - public StatementRunner get() { - - if (!TransactionSynchronizationManager.isSynchronizationActive()) { - return driver.session(); - } - - // Try existing transaction - Neo4jResourceHolder resourceHolder = (Neo4jResourceHolder) TransactionSynchronizationManager - .getResource(driver); - - if (resourceHolder != null) { - - return resourceHolder.getTransaction(); - } - - // Manually create a new synchronization - resourceHolder = new Neo4jResourceHolder(driver.session(AccessMode.WRITE, Collections.emptyList())); - - TransactionSynchronizationManager.registerSynchronization( - new Neo4jSessionSynchronization(resourceHolder, driver)); - resourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.bindResource(driver, resourceHolder); - - return resourceHolder.getTransaction(); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java index f49a0925a..c330bf909 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java @@ -32,17 +32,17 @@ import org.springframework.util.Assert; * * @author Michael J. Simons */ -public class Neo4jResourceHolder extends ResourceHolderSupport { +public class Neo4jConnectionHolder extends ResourceHolderSupport { private final Session session; private final Transaction transaction; - Neo4jResourceHolder(Session session) { + Neo4jConnectionHolder(Session session) { this(session, TransactionConfig.empty()); } - Neo4jResourceHolder(Session session, TransactionConfig transactionConfig) { + Neo4jConnectionHolder(Session session, TransactionConfig transactionConfig) { this.session = session; this.transaction = this.session.beginTransaction(transactionConfig); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java index 6b6022756..b1883ed8f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java @@ -24,17 +24,17 @@ import org.springframework.transaction.support.TransactionSynchronization; /** * Neo4j specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when - * participating in a non-native Neoj4 transaction, such as a Jta transaction. + * participating in a non-native Neo4j transaction, such as a Jta transaction. */ class Neo4jSessionSynchronization - extends ResourceHolderSynchronization { + extends ResourceHolderSynchronization { - private final Neo4jResourceHolder localResourceHolder; + private final Neo4jConnectionHolder localConnectionHolder; - Neo4jSessionSynchronization(Neo4jResourceHolder resourceHolder, Driver driver) { + Neo4jSessionSynchronization(Neo4jConnectionHolder connectionHolder, Driver driver) { - super(resourceHolder, driver); - this.localResourceHolder = resourceHolder; + super(connectionHolder, driver); + this.localConnectionHolder = connectionHolder; } /* @@ -51,7 +51,7 @@ class Neo4jSessionSynchronization * @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object) */ @Override - protected void processResourceAfterCommit(Neo4jResourceHolder resourceHolder) { + protected void processResourceAfterCommit(Neo4jConnectionHolder resourceHolder) { super.processResourceAfterCommit(resourceHolder); @@ -67,8 +67,8 @@ class Neo4jSessionSynchronization @Override public void afterCompletion(int status) { - if (status == TransactionSynchronization.STATUS_ROLLED_BACK && localResourceHolder.hasActiveTransaction()) { - localResourceHolder.rollback(); + if (status == TransactionSynchronization.STATUS_ROLLED_BACK && localConnectionHolder.hasActiveTransaction()) { + localConnectionHolder.rollback(); } super.afterCompletion(status); @@ -79,7 +79,7 @@ class Neo4jSessionSynchronization * @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object) */ @Override - protected void releaseResource(Neo4jResourceHolder resourceHolder, Object resourceKey) { + protected void releaseResource(Neo4jConnectionHolder resourceHolder, Object resourceKey) { if (resourceHolder.hasActiveSession()) { resourceHolder.close(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index 486d40d79..7a4c1abca 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -18,15 +18,23 @@ */ package org.springframework.data.neo4j.core.transaction; +import lombok.extern.slf4j.Slf4j; + import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.driver.v1.AccessMode; import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.TransactionConfig; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.lang.Nullable; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.InvalidIsolationLevelException; @@ -41,23 +49,44 @@ import org.springframework.transaction.support.TransactionSynchronizationUtils; import org.springframework.util.Assert; /** - * Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. + * Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. This + * transaction manager will synchronize a pair of a native Neo4j session/transaction and one instance of a + * {@link org.springframework.data.neo4j.core.NodeManager} with the transaction. * * @author Michael J. Simons */ -public class Neo4jTransactionManager extends AbstractPlatformTransactionManager { +@Slf4j +public class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements BeanFactoryAware { private final Driver driver; + @Nullable + private NodeManagerFactory nodeManagerFactory; + @API(status = API.Status.STABLE, since = "1.0") public Neo4jTransactionManager(Driver driver) { this.driver = driver; } + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + try { + this.setNodeManagerFactory(beanFactory.getBean(NodeManagerFactory.class)); + } catch (NoSuchBeanDefinitionException ex) { + log.warn("Found no instance of {}", NodeManagerFactory.class); + } + } + + public void setNodeManagerFactory(@Nullable NodeManagerFactory nodeManagerFactory) { + + // TODO Check if the NodeManager uses the same datasource aka driver as we do + this.nodeManagerFactory = nodeManagerFactory; + } + @Override protected Object doGetTransaction() throws TransactionException { - Neo4jResourceHolder resourceHolder = (Neo4jResourceHolder) TransactionSynchronizationManager + Neo4jConnectionHolder resourceHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager .getResource(driver); return new Neo4jTransactionObject(resourceHolder); } @@ -83,13 +112,21 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager try { Session session = this.driver.session(accessMode, bookmarks); - Neo4jResourceHolder resourceHolder = new Neo4jResourceHolder(session, transactionConfig); - transactionObject.setResourceHolder(resourceHolder); + Neo4jConnectionHolder connectionHolder = new Neo4jConnectionHolder(session, transactionConfig); + connectionHolder.setSynchronizedWithTransaction(true); + transactionObject.setResourceHolder(connectionHolder); + TransactionSynchronizationManager.bindResource(this.driver, connectionHolder); - resourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.bindResource(driver, resourceHolder); + if (this.nodeManagerFactory != null) { + NodeManagerHolder nodeManagerHolder = new NodeManagerHolder(this.nodeManagerFactory.createNodeManager()); + nodeManagerHolder.setSynchronizedWithTransaction(true); + transactionObject.setNodeManagerHolder(nodeManagerHolder); + TransactionSynchronizationManager.bindResource(this.nodeManagerFactory, nodeManagerHolder); + } } catch (Exception ex) { + ex.printStackTrace(); throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage())); + } } @@ -134,8 +171,14 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); transactionObject.getRequiredResourceHolder().close(); + transactionObject.getNodeManagerHolder() + .map(NodeManagerHolder::getNodeManager) + .ifPresent(nodeManager -> nodeManager.flush()); TransactionSynchronizationManager.unbindResource(driver); + if (this.nodeManagerFactory != null) { + TransactionSynchronizationManager.unbindResource(this.nodeManagerFactory); + } } private static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) { @@ -171,16 +214,21 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager return extractNeo4jTransaction(status.getTransaction()); } + static class Neo4jTransactionObject implements SmartTransactionObject { - private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jResourceHolder is required but not present. o_O"; + private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jConnectionHolder is required but not present. o_O"; // The resource holder is null when the call to TransactionSynchronizationManager.getResource // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder. // If it is null, there's no existing session / transaction. - private @Nullable Neo4jResourceHolder resourceHolder; + @Nullable + private Neo4jConnectionHolder resourceHolder; - Neo4jTransactionObject(@Nullable Neo4jResourceHolder resourceHolder) { + @Nullable + private NodeManagerHolder nodeManagerHolder; + + Neo4jTransactionObject(@Nullable Neo4jConnectionHolder resourceHolder) { this.resourceHolder = resourceHolder; } @@ -190,23 +238,31 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager * * @param resourceHolder A newly created resource holder with a fresh drivers session, */ - void setResourceHolder(@Nullable Neo4jResourceHolder resourceHolder) { + void setResourceHolder(@Nullable Neo4jConnectionHolder resourceHolder) { this.resourceHolder = resourceHolder; } + void setNodeManagerHolder(@Nullable NodeManagerHolder nodeManagerHolder) { + this.nodeManagerHolder = nodeManagerHolder; + } + /** - * @return {@literal true} if a {@link Neo4jResourceHolder} is set. + * @return {@literal true} if a {@link Neo4jConnectionHolder} is set. */ boolean hasResourceHolder() { return resourceHolder != null; } - Neo4jResourceHolder getRequiredResourceHolder() { + Neo4jConnectionHolder getRequiredResourceHolder() { Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); return resourceHolder; } + Optional getNodeManagerHolder() { + return Optional.ofNullable(nodeManagerHolder); + } + void setRollbackOnly() { getRequiredResourceHolder().setRollbackOnly(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java new file mode 100644 index 000000000..c85cb08e1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.transaction; + +import java.util.Collections; + +import org.neo4j.driver.v1.AccessMode; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.StatementRunner; +import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.NodeManagerFactory; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Internal use only. + */ +public final class Neo4jTransactionUtils { + + public static StatementRunner retrieveTransactionalStatementRunner(Driver driver) { + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + return driver.session(); + } + + // Try existing transaction + Neo4jConnectionHolder connectionHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager + .getResource(driver); + + if (connectionHolder != null) { + + return connectionHolder.getTransaction(); + } + + // Manually create a new synchronization + connectionHolder = new Neo4jConnectionHolder(driver.session(AccessMode.WRITE, Collections.emptyList())); + connectionHolder.setSynchronizedWithTransaction(true); + + TransactionSynchronizationManager.registerSynchronization( + new Neo4jSessionSynchronization(connectionHolder, driver)); + + TransactionSynchronizationManager.bindResource(driver, connectionHolder); + + return connectionHolder.getTransaction(); + } + + public static NodeManager retrieveTransactionalNodeManager(NodeManagerFactory nodeManagerFactory) { + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + return nodeManagerFactory.createNodeManager(); + } + + // Try existing transaction + NodeManagerHolder nodeManagerHolder = (NodeManagerHolder) TransactionSynchronizationManager + .getResource(nodeManagerFactory); + + if (nodeManagerHolder != null) { + + return nodeManagerHolder.getNodeManager(); + } + + // Manually create a new synchronization + nodeManagerHolder = new NodeManagerHolder(nodeManagerFactory.createNodeManager()); + nodeManagerHolder.setSynchronizedWithTransaction(true); + + TransactionSynchronizationManager.registerSynchronization( + new NodeManagerSynchronization(nodeManagerHolder, nodeManagerFactory)); + + TransactionSynchronizationManager.bindResource(nodeManagerFactory, nodeManagerHolder); + + return nodeManagerHolder.getNodeManager(); + } + + private Neo4jTransactionUtils() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java similarity index 56% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java index 4d2a2934c..44d13ff18 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java @@ -18,15 +18,30 @@ */ package org.springframework.data.neo4j.core.transaction; -import java.util.function.Supplier; +import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; /** - * Used for obtaining {@link org.neo4j.driver.v1.Session session bound (standard or reactive)} bound statement runners. + * Dedicated holder for storing a NodeManager inside a transaction. *

* Note: Intended for internal usage only. * * @author Michael J. Simons */ -@FunctionalInterface -public interface StatementRunnerSupplier extends Supplier { +public class NodeManagerHolder extends ResourceHolderSupport { + + @Nullable + private final NodeManager nodeManager; + + public NodeManagerHolder(@Nullable NodeManager nodeManager) { + this.nodeManager = nodeManager; + } + + public NodeManager getNodeManager() { + Assert.state(this.nodeManager != null, "No NodeManager available"); + return this.nodeManager; + } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerSynchronization.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerSynchronization.java new file mode 100644 index 000000000..881f1d565 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerSynchronization.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.core.transaction; + +import org.springframework.data.neo4j.core.NodeManagerFactory; +import org.springframework.transaction.support.ResourceHolderSynchronization; + +/** + * TODO Update Licence header https + * + * @author Michael J. Simons + */ +public class NodeManagerSynchronization + extends ResourceHolderSynchronization { + + private final NodeManagerHolder localNodeManagerHolder; + + NodeManagerSynchronization(NodeManagerHolder nodeManagerHolder, NodeManagerFactory nodeManagerFactory) { + + super(nodeManagerHolder, nodeManagerFactory); + this.localNodeManagerHolder = nodeManagerHolder; + } + + @Override + protected void flushResource(NodeManagerHolder resourceHolder) { + resourceHolder.getNodeManager().flush(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java index 90cdedc3b..364fdc552 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java @@ -31,7 +31,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; import org.springframework.transaction.PlatformTransactionManager; @@ -78,9 +78,9 @@ public @interface EnableNeo4jRepositories { Class repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class; /** - * Configures the name of the {@link Neo4jTemplate} bean to be used with the repositories detected. + * Configures the name of the {@link NodeManager} bean to be used with the repositories detected. */ - String neo4jTemplateRef() default DEFAULT_NEO4J_TEMPLATE_BEAN_NAME; + String nodeManagerFactoryRef() default DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME; /** * Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index cecf03300..6f62637e9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -24,9 +24,12 @@ import java.util.Collections; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.data.neo4j.repository.support.NodeManagerFactoryBean; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; @@ -48,9 +51,14 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati */ static final String GENERATE_BEAN_NAME = "(generated)"; - static final String DEFAULT_NEO4J_TEMPLATE_BEAN_NAME = "neo4jTemplate"; + static final String DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME = "nodeManagerFactory"; static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; + /** + * Holds the name of the shared NodeManagerBean created from the factory with the configured name. + */ + private String generatedNodeManagerBeanName; + /* * (non-Javadoc) * @see org.springframework.data.repository.config14.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() @@ -89,7 +97,57 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati builder.addPropertyValue("transactionManager", source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); - builder.addPropertyReference("neo4jOperations", - source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)); + builder.addPropertyReference("nodeManager", this.generatedNodeManagerBeanName); + } + + @Override + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource config) { + + // Mapping context + AbstractBeanDefinition neo4jMappingContextBeanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(Neo4jMappingContext.class) + .getBeanDefinition(); + String nameOfMappingContextBean = registerWithSourceAndGeneratedBeanName( + neo4jMappingContextBeanDefinition, registry, config); + + // Augmented node manager factory (creating injectable, shared instances of NodeManager) + String nameOfNodeManagerFactory = config.getAttribute("nodeManagerFactoryRef") + .orElse(DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME); + AbstractBeanDefinition sharedSessionCreatorBeanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(NodeManagerFactoryBean.class) + .addConstructorArgReference(nameOfNodeManagerFactory) + .addConstructorArgReference(nameOfMappingContextBean) + .getBeanDefinition(); + this.generatedNodeManagerBeanName = registerWithSourceAndGeneratedBeanName( + sharedSessionCreatorBeanDefinition, registry, config); + } + + /** + * Uses a generated bean name if {@code configuredBeanName} is equal to {@link #GENERATE_BEAN_NAME}, otherwise uses + * the configured bean name to register the new bean. Does not check if there's already a bean under the configured + * name but throws a {@link org.springframework.beans.factory.BeanDefinitionStoreException}. Checks whether a bean is + * already registered under {@code configuredBeanName} in the given {@link BeanDefinitionRegistry} and uses a + * generated name for registering the bean instead. If not, the suggested bean name is used. + * + * @param bean must not be {@literal null}. + * @param registry must not be {@literal null}. + * @param configuredBeanName must not be {@literal null} or empty. + * @param source must not be {@literal null}. + * @return the bean name used for registering the given {@link AbstractBeanDefinition} + * @throws org.springframework.beans.factory.BeanDefinitionStoreException if the BeanDefinition is invalid or if there + * is already a BeanDefinition for the specified bean name * (and we are not allowed to override it) + */ + private static String registerWithGeneratedNameOrUseConfigured(AbstractBeanDefinition bean, + BeanDefinitionRegistry registry, String configuredBeanName, Object source) { + + String registeredBeanName = configuredBeanName; + if (GENERATE_BEAN_NAME.equals(configuredBeanName)) { + registeredBeanName = registerWithSourceAndGeneratedBeanName(bean, registry, source); + } else { + bean.setSource(source); + registry.registerBeanDefinition(configuredBeanName, bean); + } + return registeredBeanName; } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 3fdc80d15..f390a8bf3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -18,7 +18,7 @@ */ package org.springframework.data.neo4j.repository.query; -import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; @@ -26,10 +26,11 @@ import org.springframework.data.repository.query.RepositoryQuery; * Implementation of {@link RepositoryQuery} for derived finder methods. * * @author Gerrit Meier - **/ + * @author Michael J. Simons + */ public class PartTreeNeo4jQuery extends AbstractNeo4jQuery { - public PartTreeNeo4jQuery(Neo4jQueryMethod queryMethod, Neo4jOperations neo4jOperations) { + public PartTreeNeo4jQuery(Neo4jQueryMethod queryMethod, NodeManager nodeManager) { } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index 2e6222516..5ce499521 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -18,7 +18,7 @@ */ package org.springframework.data.neo4j.repository.query; -import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; @@ -26,21 +26,22 @@ import org.springframework.data.repository.query.RepositoryQuery; * Implementation of {@link RepositoryQuery} for String based custom Cypher query. * * @author Gerrit Meier + * @author Michael J. Simons */ public class StringBasedNeo4jQuery extends AbstractNeo4jQuery { private final Neo4jQueryMethod queryMethod; - private final Neo4jOperations neo4jOperations; + private final NodeManager nodeManager; - public StringBasedNeo4jQuery(Neo4jQueryMethod queryMethod, Neo4jOperations neo4jOperations) { + public StringBasedNeo4jQuery(Neo4jQueryMethod queryMethod, NodeManager nodeManager) { this.queryMethod = queryMethod; - this.neo4jOperations = neo4jOperations; + this.nodeManager = nodeManager; } @Override public Object execute(Object[] parameters) { - return neo4jOperations.executeQuery(queryMethod.getAnnotatedQuery()); + return nodeManager.executeQuery(queryMethod.getAnnotatedQuery()); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index c2f8943fd..d7781e0a1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -21,7 +21,7 @@ package org.springframework.data.neo4j.repository.support; import java.lang.reflect.Method; import java.util.Optional; -import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod; import org.springframework.data.neo4j.repository.query.PartTreeNeo4jQuery; @@ -41,13 +41,14 @@ import org.springframework.data.repository.query.RepositoryQuery; * Factory to create {@link Neo4jRepository} instances. * * @author Gerrit Meier + * @author Michael J. Simons */ -class Neo4jRepositoryFactory extends RepositoryFactorySupport { +final class Neo4jRepositoryFactory extends RepositoryFactorySupport { - private final Neo4jOperations neo4jOperations; + private final NodeManager nodeManager; - Neo4jRepositoryFactory(Neo4jOperations neo4jOperations) { - this.neo4jOperations = neo4jOperations; + Neo4jRepositoryFactory(NodeManager nodeManager) { + this.nodeManager = nodeManager; } @Override @@ -57,7 +58,7 @@ class Neo4jRepositoryFactory extends RepositoryFactorySupport { @Override protected Object getTargetRepository(RepositoryInformation metadata) { - return getTargetRepositoryViaReflection(metadata, neo4jOperations); + return getTargetRepositoryViaReflection(metadata, nodeManager); } @Override @@ -73,18 +74,18 @@ class Neo4jRepositoryFactory extends RepositoryFactorySupport { protected Optional getQueryLookupStrategy(Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new Neo4jQueryLookupStrategy(neo4jOperations, evaluationContextProvider)); + return Optional.of(new Neo4jQueryLookupStrategy(nodeManager, evaluationContextProvider)); } private class Neo4jQueryLookupStrategy implements QueryLookupStrategy { - private final Neo4jOperations neo4jOperations; + private final NodeManager nodeManager; private final QueryMethodEvaluationContextProvider evaluationContextProvider; - private Neo4jQueryLookupStrategy(Neo4jOperations neo4jOperations, + private Neo4jQueryLookupStrategy(NodeManager nodeManager, QueryMethodEvaluationContextProvider evaluationContextProvider) { - this.neo4jOperations = neo4jOperations; + this.nodeManager = nodeManager; this.evaluationContextProvider = evaluationContextProvider; } @@ -97,10 +98,10 @@ class Neo4jRepositoryFactory extends RepositoryFactorySupport { Neo4jQueryMethod queryMethod = new Neo4jQueryMethod(method, metadata, factory); if (queryMethod.hasAnnotatedQuery()) { - return new StringBasedNeo4jQuery(queryMethod, neo4jOperations); + return new StringBasedNeo4jQuery(queryMethod, nodeManager); } - return new PartTreeNeo4jQuery(queryMethod, neo4jOperations); + return new PartTreeNeo4jQuery(queryMethod, nodeManager); } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java index 7a6a90080..b452be392 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java @@ -20,7 +20,7 @@ package org.springframework.data.neo4j.repository.support; import java.io.Serializable; -import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; @@ -35,7 +35,7 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF public class Neo4jRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport { - private Neo4jOperations neo4jOperations; + private NodeManager nodeManager; /** * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface. @@ -46,13 +46,13 @@ public class Neo4jRepositoryFactoryBean, S, ID exten super(repositoryInterface); } - public void setNeo4jOperations(Neo4jOperations neo4jOperations) { - this.neo4jOperations = neo4jOperations; + public void setNodeManager(NodeManager nodeManager) { + this.nodeManager = nodeManager; } @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { - return new Neo4jRepositoryFactory(neo4jOperations); + return new Neo4jRepositoryFactory(nodeManager); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java new file mode 100644 index 000000000..0cf0904f2 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.support; + +import org.apiguardian.api.API; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.NodeManagerFactory; +import org.springframework.data.neo4j.core.mapping.MappingContextBasedScannerImpl; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; + +/** + * This is a shim that integrates a {@link NodeManagerFactory} with Spring Datas infrastructure. It takes in the factory + * that needs to be provided by the user and the mapping context provided by our infrastructure. The node manager factory + * is than augment by our {@link Neo4jTransactionUtils} so that it doesn't use unmanaged native transaction, but managed + * native transaction. Furthermore, the default noop scanner is replaced by a scanner based on our Neo4j mapping context. + * + * @author Gerrit Meier + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class NodeManagerFactoryBean implements InitializingBean, FactoryBean { + private final NodeManagerFactory target; + + private final Neo4jMappingContext neo4jMappingContext; + + public NodeManagerFactoryBean(NodeManagerFactory target, Neo4jMappingContext neo4jMappingContext) { + + this.target = target; + this.neo4jMappingContext = neo4jMappingContext; + } + + @Override + public NodeManager getObject() { + return SharedNodeManagerCreator.createSharedNodeManager(this.target); + } + + @Override + public Class getObjectType() { + return NodeManager.class; + } + + @Override + public void afterPropertiesSet() { + target.setStatementRunnerProvider(Neo4jTransactionUtils::retrieveTransactionalStatementRunner); + target.setScanner(new MappingContextBasedScannerImpl(this.neo4jMappingContext)); + target.initialize(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SharedNodeManagerCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SharedNodeManagerCreator.java new file mode 100644 index 000000000..f2ca993d9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SharedNodeManagerCreator.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 org.springframework.data.neo4j.repository.support; + +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.NodeManagerFactory; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.ReflectionUtils; + +/** + * @author Michael J. Simons + */ +@Slf4j +final class SharedNodeManagerCreator { + + private static final Set TRANSACTION_REQUIRING_METHODS; + + static { + Set tmp = new HashSet<>(); + + tmp.add("save"); + tmp.add("delete"); + + TRANSACTION_REQUIRING_METHODS = Collections.unmodifiableSet(tmp); + } + + public static NodeManager createSharedNodeManager(NodeManagerFactory nodeManagerFactory) { + + return (NodeManager) Proxy.newProxyInstance(SharedNodeManagerCreator.class.getClassLoader(), + new Class[] { NodeManager.class }, new SharedNodeManagerInvocationHandler(nodeManagerFactory)); + } + + private static class SharedNodeManagerInvocationHandler implements InvocationHandler, Serializable { + + private final NodeManagerFactory targetFactory; + + SharedNodeManagerInvocationHandler(NodeManagerFactory targetFactory) { + this.targetFactory = targetFactory; + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + String methodName = method.getName(); + switch (methodName) { + case "equals": + // Only consider equal when proxies are identical. + return (proxy == args[0]); + case "hashCode": + // Use hashCode of proxy. + return hashCode(); + case "toString": + // Deliver toString without touching a target Session. + return "Shared Session proxy for target factory [" + this.targetFactory + "]"; + default: + Function methodCall = + targetSession -> ReflectionUtils.invokeMethod(method, targetSession, args); + return invokeInTransaction(methodName, methodCall); + } + } + + private Object invokeInTransaction(String methodName, Function methodCall) { + + // Determine current Session: either the transactional one + // managed by the factory or a temporary one for the given invocation. + NodeManager target = Neo4jTransactionUtils.retrieveTransactionalNodeManager(this.targetFactory); + + if (TRANSACTION_REQUIRING_METHODS.contains(methodName)) { + if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive() + && target.getTransaction() != null)) { + throw new IllegalStateException("No NodeManager with actual transaction available " + + "for current thread - cannot reliably process '" + methodName + "' call"); + } + } + + // Regular Session operations. + boolean isNewSession = false; + if (target == null) { + log.debug("Creating new Session for shared Session invocation"); + target = this.targetFactory.createNodeManager(); + isNewSession = true; + } + + // Invoke method on current Session. + try { + return methodCall.apply(target); + } finally { + if (isNewSession) { + target.flush(); + } + } + } + } + + private SharedNodeManagerCreator() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index ceebc17b0..c84107ab2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -24,7 +24,7 @@ import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -39,10 +39,10 @@ import org.springframework.transaction.annotation.Transactional; @Transactional(readOnly = true) class SimpleNeo4jRepository implements Neo4jRepository { - private final Neo4jOperations neo4jOperations; + private final NodeManager nodeManager; - SimpleNeo4jRepository(Neo4jOperations neo4jOperations) { - this.neo4jOperations = neo4jOperations; + SimpleNeo4jRepository(NodeManager nodeManager) { + this.nodeManager = nodeManager; } @Override diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4JPersistentEntityTest.java similarity index 96% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4JPersistentEntityTest.java index e3f6b6040..0da61ef8c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4JPersistentEntityTest.java @@ -26,7 +26,7 @@ import org.springframework.data.neo4j.core.schema.Node; /** * @author Michael J. Simons */ -public class Neo4jPersistentEntityImplTest { +public class DefaultNeo4JPersistentEntityTest { private Neo4jMappingContext mappingContext = new Neo4jMappingContext(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java index 017f53111..87037f81c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java @@ -49,7 +49,7 @@ class MappingContextBasedScannerImplTest { neo4jMappingContext.initialize(); final Scanner scanner = new MappingContextBasedScannerImpl(neo4jMappingContext); - Schema schema = scanner.scan(); + Schema schema = scanner.scan(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class))); Optional optionalUserNodeDescription = schema.getNodeDescription("User"); assertThat(optionalUserNodeDescription) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java similarity index 88% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java index 3a6266f50..ec74b6ba2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java @@ -54,7 +54,7 @@ import org.springframework.transaction.support.TransactionTemplate; */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class DefaultNeo4jStatementRunnerSupplierTest { +class Neo4jTransactionUtilsTest { @Mock private Driver driver; @@ -90,11 +90,8 @@ class DefaultNeo4jStatementRunnerSupplierTest { @Test void shouldWorkWithoutSynchronizations() { - StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( - driver); - @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = statementRunnerSupplier.get(); + StatementRunner statementRunner = Neo4jTransactionUtils.retrieveTransactionalStatementRunner(driver); verify(driver).session(); verifyNoMoreInteractions(driver, session, transaction); @@ -107,8 +104,6 @@ class DefaultNeo4jStatementRunnerSupplierTest { Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); TransactionTemplate txTemplate = new TransactionTemplate(txManager); - StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( - driver); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -120,7 +115,8 @@ class DefaultNeo4jStatementRunnerSupplierTest { assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = statementRunnerSupplier.get(); + StatementRunner statementRunner = Neo4jTransactionUtils + .retrieveTransactionalStatementRunner(driver); transactionStatus.setRollbackOnly(); } @@ -142,8 +138,6 @@ class DefaultNeo4jStatementRunnerSupplierTest { Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); TransactionTemplate txTemplate = new TransactionTemplate(txManager); - StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( - driver); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -151,7 +145,8 @@ class DefaultNeo4jStatementRunnerSupplierTest { protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { @SuppressWarnings({ "unused" }) - StatementRunner outerStatementRunner = statementRunnerSupplier.get(); + StatementRunner outerStatementRunner = Neo4jTransactionUtils + .retrieveTransactionalStatementRunner(driver); assertThat(outerStatus.isNewTransaction()).isTrue(); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -162,7 +157,8 @@ class DefaultNeo4jStatementRunnerSupplierTest { assertThat(innerStatus.isNewTransaction()).isFalse(); @SuppressWarnings({ "unused" }) - StatementRunner innerStatementRunner = statementRunnerSupplier.get(); + StatementRunner innerStatementRunner = Neo4jTransactionUtils + .retrieveTransactionalStatementRunner(driver); } }); @@ -193,8 +189,6 @@ class DefaultNeo4jStatementRunnerSupplierTest { JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); TransactionTemplate txTemplate = new TransactionTemplate(txManager); - StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( - driver); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -206,7 +200,8 @@ class DefaultNeo4jStatementRunnerSupplierTest { assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = statementRunnerSupplier.get(); + StatementRunner statementRunner = Neo4jTransactionUtils + .retrieveTransactionalStatementRunner(driver); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); } @@ -233,8 +228,6 @@ class DefaultNeo4jStatementRunnerSupplierTest { JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); TransactionTemplate txTemplate = new TransactionTemplate(txManager); - StatementRunnerSupplier statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier( - driver); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -246,7 +239,8 @@ class DefaultNeo4jStatementRunnerSupplierTest { assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = statementRunnerSupplier.get(); + StatementRunner statementRunner = Neo4jTransactionUtils + .retrieveTransactionalStatementRunner(driver); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index cef268283..a51c641cf 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -23,9 +23,11 @@ import java.util.List; import org.neo4j.driver.v1.Record; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; +import org.springframework.transaction.annotation.Transactional; public interface PersonRepository extends Neo4jRepository { + @Transactional @Query("RETURN 1") List customQuery(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 9dc6ef2b8..4468b06e2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -31,8 +31,7 @@ import org.neo4j.driver.v1.Record; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.core.Neo4jOperations; -import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.test.context.ContextConfiguration; @@ -90,15 +89,15 @@ class RepositoryIT { } @Bean - public PlatformTransactionManager transactionManager() { + public NodeManagerFactory nodeManagerFactory(Driver driver) { - return new Neo4jTransactionManager(driver()); + return new NodeManagerFactory(driver, Person.class); } @Bean - public Neo4jOperations neo4jTemplate() { + public PlatformTransactionManager transactionManager(Driver driver) { - return new Neo4jTemplate(driver()); + return new Neo4jTransactionManager(driver); } } } From 8048caaa05474f5cbfe93aaa5a6663c47c29dc5c Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 2 Apr 2019 09:06:39 +0200 Subject: [PATCH 048/342] Upgrade to neo4j-java-driver 2.0 alpha01. --- pom.xml | 2 +- .../data/neo4j/core/DefaultNodeManager.java | 4 +-- .../data/neo4j/core/Neo4jTemplate.java | 6 ++--- .../data/neo4j/core/NodeManager.java | 2 +- .../data/neo4j/core/NodeManagerFactory.java | 9 ++++--- .../transaction/Neo4jConnectionHolder.java | 8 +++--- .../Neo4jSessionSynchronization.java | 2 +- .../transaction/Neo4jTransactionManager.java | 12 +++++---- .../transaction/Neo4jTransactionUtils.java | 25 ++++++++++++++++--- .../Neo4jTransactionManagerTest.java | 17 ++++++------- .../Neo4jTransactionUtilsTest.java | 23 ++++++++--------- .../neo4j/integration/PersonRepository.java | 2 +- .../data/neo4j/integration/RepositoryIT.java | 8 +++--- 13 files changed, 69 insertions(+), 51 deletions(-) diff --git a/pom.xml b/pom.xml index 8a6d1c01b..95d731d6e 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ ${java.version} ${java.version} 2.23.4 - 1.7.2 + 2.0.0-alpha01 3.5.2 UTF-8 1.2.1 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index 19ab19d24..e717f5696 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -19,8 +19,8 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; -import org.neo4j.driver.v1.StatementRunner; -import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.StatementRunner; +import org.neo4j.driver.Transaction; import org.springframework.data.neo4j.core.context.DefaultPersistenceContext; import org.springframework.data.neo4j.core.context.PersistenceContext; import org.springframework.data.neo4j.core.schema.Schema; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index fe2a417e7..816bc6df0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -20,9 +20,9 @@ package org.springframework.data.neo4j.core; import java.util.function.Supplier; -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.StatementResult; -import org.neo4j.driver.v1.StatementRunner; +import org.neo4j.driver.Driver; +import org.neo4j.driver.StatementResult; +import org.neo4j.driver.StatementRunner; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java index db7c8aa76..0a2d51777 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -19,7 +19,7 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; -import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.Transaction; import org.springframework.lang.Nullable; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java index 324211dd0..07b02ca19 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + import lombok.extern.slf4j.Slf4j; import java.util.Arrays; @@ -29,9 +31,8 @@ import java.util.Set; import java.util.function.Function; import org.apiguardian.api.API; -import org.neo4j.driver.v1.AccessMode; -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.StatementRunner; +import org.neo4j.driver.Driver; +import org.neo4j.driver.StatementRunner; import org.springframework.data.neo4j.core.schema.Scanner; import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.lang.Nullable; @@ -63,7 +64,7 @@ public final class NodeManagerFactory { private Schema schema; private Function statementRunnerProvider = sourceDriver -> sourceDriver - .session(AccessMode.WRITE, Collections.emptyList()).beginTransaction(); + .session(defaultSessionParameters(null)).beginTransaction(); /** * Creates a new instance of a factory producing {@link NodeManager node managers}. When used in a transactional setup, diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java index c330bf909..567cd2f37 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java @@ -18,14 +18,14 @@ */ package org.springframework.data.neo4j.core.transaction; -import org.neo4j.driver.v1.Session; -import org.neo4j.driver.v1.Transaction; -import org.neo4j.driver.v1.TransactionConfig; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.TransactionConfig; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; /** - * Neo4j specific {@link ResourceHolderSupport resource holder}, wrapping a {@link org.neo4j.driver.v1.Transaction}. + * Neo4j specific {@link ResourceHolderSupport resource holder}, wrapping a {@link org.neo4j.driver.Transaction}. * {@link Neo4jTransactionManager} binds instances of this class to the thread. *

* Note: Intended for internal usage only. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java index b1883ed8f..062453db7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java @@ -18,7 +18,7 @@ */ package org.springframework.data.neo4j.core.transaction; -import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.Driver; import org.springframework.transaction.support.ResourceHolderSynchronization; import org.springframework.transaction.support.TransactionSynchronization; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index 7a4c1abca..57736c128 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -26,10 +26,10 @@ import java.util.List; import java.util.Optional; import org.apiguardian.api.API; -import org.neo4j.driver.v1.AccessMode; -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.Session; -import org.neo4j.driver.v1.TransactionConfig; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.TransactionConfig; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -106,11 +106,13 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager boolean readOnly = definition.isReadOnly(); AccessMode accessMode = readOnly ? AccessMode.READ : AccessMode.WRITE; List bookmarks = Collections.emptyList(); // TODO Bookmarksupport + String database = ""; // TODO Database selection TransactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); try { - Session session = this.driver.session(accessMode, bookmarks); + Session session = this.driver + .session(t -> t.withDefaultAccessMode(accessMode).withBookmarks(bookmarks).withDatabase(database)); Neo4jConnectionHolder connectionHolder = new Neo4jConnectionHolder(session, transactionConfig); connectionHolder.setSynchronizedWithTransaction(true); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index c85cb08e1..67e2483f2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -19,12 +19,16 @@ package org.springframework.data.neo4j.core.transaction; import java.util.Collections; +import java.util.Optional; +import java.util.function.Consumer; -import org.neo4j.driver.v1.AccessMode; -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.StatementRunner; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.SessionParametersTemplate; +import org.neo4j.driver.StatementRunner; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.NodeManagerFactory; +import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; /** @@ -32,6 +36,19 @@ import org.springframework.transaction.support.TransactionSynchronizationManager */ public final class Neo4jTransactionUtils { + /** + * The default session uses {@link AccessMode#WRITE} and an empty list of bookmarks. + * + * @param database The database to use. May be null, which then designates the default database. + * @return Session parameters to configure the default session used + */ + public static Consumer defaultSessionParameters(@Nullable String database) { + return t -> t + .withDefaultAccessMode(AccessMode.WRITE) + .withBookmarks(Collections.EMPTY_LIST) + .withDatabase(Optional.ofNullable(database).orElse("")); + } + public static StatementRunner retrieveTransactionalStatementRunner(Driver driver) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { @@ -48,7 +65,7 @@ public final class Neo4jTransactionUtils { } // Manually create a new synchronization - connectionHolder = new Neo4jConnectionHolder(driver.session(AccessMode.WRITE, Collections.emptyList())); + connectionHolder = new Neo4jConnectionHolder(driver.session(defaultSessionParameters(null))); connectionHolder.setSynchronizedWithTransaction(true); TransactionSynchronizationManager.registerSynchronization( diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java index 7b4ac3a84..d1ba4a85b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java @@ -20,18 +20,17 @@ package org.springframework.data.neo4j.core.transaction; import static org.mockito.Mockito.*; -import java.util.Collections; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.neo4j.driver.v1.AccessMode; -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.Session; -import org.neo4j.driver.v1.StatementResult; -import org.neo4j.driver.v1.Transaction; -import org.neo4j.driver.v1.TransactionConfig; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.StatementResult; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.TransactionConfig; import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; @@ -54,7 +53,7 @@ class Neo4jTransactionManagerTest { @Test public void triggerCommitCorrectly() { - when(driver.session(AccessMode.WRITE, Collections.emptyList())).thenReturn(session); + when(driver.session(any(Consumer.class))).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); when(transaction.run(anyString())).thenReturn(statementResult); when(session.isOpen()).thenReturn(true); @@ -68,7 +67,7 @@ class Neo4jTransactionManagerTest { txManager.commit(txStatus); - verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + verify(driver).session(any(Consumer.class)); verify(session).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java index ec74b6ba2..3c5630d1a 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java @@ -22,8 +22,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; import static org.mockito.Mockito.*; -import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import javax.transaction.Status; import javax.transaction.UserTransaction; @@ -37,12 +37,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.neo4j.driver.v1.AccessMode; -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.Session; -import org.neo4j.driver.v1.StatementRunner; -import org.neo4j.driver.v1.Transaction; -import org.neo4j.driver.v1.TransactionConfig; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.StatementRunner; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.TransactionConfig; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.transaction.support.TransactionCallbackWithoutResult; @@ -72,7 +71,7 @@ class Neo4jTransactionUtilsTest { AtomicBoolean transactionIsOpen = new AtomicBoolean(true); when(driver.session()).thenReturn(session); - when(driver.session(AccessMode.WRITE, Collections.emptyList())).thenReturn(session); + when(driver.session(any(Consumer.class))).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); doAnswer(invocation -> { @@ -122,7 +121,7 @@ class Neo4jTransactionUtilsTest { } }); - verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + verify(driver).session(any(Consumer.class)); verify(session).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -166,7 +165,7 @@ class Neo4jTransactionUtilsTest { } }); - verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + verify(driver).session(any(Consumer.class)); verify(session).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -209,7 +208,7 @@ class Neo4jTransactionUtilsTest { verify(userTransaction).begin(); - verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + verify(driver).session(any(Consumer.class)); verify(session, times(2)).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -251,7 +250,7 @@ class Neo4jTransactionUtilsTest { verify(userTransaction).begin(); verify(userTransaction).rollback(); - verify(driver).session(AccessMode.WRITE, Collections.emptyList()); + verify(driver).session(any(Consumer.class)); verify(session, times(2)).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index a51c641cf..40a6aa13a 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -20,7 +20,7 @@ package org.springframework.data.neo4j.integration; import java.util.List; -import org.neo4j.driver.v1.Record; +import org.neo4j.driver.Record; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.transaction.annotation.Transactional; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 4468b06e2..28dfb9737 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -24,10 +24,10 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.neo4j.driver.v1.AuthTokens; -import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.GraphDatabase; -import org.neo4j.driver.v1.Record; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Record; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; From 87988851cd9aa17c7ebf1ef1b102e5e71fe58061 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 2 Apr 2019 09:19:26 +0200 Subject: [PATCH 049/342] Use https URLs everywhere. --- .mvn/wrapper/MavenWrapperDownloader.java | 2 +- etc/checkstyle/java-header.txt | 4 ++-- mvnw | 2 +- mvnw.cmd | 4 ++-- pom.xml | 8 ++++---- spring-data-neo4j/pom.xml | 4 ++-- .../data/neo4j/core/DefaultNodeManager.java | 4 ++-- .../springframework/data/neo4j/core/Neo4jOperations.java | 4 ++-- .../springframework/data/neo4j/core/Neo4jTemplate.java | 4 ++-- .../org/springframework/data/neo4j/core/NodeManager.java | 4 ++-- .../data/neo4j/core/NodeManagerFactory.java | 4 ++-- .../neo4j/core/context/DefaultPersistenceContext.java | 4 ++-- .../data/neo4j/core/context/PersistenceContext.java | 4 ++-- .../neo4j/core/mapping/DefaultNeo4jPersistentEntity.java | 4 ++-- .../core/mapping/DefaultNeo4jPersistentProperty.java | 4 ++-- .../core/mapping/MappingContextBasedScannerImpl.java | 4 ++-- .../data/neo4j/core/mapping/Neo4jMappingContext.java | 4 ++-- .../data/neo4j/core/mapping/Neo4jPersistentEntity.java | 4 ++-- .../data/neo4j/core/mapping/Neo4jPersistentProperty.java | 4 ++-- .../org/springframework/data/neo4j/core/schema/Id.java | 4 ++-- .../data/neo4j/core/schema/IdDescription.java | 4 ++-- .../data/neo4j/core/schema/IdGenerator.java | 4 ++-- .../neo4j/core/schema/IllegalSchemaChangeException.java | 4 ++-- .../data/neo4j/core/schema/Neo4jSimpleTypes.java | 4 ++-- .../org/springframework/data/neo4j/core/schema/Node.java | 4 ++-- .../data/neo4j/core/schema/NodeDescription.java | 4 ++-- .../springframework/data/neo4j/core/schema/Property.java | 4 ++-- .../data/neo4j/core/schema/PropertyDescription.java | 4 ++-- .../data/neo4j/core/schema/Relationship.java | 4 ++-- .../data/neo4j/core/schema/RelationshipDescription.java | 4 ++-- .../springframework/data/neo4j/core/schema/Scanner.java | 4 ++-- .../springframework/data/neo4j/core/schema/Schema.java | 4 ++-- .../neo4j/core/transaction/Neo4jConnectionHolder.java | 4 ++-- .../core/transaction/Neo4jSessionSynchronization.java | 4 ++-- .../neo4j/core/transaction/Neo4jTransactionManager.java | 4 ++-- .../neo4j/core/transaction/Neo4jTransactionUtils.java | 4 ++-- .../data/neo4j/core/transaction/NodeManagerHolder.java | 4 ++-- .../core/transaction/NodeManagerSynchronization.java | 6 ++---- .../data/neo4j/repository/Neo4jRepository.java | 4 ++-- .../data/neo4j/repository/ReactiveNeo4jRepository.java | 4 ++-- .../neo4j/repository/config/EnableNeo4jRepositories.java | 4 ++-- .../repository/config/Neo4jRepositoriesRegistrar.java | 4 ++-- .../config/Neo4jRepositoryConfigurationExtension.java | 4 ++-- .../data/neo4j/repository/query/AbstractNeo4jQuery.java | 4 ++-- .../data/neo4j/repository/query/CountQuery.java | 4 ++-- .../data/neo4j/repository/query/ExistsQuery.java | 4 ++-- .../data/neo4j/repository/query/Neo4jQueryMethod.java | 4 ++-- .../data/neo4j/repository/query/PartTreeNeo4jQuery.java | 4 ++-- .../data/neo4j/repository/query/Query.java | 4 ++-- .../neo4j/repository/query/StringBasedNeo4jQuery.java | 4 ++-- .../neo4j/repository/support/Neo4jRepositoryFactory.java | 4 ++-- .../repository/support/Neo4jRepositoryFactoryBean.java | 4 ++-- .../neo4j/repository/support/NodeManagerFactoryBean.java | 4 ++-- .../repository/support/SharedNodeManagerCreator.java | 4 ++-- .../neo4j/repository/support/SimpleNeo4jRepository.java | 4 ++-- .../core/mapping/DefaultNeo4JPersistentEntityTest.java | 4 ++-- .../core/mapping/MappingContextBasedScannerImplTest.java | 4 ++-- .../data/neo4j/core/schema/SchemaTest.java | 4 ++-- .../core/transaction/Neo4jTransactionManagerTest.java | 4 ++-- .../neo4j/core/transaction/Neo4jTransactionUtilsTest.java | 4 ++-- .../springframework/data/neo4j/integration/Person.java | 4 ++-- .../data/neo4j/integration/PersonRepository.java | 4 ++-- .../data/neo4j/integration/RepositoryIT.java | 4 ++-- .../repository/config/EnableNeo4jRepositoriesTests.java | 4 ++-- .../data/neo4j/repository/query/Neo4jQueryMethodTest.java | 4 ++-- .../repository/support/SimpleNeo4jRepositoryTest.java | 4 ++-- spring-data-neo4j/src/test/resources/logback-test.xml | 4 ++-- 67 files changed, 134 insertions(+), 136 deletions(-) diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java index fa4f7b499..2e394d5b3 100755 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -7,7 +7,7 @@ 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 + https://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 diff --git a/etc/checkstyle/java-header.txt b/etc/checkstyle/java-header.txt index ef8f05fad..1197aab68 100644 --- a/etc/checkstyle/java-header.txt +++ b/etc/checkstyle/java-header.txt @@ -1,6 +1,6 @@ ^\Q/*\E$ ^\Q * Copyright (c) 2019 "Neo4j,"\E$ -^\Q * Neo4j Sweden AB [http://neo4j.com]\E$ +^\Q * Neo4j Sweden AB [https://neo4j.com]\E$ ^\Q *\E$ ^\Q * This file is part of Neo4j.\E$ ^\Q *\E$ @@ -8,7 +8,7 @@ ^\Q * you may not use this file except in compliance with the License.\E$ ^\Q * You may obtain a copy of the License at\E$ ^\Q *\E$ -^\Q * http://www.apache.org/licenses/LICENSE-2.0\E$ +^\Q * https://www.apache.org/licenses/LICENSE-2.0\E$ ^\Q *\E$ ^\Q * Unless required by applicable law or agreed to in writing, software\E$ ^\Q * distributed under the License is distributed on an "AS IS" BASIS,\E$ diff --git a/mvnw b/mvnw index 5551fde8e..8b9da3b8b 100755 --- a/mvnw +++ b/mvnw @@ -8,7 +8,7 @@ # "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 +# https://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 diff --git a/mvnw.cmd b/mvnw.cmd index 48363fa60..f954cb294 100755 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -122,7 +122,7 @@ set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central diff --git a/pom.xml b/pom.xml index 95d731d6e..e4db00e4e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ + ${assertj.version} + ${mockito.version} @@ -144,6 +148,7 @@ org.assertj assertj-core ${assertj.version} + test org.junit @@ -156,11 +161,13 @@ org.mockito mockito-core ${mockito.version} + test org.mockito mockito-junit-jupiter ${mockito.version} + test org.neo4j @@ -191,28 +198,17 @@ org.testcontainers junit-jupiter ${testcontainers.version} + test org.testcontainers neo4j ${testcontainers.version} + test - - - spring-libs-snapshot - https://repo.spring.io/libs-snapshot - - - - - spring-plugins-snapshot - https://repo.spring.io/plugins-snapshot - - - diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 92047ee5b..3d590338e 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -92,11 +92,6 @@ org.neo4j.driver neo4j-java-driver - - org.slf4j - slf4j-api - test - org.slf4j slf4j-api From 72996656b603260231caa58b5d7725ce4d1f254a Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 3 Apr 2019 17:45:46 +0200 Subject: [PATCH 053/342] Make Neo4jTemplate close unmanaged sessions. --- .../data/neo4j/core/DefaultNodeManager.java | 13 +-- .../data/neo4j/core/Neo4jTemplate.java | 87 ++++++++++++++++--- .../data/neo4j/core/NodeManagerFactory.java | 26 +++--- .../DefaultTransactionProvider.java | 37 ++++++++ .../ManagedTransactionProvider.java | 67 ++++++++++++++ .../NativeTransactionProvider.java | 44 ++++++++++ .../transaction/Neo4jTransactionUtils.java | 29 ------- .../support/NodeManagerFactoryBean.java | 15 ++-- .../data/neo4j/core/Neo4jTemplateTest.java | 81 +++++++++++++++++ ...va => ManagedTransactionProviderTest.java} | 62 ++++++------- 10 files changed, 367 insertions(+), 94 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/{Neo4jTransactionUtilsTest.java => ManagedTransactionProviderTest.java} (78%) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index e13a8d4b7..15496cfa1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -19,7 +19,6 @@ package org.springframework.data.neo4j.core; import org.apiguardian.api.API; -import org.neo4j.driver.StatementRunner; import org.neo4j.driver.Transaction; import org.springframework.data.neo4j.core.context.DefaultPersistenceContext; import org.springframework.data.neo4j.core.context.PersistenceContext; @@ -34,17 +33,19 @@ class DefaultNodeManager implements NodeManager { private final Schema schema; - private final PersistenceContext persistenceContext; - private final Neo4jTemplate neo4jTemplate; private final Transaction transaction; - DefaultNodeManager(Schema schema, StatementRunner statementRunner) { + private final PersistenceContext persistenceContext; + + DefaultNodeManager(Schema schema, Neo4jTemplate neo4jTemplate, @Nullable Transaction transaction) { + this.schema = schema; + this.neo4jTemplate = neo4jTemplate; + this.transaction = transaction; + this.persistenceContext = new DefaultPersistenceContext(schema); - this.neo4jTemplate = new Neo4jTemplate(() -> statementRunner); - this.transaction = statementRunner instanceof Transaction ? (Transaction) statementRunner : null; } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 322835ad5..cfd18ae50 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -18,12 +18,20 @@ */ package org.springframework.data.neo4j.core; -import java.util.function.Supplier; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; import org.neo4j.driver.StatementRunner; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; +import org.springframework.data.neo4j.core.transaction.ManagedTransactionProvider; +import org.springframework.data.neo4j.core.transaction.NativeTransactionProvider; /** * Default implementation of {@link Neo4jOperations}. Uses the Neo4j Java driver to connect to and interact with the @@ -34,22 +42,81 @@ import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; */ public class Neo4jTemplate implements Neo4jOperations { - private Supplier statementRunnerSupplier; + private final Driver driver; + + private final NativeTransactionProvider transactionProvider; public Neo4jTemplate(Driver driver) { - this(() -> Neo4jTransactionUtils.retrieveTransactionalStatementRunner(driver)); + + this(driver, new ManagedTransactionProvider()); } - Neo4jTemplate(Supplier statementRunnerSupplier) { - this.statementRunnerSupplier = statementRunnerSupplier; + Neo4jTemplate(Driver driver, NativeTransactionProvider transactionProvider) { + + this.driver = driver; + this.transactionProvider = transactionProvider; + } + + AutoCloseableStatementRunner getStatementRunner() { + + StatementRunner statementRunner = transactionProvider.retrieveTransaction(driver) + .map(StatementRunner.class::cast) + .orElseGet(() -> driver.session()); + + return (AutoCloseableStatementRunner) Proxy.newProxyInstance(StatementRunner.class.getClassLoader(), + new Class[] { AutoCloseableStatementRunner.class }, + new AutoCloseableStatementRunnerHandler(statementRunner)); } @Override public Object executeQuery(String query) { - // TODO Let's see whether we can stick with the statementrunner or if we need to differentiate between reactive runner and default. Current 2.0 react version has two complete separate interfaces - StatementRunner statementRunner = statementRunnerSupplier.get(); - StatementResult result = statementRunner.run(query); - return result.list(); + try (AutoCloseableStatementRunner statementRunner = getStatementRunner()) { + + StatementResult result = statementRunner.run(query); + + return result.list(); + } + } + + /** + * Makes a statement runner autoclosable and aware wether it's session or a transaction + */ + interface AutoCloseableStatementRunner extends StatementRunner, AutoCloseable { + + @Override void close(); + } + + private static class AutoCloseableStatementRunnerHandler implements InvocationHandler { + + private final Map cachedHandles = new ConcurrentHashMap<>(); + private final StatementRunner target; + + AutoCloseableStatementRunnerHandler(StatementRunner target) { + this.target = target; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + if ("close".equals(method.getName())) { + if (this.target instanceof Session) { + ((Session) this.target).close(); + } + return null; + } else { + return cachedHandles.computeIfAbsent(method, this::findHandleFor) + .invokeWithArguments(args); + } + } + + MethodHandle findHandleFor(Method method) { + try { + return MethodHandles.publicLookup().unreflect(method).bindTo(target); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } } } + diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java index 77b76cfac..70cb6ddc6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java @@ -18,8 +18,6 @@ */ package org.springframework.data.neo4j.core; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; - import lombok.extern.slf4j.Slf4j; import java.util.Arrays; @@ -28,13 +26,13 @@ import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; -import java.util.function.Function; import org.apiguardian.api.API; import org.neo4j.driver.Driver; -import org.neo4j.driver.StatementRunner; import org.springframework.data.neo4j.core.schema.Scanner; import org.springframework.data.neo4j.core.schema.Schema; +import org.springframework.data.neo4j.core.transaction.DefaultTransactionProvider; +import org.springframework.data.neo4j.core.transaction.NativeTransactionProvider; import org.springframework.lang.Nullable; /** @@ -63,8 +61,10 @@ public final class NodeManagerFactory { @Nullable private Schema schema; - private Function statementRunnerProvider = sourceDriver -> sourceDriver - .session(defaultSessionParameters(null)).beginTransaction(); + /** + * A provider of native transactions, defaults to unmanaged native transactions opened with default parameters. + */ + private NativeTransactionProvider nativeTransactionProvider = new DefaultTransactionProvider(); /** * Creates a new instance of a factory producing {@link NodeManager node managers}. When used in a transactional setup, @@ -77,9 +77,10 @@ public final class NodeManagerFactory { * @param initialPersistentClasses The set of classes that should be initially scanned */ public NodeManagerFactory(Driver driver, Class... initialPersistentClasses) { - this.driver = driver; + this.driver = driver; this.initialPersistentClasses = new HashSet<>(); + Arrays.stream(initialPersistentClasses).forEach(this.initialPersistentClasses::add); } @@ -93,20 +94,21 @@ public final class NodeManagerFactory { public NodeManager createNodeManager() { Objects.requireNonNull(schema, "A schema is required. Did you call #initialize() before using this factory?"); - return new DefaultNodeManager(schema, statementRunnerProvider.apply(driver)); + return new DefaultNodeManager(schema, new Neo4jTemplate(driver, this.nativeTransactionProvider), + this.nativeTransactionProvider.retrieveTransaction(driver).orElse(null)); } /** * Configures a provider for extracting sessions/transactions from a Neo4j driver. This method is not to be called * from application code and only used by internal API. * - * @param statementRunnerProvider A required provider of statement runners + * @param nativeTransactionProvider A required transaction supplier */ @API(status = API.Status.INTERNAL, since = "1.0") - public void setStatementRunnerProvider(Function statementRunnerProvider) { + public void setNativeTransactionProvider(NativeTransactionProvider nativeTransactionProvider) { - Objects.requireNonNull(statementRunnerProvider, "A node manager factory requires a provider of statement runners."); - this.statementRunnerProvider = statementRunnerProvider; + Objects.requireNonNull(nativeTransactionProvider, "A node manager factory requires a transaction provider."); + this.nativeTransactionProvider = nativeTransactionProvider; } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java new file mode 100644 index 000000000..30f84a2ef --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.transaction; + +import java.util.Optional; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Transaction; + +/** + * A transaction provider that opens a new session using default parameters and starting an explicit transaction on that. + * + * @author Michael J. Simons + */ +public class DefaultTransactionProvider implements NativeTransactionProvider { + + @Override + public Optional retrieveTransaction(Driver driver) { + return Optional.of(driver.session(Neo4jTransactionUtils.defaultSessionParameters(null)).beginTransaction()); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java new file mode 100644 index 000000000..8dc2f5e88 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.transaction; + +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + +import java.util.Optional; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Transaction; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * A hook into Springs transaction management to provide managed, native transactions. + * + * @author Michael J. Simons + * @soundtrack Die Toten Hosen - Die Toten Hosen Live - Der Krach der Republik + */ +public class ManagedTransactionProvider implements NativeTransactionProvider { + + /** + * @param driver The driver whos transactions are bound from withing {@link TransactionSynchronizationManager}. + * @return An optional containing a managed transaction or an empty optional if the method hasn't been called inside + * an ongoing Spring transaction + */ + @Override + public Optional retrieveTransaction(final Driver driver) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + return Optional.empty(); + } + + // Try existing transaction + Neo4jConnectionHolder connectionHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager + .getResource(driver); + + if (connectionHolder != null) { + + return Optional.of(connectionHolder.getTransaction()); + } + + // Manually create a new synchronization + connectionHolder = new Neo4jConnectionHolder(driver.session(defaultSessionParameters(null))); + connectionHolder.setSynchronizedWithTransaction(true); + + TransactionSynchronizationManager.registerSynchronization( + new Neo4jSessionSynchronization(connectionHolder, driver)); + + TransactionSynchronizationManager.bindResource(driver, connectionHolder); + return Optional.of(connectionHolder.getTransaction()); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java new file mode 100644 index 000000000..7f702d183 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.transaction; + +import java.util.Optional; + +import org.apiguardian.api.API; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Transaction; + +/** + * A shim to decouple the retrieval of managed and unmanaged native transaction from Spring infractructure. + * + * @author Michael J. Simons + * @soundtrack Die Toten Hosen - Die Toten Hosen Live - Der Krach der Republik + */ +@FunctionalInterface +@API(status = API.Status.INTERNAL, since = "1.0") +public interface NativeTransactionProvider { + + /** + * Retrieves a transaction from the driver. + * + * @param driver + * @return A transaction or an empty optional in case a transaction could not be provided + */ + Optional retrieveTransaction(Driver driver); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index 2a4735846..19df8a236 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -23,9 +23,7 @@ import java.util.Optional; import java.util.function.Consumer; import org.neo4j.driver.AccessMode; -import org.neo4j.driver.Driver; import org.neo4j.driver.SessionParametersTemplate; -import org.neo4j.driver.StatementRunner; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.lang.Nullable; @@ -49,33 +47,6 @@ public final class Neo4jTransactionUtils { .withDatabase(Optional.ofNullable(database).orElse("")); } - public static StatementRunner retrieveTransactionalStatementRunner(Driver driver) { - - if (!TransactionSynchronizationManager.isSynchronizationActive()) { - return driver.session(); - } - - // Try existing transaction - Neo4jConnectionHolder connectionHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager - .getResource(driver); - - if (connectionHolder != null) { - - return connectionHolder.getTransaction(); - } - - // Manually create a new synchronization - connectionHolder = new Neo4jConnectionHolder(driver.session(defaultSessionParameters(null))); - connectionHolder.setSynchronizedWithTransaction(true); - - TransactionSynchronizationManager.registerSynchronization( - new Neo4jSessionSynchronization(connectionHolder, driver)); - - TransactionSynchronizationManager.bindResource(driver, connectionHolder); - - return connectionHolder.getTransaction(); - } - public static NodeManager retrieveTransactionalNodeManager(NodeManagerFactory nodeManagerFactory) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java index 1d87d8445..86f2f7e66 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.repository.support; +import lombok.RequiredArgsConstructor; + import org.apiguardian.api.API; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; @@ -25,6 +27,7 @@ import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.data.neo4j.core.mapping.MappingContextBasedScannerImpl; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.transaction.ManagedTransactionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; /** @@ -37,17 +40,13 @@ import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") +@RequiredArgsConstructor public final class NodeManagerFactoryBean implements InitializingBean, FactoryBean { + private final NodeManagerFactory target; private final Neo4jMappingContext neo4jMappingContext; - public NodeManagerFactoryBean(NodeManagerFactory target, Neo4jMappingContext neo4jMappingContext) { - - this.target = target; - this.neo4jMappingContext = neo4jMappingContext; - } - @Override public NodeManager getObject() { return SharedNodeManagerCreator.createSharedNodeManager(this.target); @@ -60,7 +59,9 @@ public final class NodeManagerFactoryBean implements InitializingBean, FactoryBe @Override public void afterPropertiesSet() { - target.setStatementRunnerProvider(Neo4jTransactionUtils::retrieveTransactionalStatementRunner); + + // Use managed transactions + target.setNativeTransactionProvider(new ManagedTransactionProvider()); target.setScanner(new MappingContextBasedScannerImpl(this.neo4jMappingContext)); target.initialize(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java new file mode 100644 index 000000000..6d97ea6b4 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.springframework.data.neo4j.core.Neo4jTemplate.AutoCloseableStatementRunner; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class Neo4jTemplateTest { + + @Mock + private Driver driver; + @Mock + private Session session; + @Mock + private Transaction transaction; + + @Nested + class AutoCloseableStatementRunnerHandlerTest { + + @Test + public void shouldCallCloseOnSession() { + + when(driver.session()).thenReturn(session); + + // Make template acquire session + Neo4jTemplate neo4jTemplate = new Neo4jTemplate(driver, d -> Optional.empty()); + try (AutoCloseableStatementRunner s = neo4jTemplate.getStatementRunner()) { + s.run("MATCH (n) RETURN n"); + } + + verify(driver).session(); + verify(session).run(any(String.class)); + verify(session).close(); + verifyNoMoreInteractions(driver, session, transaction); + } + + @Test + public void shouldNotInvokeCloseOnTransaction() { + + Neo4jTemplate neo4jTemplate = new Neo4jTemplate(driver, d -> Optional.of(transaction)); + try (AutoCloseableStatementRunner s = neo4jTemplate.getStatementRunner()) { + s.run("MATCH (n) RETURN n"); + } + + verify(transaction).run(any(String.class)); + verifyNoMoreInteractions(driver, session, transaction); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java similarity index 78% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java index 6fbbc092d..f3bd56ab9 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java @@ -18,10 +18,10 @@ */ package org.springframework.data.neo4j.core.transaction; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -39,7 +39,6 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; -import org.neo4j.driver.StatementRunner; import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; import org.springframework.transaction.TransactionStatus; @@ -53,7 +52,7 @@ import org.springframework.transaction.support.TransactionTemplate; */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class Neo4jTransactionUtilsTest { +class ManagedTransactionProviderTest { @Mock private Driver driver; @@ -70,7 +69,6 @@ class Neo4jTransactionUtilsTest { AtomicBoolean sessionIsOpen = new AtomicBoolean(true); AtomicBoolean transactionIsOpen = new AtomicBoolean(true); - when(driver.session()).thenReturn(session); when(driver.session(any(Consumer.class))).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); @@ -89,11 +87,11 @@ class Neo4jTransactionUtilsTest { @Test void shouldWorkWithoutSynchronizations() { - @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = Neo4jTransactionUtils.retrieveTransactionalStatementRunner(driver); + Optional optionalTransaction = new ManagedTransactionProvider().retrieveTransaction(driver); - verify(driver).session(); - verifyNoMoreInteractions(driver, session, transaction); + assertThat(optionalTransaction).isEmpty(); + + verifyZeroInteractions(driver, session, transaction); } @Nested @@ -101,6 +99,8 @@ class Neo4jTransactionUtilsTest { @Test void shouldOpenNewTransaction() { + final ManagedTransactionProvider transactionProvider = new ManagedTransactionProvider(); + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); TransactionTemplate txTemplate = new TransactionTemplate(txManager); @@ -113,9 +113,8 @@ class Neo4jTransactionUtilsTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); - @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = Neo4jTransactionUtils - .retrieveTransactionalStatementRunner(driver); + Optional optionalTransaction = transactionProvider.retrieveTransaction(driver); + assertThat(optionalTransaction).isPresent(); transactionStatus.setRollbackOnly(); } @@ -135,6 +134,8 @@ class Neo4jTransactionUtilsTest { @Test void shouldParticipateInOngoingTransaction() { + final ManagedTransactionProvider transactionProvider = new ManagedTransactionProvider(); + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); TransactionTemplate txTemplate = new TransactionTemplate(txManager); @@ -143,9 +144,8 @@ class Neo4jTransactionUtilsTest { @Override protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { - @SuppressWarnings({ "unused" }) - StatementRunner outerStatementRunner = Neo4jTransactionUtils - .retrieveTransactionalStatementRunner(driver); + Optional outerNativeTransaction = transactionProvider.retrieveTransaction(driver); + assertThat(outerNativeTransaction).isPresent(); assertThat(outerStatus.isNewTransaction()).isTrue(); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -155,9 +155,9 @@ class Neo4jTransactionUtilsTest { assertThat(innerStatus.isNewTransaction()).isFalse(); - @SuppressWarnings({ "unused" }) - StatementRunner innerStatementRunner = Neo4jTransactionUtils - .retrieveTransactionalStatementRunner(driver); + Optional innerNativeTransaction = transactionProvider + .retrieveTransaction(driver); + assertThat(innerNativeTransaction).isPresent(); } }); @@ -186,6 +186,8 @@ class Neo4jTransactionUtilsTest { when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); + final ManagedTransactionProvider transactionProvider = new ManagedTransactionProvider(); + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); TransactionTemplate txTemplate = new TransactionTemplate(txManager); @@ -198,10 +200,9 @@ class Neo4jTransactionUtilsTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = Neo4jTransactionUtils - .retrieveTransactionalStatementRunner(driver); + Optional nativeTransaction = transactionProvider.retrieveTransaction(driver); + assertThat(nativeTransaction).isPresent(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); } }); @@ -225,6 +226,8 @@ class Neo4jTransactionUtilsTest { when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); + final ManagedTransactionProvider transactionProvider = new ManagedTransactionProvider(); + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); TransactionTemplate txTemplate = new TransactionTemplate(txManager); @@ -237,10 +240,9 @@ class Neo4jTransactionUtilsTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - @SuppressWarnings({ "unused" }) - StatementRunner statementRunner = Neo4jTransactionUtils - .retrieveTransactionalStatementRunner(driver); + Optional nativeTransaction = transactionProvider.retrieveTransaction(driver); + assertThat(nativeTransaction).isPresent(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); transactionStatus.setRollbackOnly(); @@ -265,11 +267,11 @@ class Neo4jTransactionUtilsTest { @AfterEach void verifyTransactionSynchronizationManagerState() { - assertTrue(TransactionSynchronizationManager.getResourceMap().isEmpty()); - assertFalse(TransactionSynchronizationManager.isSynchronizationActive()); - assertNull(TransactionSynchronizationManager.getCurrentTransactionName()); - assertFalse(TransactionSynchronizationManager.isCurrentTransactionReadOnly()); - assertNull(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel()); - assertFalse(TransactionSynchronizationManager.isActualTransactionActive()); + assertThat(TransactionSynchronizationManager.getResourceMap().isEmpty()).isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + assertThat(TransactionSynchronizationManager.getCurrentTransactionName()).isNull(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel()).isNull(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); } } From 0d01b7b5b6240fe705b2be9c861979ca0d2cf7bb Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 4 Apr 2019 15:01:41 +0200 Subject: [PATCH 054/342] Prepare selection of databasename. --- .../data/neo4j/core/Neo4jTemplate.java | 2 +- .../data/neo4j/core/NodeManagerFactory.java | 2 +- .../DefaultTransactionProvider.java | 8 ++- .../ManagedTransactionProvider.java | 11 ++-- .../NativeTransactionProvider.java | 5 +- .../transaction/Neo4jConnectionHolder.java | 31 ++++++++-- .../transaction/Neo4jTransactionManager.java | 2 +- .../transaction/Neo4jTransactionUtils.java | 8 ++- .../data/neo4j/core/Neo4jTemplateTest.java | 4 +- .../ManagedTransactionProviderTest.java | 14 +++-- .../Neo4jConnectionHolderTest.java | 60 +++++++++++++++++++ 11 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index cfd18ae50..8e3861927 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -59,7 +59,7 @@ public class Neo4jTemplate implements Neo4jOperations { AutoCloseableStatementRunner getStatementRunner() { - StatementRunner statementRunner = transactionProvider.retrieveTransaction(driver) + StatementRunner statementRunner = transactionProvider.retrieveTransaction(driver, null) .map(StatementRunner.class::cast) .orElseGet(() -> driver.session()); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java index 70cb6ddc6..65bc9fe0f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java @@ -95,7 +95,7 @@ public final class NodeManagerFactory { Objects.requireNonNull(schema, "A schema is required. Did you call #initialize() before using this factory?"); return new DefaultNodeManager(schema, new Neo4jTemplate(driver, this.nativeTransactionProvider), - this.nativeTransactionProvider.retrieveTransaction(driver).orElse(null)); + this.nativeTransactionProvider.retrieveTransaction(driver, null).orElse(null)); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java index 30f84a2ef..95d9da0f3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java @@ -31,7 +31,11 @@ import org.neo4j.driver.Transaction; public class DefaultTransactionProvider implements NativeTransactionProvider { @Override - public Optional retrieveTransaction(Driver driver) { - return Optional.of(driver.session(Neo4jTransactionUtils.defaultSessionParameters(null)).beginTransaction()); + public Optional retrieveTransaction(Driver driver, String database) { + + Transaction nativeTransaction = driver + .session(Neo4jTransactionUtils.defaultSessionParameters(database)) + .beginTransaction(); + return Optional.of(nativeTransaction); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java index 8dc2f5e88..dc4964504 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java @@ -35,33 +35,36 @@ import org.springframework.transaction.support.TransactionSynchronizationManager public class ManagedTransactionProvider implements NativeTransactionProvider { /** + * @param databaseName * @param driver The driver whos transactions are bound from withing {@link TransactionSynchronizationManager}. * @return An optional containing a managed transaction or an empty optional if the method hasn't been called inside * an ongoing Spring transaction */ @Override - public Optional retrieveTransaction(final Driver driver) { + public Optional retrieveTransaction(final Driver driver, String databaseName) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { return Optional.empty(); } + // TODO Check existing transaction wether // Try existing transaction Neo4jConnectionHolder connectionHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager .getResource(driver); if (connectionHolder != null) { - return Optional.of(connectionHolder.getTransaction()); + return Optional.of(connectionHolder.getTransaction(databaseName)); } // Manually create a new synchronization - connectionHolder = new Neo4jConnectionHolder(driver.session(defaultSessionParameters(null))); + connectionHolder = new Neo4jConnectionHolder(databaseName, driver.session(defaultSessionParameters(databaseName))); connectionHolder.setSynchronizedWithTransaction(true); TransactionSynchronizationManager.registerSynchronization( new Neo4jSessionSynchronization(connectionHolder, driver)); TransactionSynchronizationManager.bindResource(driver, connectionHolder); - return Optional.of(connectionHolder.getTransaction()); + return Optional.of(connectionHolder.getTransaction(databaseName)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java index 7f702d183..9718de876 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java @@ -37,8 +37,9 @@ public interface NativeTransactionProvider { /** * Retrieves a transaction from the driver. * - * @param driver + * @param driver The driver to be used to retrieve transactions from or create new ones + * @param databaseName The name of the database to work with * @return A transaction or an empty optional in case a transaction could not be provided */ - Optional retrieveTransaction(Driver driver); + Optional retrieveTransaction(Driver driver, String databaseName); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java index cac4f45d9..21e9e7376 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.transaction; +import java.util.Optional; + import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; @@ -34,22 +36,39 @@ import org.springframework.util.Assert; */ public class Neo4jConnectionHolder extends ResourceHolderSupport { + private final String databaseName; + private final Session session; private final Transaction transaction; - Neo4jConnectionHolder(Session session) { - this(session, TransactionConfig.empty()); + Neo4jConnectionHolder(String databaseName, Session session) { + this(databaseName, session, TransactionConfig.empty()); } - Neo4jConnectionHolder(Session session, TransactionConfig transactionConfig) { - this.session = session; + Neo4jConnectionHolder(String databaseName, Session session, TransactionConfig transactionConfig) { + this.databaseName = databaseName; + this.session = session; this.transaction = this.session.beginTransaction(transactionConfig); } - Transaction getTransaction() { - return transaction; + /** + * Returns the transaction if it has been opened in a session for the requested database. + * + * @param inDatabase + * @return + */ + Transaction getTransaction(String inDatabase) { + + return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? transaction : null; + } + + static boolean namesMapToTheSameDatabase(String name1, String name2) { + String d1 = Optional.ofNullable(name1).orElse(Neo4jTransactionUtils.DEFAULT_DATABASE_NAME); + String d2 = Optional.ofNullable(name2).orElse(Neo4jTransactionUtils.DEFAULT_DATABASE_NAME); + + return d1.equals(d2); } void commit() { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index 92ddcfc6e..0ae41211e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -114,7 +114,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager Session session = this.driver .session(t -> t.withDefaultAccessMode(accessMode).withBookmarks(bookmarks).withDatabase(database)); - Neo4jConnectionHolder connectionHolder = new Neo4jConnectionHolder(session, transactionConfig); + Neo4jConnectionHolder connectionHolder = new Neo4jConnectionHolder(database, session, transactionConfig); connectionHolder.setSynchronizedWithTransaction(true); transactionObject.setResourceHolder(connectionHolder); TransactionSynchronizationManager.bindResource(this.driver, connectionHolder); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index 19df8a236..f326f9ca6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -34,17 +34,19 @@ import org.springframework.transaction.support.TransactionSynchronizationManager */ public final class Neo4jTransactionUtils { + static final String DEFAULT_DATABASE_NAME = ""; + /** * The default session uses {@link AccessMode#WRITE} and an empty list of bookmarks. * - * @param database The database to use. May be null, which then designates the default database. + * @param databaseName The database to use. May be null, which then designates the default database. * @return Session parameters to configure the default session used */ - public static Consumer defaultSessionParameters(@Nullable String database) { + public static Consumer defaultSessionParameters(@Nullable String databaseName) { return t -> t .withDefaultAccessMode(AccessMode.WRITE) .withBookmarks(Collections.EMPTY_LIST) - .withDatabase(Optional.ofNullable(database).orElse("")); + .withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME)); } public static NodeManager retrieveTransactionalNodeManager(NodeManagerFactory nodeManagerFactory) { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java index 6d97ea6b4..8d5b49acc 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java @@ -55,7 +55,7 @@ class Neo4jTemplateTest { when(driver.session()).thenReturn(session); // Make template acquire session - Neo4jTemplate neo4jTemplate = new Neo4jTemplate(driver, d -> Optional.empty()); + Neo4jTemplate neo4jTemplate = new Neo4jTemplate(driver, (d, n) -> Optional.empty()); try (AutoCloseableStatementRunner s = neo4jTemplate.getStatementRunner()) { s.run("MATCH (n) RETURN n"); } @@ -69,7 +69,7 @@ class Neo4jTemplateTest { @Test public void shouldNotInvokeCloseOnTransaction() { - Neo4jTemplate neo4jTemplate = new Neo4jTemplate(driver, d -> Optional.of(transaction)); + Neo4jTemplate neo4jTemplate = new Neo4jTemplate(driver, (d, n) -> Optional.of(transaction)); try (AutoCloseableStatementRunner s = neo4jTemplate.getStatementRunner()) { s.run("MATCH (n) RETURN n"); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java index f3bd56ab9..5d68c0669 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java @@ -54,6 +54,8 @@ import org.springframework.transaction.support.TransactionTemplate; @MockitoSettings(strictness = Strictness.LENIENT) class ManagedTransactionProviderTest { + private String databaseName = Neo4jTransactionUtils.DEFAULT_DATABASE_NAME; + @Mock private Driver driver; @Mock @@ -87,7 +89,7 @@ class ManagedTransactionProviderTest { @Test void shouldWorkWithoutSynchronizations() { - Optional optionalTransaction = new ManagedTransactionProvider().retrieveTransaction(driver); + Optional optionalTransaction = new ManagedTransactionProvider().retrieveTransaction(driver, databaseName); assertThat(optionalTransaction).isEmpty(); @@ -113,7 +115,7 @@ class ManagedTransactionProviderTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); - Optional optionalTransaction = transactionProvider.retrieveTransaction(driver); + Optional optionalTransaction = transactionProvider.retrieveTransaction(driver, databaseName); assertThat(optionalTransaction).isPresent(); transactionStatus.setRollbackOnly(); @@ -144,7 +146,7 @@ class ManagedTransactionProviderTest { @Override protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { - Optional outerNativeTransaction = transactionProvider.retrieveTransaction(driver); + Optional outerNativeTransaction = transactionProvider.retrieveTransaction(driver, databaseName); assertThat(outerNativeTransaction).isPresent(); assertThat(outerStatus.isNewTransaction()).isTrue(); @@ -156,7 +158,7 @@ class ManagedTransactionProviderTest { assertThat(innerStatus.isNewTransaction()).isFalse(); Optional innerNativeTransaction = transactionProvider - .retrieveTransaction(driver); + .retrieveTransaction(driver, databaseName); assertThat(innerNativeTransaction).isPresent(); } }); @@ -200,7 +202,7 @@ class ManagedTransactionProviderTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - Optional nativeTransaction = transactionProvider.retrieveTransaction(driver); + Optional nativeTransaction = transactionProvider.retrieveTransaction(driver, databaseName); assertThat(nativeTransaction).isPresent(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); @@ -240,7 +242,7 @@ class ManagedTransactionProviderTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - Optional nativeTransaction = transactionProvider.retrieveTransaction(driver); + Optional nativeTransaction = transactionProvider.retrieveTransaction(driver, databaseName); assertThat(nativeTransaction).isPresent(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java new file mode 100644 index 000000000..546fa759f --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.transaction; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * @author Michael J. Simons + */ +class Neo4jConnectionHolderTest { + + @Nested + class DatabaseNameComparision { + + @ParameterizedTest + @CsvSource({ + ",", + "," + DEFAULT_DATABASE_NAME, + DEFAULT_DATABASE_NAME + ",", + DEFAULT_DATABASE_NAME + "," + DEFAULT_DATABASE_NAME, + "a,a" + }) + void nameComparisionShouldWorkForNamesTargetingTheSame(String name1, String name2) { + + assertThat(Neo4jConnectionHolder.namesMapToTheSameDatabase(name1, name2)).isTrue(); + } + + @ParameterizedTest + @CsvSource({ + "a,", + ",b", + "a,b" + }) + void nameComparisionShouldWorkForNamesTargetingOther(String name1, String name2) { + + assertThat(Neo4jConnectionHolder.namesMapToTheSameDatabase(name1, name2)).isFalse(); + } + } +} From 5c6303042bf0b6d873e34b53f0ff2b5ed8041e04 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 4 Apr 2019 15:18:42 +0200 Subject: [PATCH 055/342] All all kinds of tests skipped or individually. --- pom.xml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 408edebbf..c5a97a293 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,7 @@ 3.0.0-M1 3.0.0-M1 3.0.0-M2 + 3.0.0-M3 3.0.0-M1 3.0.1 3.7.1 @@ -105,11 +106,14 @@ Californium-SR4 1.3.8 2.2.5 - false 1.7.25 2.1.5.RELEASE 1.10.7 + ${skipTests} + ${skipTests} + ${skipTests} + ${assertj.version} ${mockito.version} @@ -259,6 +263,7 @@ ${jqassistant.version} etc/jqassistant + ${skipArchitectureTests} @@ -282,6 +287,10 @@ maven-surefire-plugin ${maven-surefire-plugin.version} + + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + @@ -345,7 +354,9 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.0.0-M3 + + ${skipIntegrationTests} + From 639086cb7494033900160dfa8786b3ac45bb0a5d Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 4 Apr 2019 15:18:38 +0200 Subject: [PATCH 056/342] Refactor entity tracking. --- .../data/neo4j/core/NodeManager.java | 3 + .../context/DefaultPersistenceContext.java | 66 +++++++++-- .../core/context/PersistenceContext.java | 16 ++- .../tracking/EntityComparisonStrategy.java | 40 ++++--- .../tracking/EntityTrackingStrategy.java | 31 +++++- .../PropertyBasedTrackingStrategy.java | 40 ------- .../DefaultPersistenceContextTest.java | 103 ++++++++++++++++++ .../EntityComparisonStrategyTest.java | 35 +++++- 8 files changed, 264 insertions(+), 70 deletions(-) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/PropertyBasedTrackingStrategy.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java index d5bfe77bc..d3f3e96fc 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -34,6 +34,9 @@ public interface NodeManager { /** * Clears all managed entities and flushes any open state to the underlying storage. + * TODO reflect if we really, really want to have this. + * If an user flushes the node manager, we will end up in a state where we do not have any information about + * the nodes / entities currently processed in the ongoing transaction. */ default void flush() { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java index 2ff1ca57a..54f4fd305 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java @@ -18,8 +18,15 @@ */ package org.springframework.data.neo4j.core.context; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; +import java.util.List; import java.util.Objects; +import java.util.Set; import org.springframework.data.neo4j.core.context.tracking.EntityChangeEvent; import org.springframework.data.neo4j.core.context.tracking.EntityComparisonStrategy; @@ -28,8 +35,12 @@ import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Schema; /** + * TODO explain what the persistence context should do other than being the plural of EntityTrackingStrategy :D + * * @author Michael J. Simons + * @author Gerrit Meier */ +@Slf4j public class DefaultPersistenceContext implements PersistenceContext { /** @@ -39,19 +50,29 @@ public class DefaultPersistenceContext implements PersistenceContext { private final EntityTrackingStrategy entityTrackingStrategy; - public DefaultPersistenceContext(final Schema schema) { + private final Set registeredObjectIds = new HashSet<>(); + public DefaultPersistenceContext(final Schema schema) { this.schema = schema; - //todo choose the right / fitting implementation - this.entityTrackingStrategy = new EntityComparisonStrategy(); + this.entityTrackingStrategy = getEntityTrackingStrategy(); } @Override public void register(Object entity) { Objects.requireNonNull(entity, "Cannot track null-entity!"); + + int identityOfEntity = getIdentityOf(entity); + + if (registeredObjectIds.contains(identityOfEntity)) { + log.info("Object " + entity + " was already registered"); + return; + } + NodeDescription nodeDescription = schema.getNodeDescription(entity.getClass()) - .orElseThrow(() -> new IllegalStateException("Cannot track entity that has no schema entry!")); + .orElseThrow(() -> new IllegalStateException("Cannot track entity that has no schema entry!")); + + registeredObjectIds.add(identityOfEntity); entityTrackingStrategy.track(nodeDescription, entity); } @@ -61,12 +82,41 @@ public class DefaultPersistenceContext implements PersistenceContext { throw new UnsupportedOperationException("Not there yet."); } - private void somethingToGetCalledBeforeSave() { - getDeltasForEachEntity(); + @Override + public Collection getChanges(Object... objects) { + + List entityChanges = new ArrayList<>(); + + for (Object object : objects) { + entityChanges.add(new EntityChanges(object, getEntityChangesFor(object))); + } + + return entityChanges; } - private Collection getDeltasForEachEntity() { - return entityTrackingStrategy.getAggregatedDelta(new Object()); + EntityTrackingStrategy getEntityTrackingStrategy() { + // todo choose the right / fitting implementation + return new EntityComparisonStrategy(); + } + + private int getIdentityOf(Object entity) { + return entityTrackingStrategy.getObjectIdentifier(entity); + } + + private Collection getEntityChangesFor(Object entity) { + return entityTrackingStrategy.getAggregatedEntityChangeEvents(entity); + } + + @Getter + static class EntityChanges { + private final Object entity; + + private final Collection changeEvents; + + EntityChanges(Object entity, Collection changeEvents) { + this.entity = entity; + this.changeEvents = changeEvents; + } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java index 433517b34..39eb41309 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java @@ -18,19 +18,20 @@ */ package org.springframework.data.neo4j.core.context; +import java.util.Collection; + import org.apiguardian.api.API; /** * Represents the state of varies instances being tracked. Those include: - * *

    *
  • All nodes
  • *
  • All relationships
  • *
- * * including properties of them. * * @author Michael J. Simons + * @author Gerrit Meier */ @API(status = API.Status.INTERNAL, since = "1.0") public interface PersistenceContext { @@ -48,4 +49,15 @@ public interface PersistenceContext { * @param managedEntity The entity to remove */ void deregister(Object managedEntity); + + /** + * Calculates the deltas for each registered object and returns them grouped by the object identifying function + * defined in + * {@link org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy#getObjectIdentifier(Object)} or + * one of its implementations. + * + * @param objects to calculate and return changes for + * @return All change events that got registered since registration. + */ + Collection getChanges(Object... objects); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java index 9f8ff245c..36dacc214 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.context.tracking; +import static org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy.*; + import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; @@ -27,11 +29,16 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.PropertyDescription; /** + * Dirty tracking strategy based on property comparison. The strategy can compare two states of the same instance on + * attribute level but uses hashes for collection-like fields. Only fields that are considered to be node properties + * will get compared. + * * @author Gerrit Meier */ public class EntityComparisonStrategy implements EntityTrackingStrategy { @@ -40,12 +47,14 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { @Override public void track(NodeDescription nodeDescription, Object entity) { - statesOfEntities.put(getObjectIdentifier(entity), new EntityState(nodeDescription, entity)); + statesOfEntities.put(getObjectIdentifier(entity), + new EntityState(nodeDescription, entity, getDefaultObjectIdentifier())); } @Override - public Collection getAggregatedDelta(Object entity) { - return statesOfEntities.get(getObjectIdentifier(entity)).computeDelta(entity); + public Collection getAggregatedEntityChangeEvents(Object entity) { + int objectIdentifier = getObjectIdentifier(entity); + return statesOfEntities.get(objectIdentifier).aggregateChanges(entity); } /** @@ -56,17 +65,21 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { private final Map oldState; private final List objectFields; private final Object identifier; + private final Function objectIdentifyingFuction; - EntityState(NodeDescription nodeDescription, Object entity) { + EntityState(NodeDescription nodeDescription, Object entity, Function objectIdentifyingFuction) { Collection properties = nodeDescription.getProperties(); - objectFields = getFieldsFromProperties(entity, properties); - this.identifier = computeIdentifier(entity); + this.objectFields = getFieldsFromProperties(entity, properties); + + this.objectIdentifyingFuction = objectIdentifyingFuction; + this.identifier = this.objectIdentifyingFuction.apply(entity); this.oldState = retrieveStateFrom(entity); } - Set computeDelta(Object newObject) { - if (!sameObject(newObject)) { + Set aggregateChanges(Object newObject) { + + if (!sameObject(objectIdentifyingFuction.apply(newObject))) { throw new IllegalArgumentException("The objects to compare are not the same."); } Set changes = new HashSet<>(); @@ -88,6 +101,7 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { } private List getFieldsFromProperties(Object entity, Collection properties) { + List fields = new ArrayList<>(); Class entityClass = entity.getClass(); @@ -100,6 +114,7 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { } private Map retrieveAllFields(Class entityClass) { + Map classFields = new HashMap<>(); Class classToScan = entityClass; @@ -114,15 +129,12 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { return classFields; } - private boolean sameObject(Object objectToCompare) { - return this.identifier.equals(computeIdentifier(objectToCompare)); - } - - private Object computeIdentifier(Object object) { - return System.identityHashCode(object); + private boolean sameObject(Integer objectIdentifier) { + return this.identifier.equals(objectIdentifier); } private Map retrieveStateFrom(Object entity) { + Map state = new HashMap<>(); for (Field field : objectFields) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java index 3f9f71235..65ef594e7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java @@ -19,21 +19,48 @@ package org.springframework.data.neo4j.core.context.tracking; import java.util.Collection; +import java.util.function.Function; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.schema.NodeDescription; /** + * A tracking strategy is used to determine if an entity has changed it state over time in a transaction. It is also + * responsible to generate and return a complete list of all these changes on an entity's attribute level. + * * @author Gerrit Meier */ @API(status = API.Status.INTERNAL, since = "1.0") public interface EntityTrackingStrategy { + static Function getDefaultObjectIdentifier() { + return System::identityHashCode; + } + + /** + * Register an entity in the entity tracking strategy. {@link NodeDescription} is needed to determine the fields that + * are considered mapping relevant. + * + * @param nodeDescription the "rich" entity's class description. + * @param entity the object that should get tracked. + */ void track(NodeDescription nodeDescription, Object entity); - Collection getAggregatedDelta(Object entity); + /** + * Aggregates all changes that were registered for this entity since the start of tracking. + * + * @param entity for which a summarized collection of {@EntityChangeEvent}s should get created. + * @return all events that occurred since tracking started. + */ + Collection getAggregatedEntityChangeEvents(Object entity); + /** + * Returns an object identifier for a given entity. + * + * @param entity the entity to get an unique identifier for. + * @return hash or something similar that represents the entity. + */ default int getObjectIdentifier(Object entity) { - return System.identityHashCode(entity); + return getDefaultObjectIdentifier().apply(entity); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/PropertyBasedTrackingStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/PropertyBasedTrackingStrategy.java deleted file mode 100644 index d49541462..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/PropertyBasedTrackingStrategy.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.context.tracking; - -import java.util.Collection; - -import org.springframework.data.neo4j.core.schema.NodeDescription; - -/** - * @author Gerrit Meier - */ -public class PropertyBasedTrackingStrategy implements EntityTrackingStrategy { - - @Override - public void track(NodeDescription nodeDescription, Object entity) { - // React to events propagated from the enhanced class and store them - } - - @Override - public Collection getAggregatedDelta(Object entity) { - // Aggregate collected events and return it - return null; - } -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java new file mode 100644 index 000000000..081910711 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.context; + +import static java.util.Collections.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.PropertyDescription; +import org.springframework.data.neo4j.core.schema.Schema; + +/** + * @author Gerrit Meier + */ +class DefaultPersistenceContextTest { + + private PersistenceContext context; + private EntityTrackingStrategy entityTrackingStrategy; + + @BeforeEach + void setup() { + entityTrackingStrategy = mock(EntityTrackingStrategy.class); + when(entityTrackingStrategy.getObjectIdentifier(any())) + .thenAnswer(invocation -> System.identityHashCode(invocation.getArguments()[0])); + + Schema schema = new Schema(); + PropertyDescription valuePropertyDescription = new PropertyDescription("value", "value"); + NodeDescription somethingNodeDescription = NodeDescription.builder() + .properties(singletonList(valuePropertyDescription)).relationships(emptyList()).underlyingClass(Something.class) + .build(); + + schema.registerNodeDescription(somethingNodeDescription); + + // override method to return a verifiable mock + context = new DefaultPersistenceContext(schema) { + @Override + EntityTrackingStrategy getEntityTrackingStrategy() { + return entityTrackingStrategy; + } + }; + } + + @Test + void registerAddsEntityToTrackingStrategy() { + context.register(new Something()); + + verify(entityTrackingStrategy).track(any(), any()); + } + + @Test + void registerTheSameObjectMultipleTimesCallsTrackJustOnce() { + Something entity = new Something(); + context.register(entity); + context.register(entity); + + verify(entityTrackingStrategy).track(any(), any()); + } + + @Test + void registerTwoObjectOfTheSameTypeCallsTrackTwice() { + Something entity1 = new Something(); + Something entity2 = new Something(); + + context.register(entity1); + context.register(entity2); + + verify(entityTrackingStrategy, times(2)).track(any(), any()); + } + + @Test + void triggersDeltaCalculationOnDeltaCall() { + Something entity = new Something(); + context.register(entity); + context.getChanges(entity); + + verify(entityTrackingStrategy).getAggregatedEntityChangeEvents(entity); + } + + class Something { + + String value; + } + +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java index c3be6fa1c..b729e4031 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java @@ -58,7 +58,7 @@ class EntityComparisonStrategyTest { String newValue = "newValue"; something.value = newValue; - Collection changeEvents = strategy.getAggregatedDelta(something); + Collection changeEvents = strategy.getAggregatedEntityChangeEvents(something); EntityChangeEvent changeEvent = changeEvents.iterator().next(); assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName); @@ -75,7 +75,7 @@ class EntityComparisonStrategyTest { String fieldName = "information"; something.information.add("additional entry"); - Collection changeEvents = strategy.getAggregatedDelta(something); + Collection changeEvents = strategy.getAggregatedEntityChangeEvents(something); EntityChangeEvent changeEvent = changeEvents.iterator().next(); assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName); @@ -95,7 +95,7 @@ class EntityComparisonStrategyTest { String fieldName = "information"; - Collection changeEvents = strategy.getAggregatedDelta(something); + Collection changeEvents = strategy.getAggregatedEntityChangeEvents(something); EntityChangeEvent changeEvent = changeEvents.iterator().next(); assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName); @@ -113,13 +113,40 @@ class EntityComparisonStrategyTest { String newValue = "newValue"; something.parentValue = newValue; - Collection changeEvents = strategy.getAggregatedDelta(something); + Collection changeEvents = strategy.getAggregatedEntityChangeEvents(something); EntityChangeEvent changeEvent = changeEvents.iterator().next(); assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName); assertThat(changeEvent.getValue()).isEqualTo(newValue); } + @Test + void trackMultipleObjects() { + EntityComparisonStrategy strategy = new EntityComparisonStrategy(); + Something something1 = new Something("oldValue"); + Something something2 = new Something("oldValue"); + + strategy.track(description, something1); + strategy.track(description, something2); + + String fieldName = "value"; + String newValue1 = "newValue1"; + String newValue2 = "newValue2"; + something1.value = newValue1; + something2.value = newValue2; + + Collection changeEvents1 = strategy.getAggregatedEntityChangeEvents(something1); + Collection changeEvents2 = strategy.getAggregatedEntityChangeEvents(something2); + + EntityChangeEvent changeEvent1 = changeEvents1.iterator().next(); + assertThat(changeEvent1.getPropertyField()).isEqualTo(fieldName); + assertThat(changeEvent1.getValue()).isEqualTo(newValue1); + + EntityChangeEvent changeEvent2 = changeEvents2.iterator().next(); + assertThat(changeEvent2.getPropertyField()).isEqualTo(fieldName); + assertThat(changeEvent2.getValue()).isEqualTo(newValue2); + } + class ParentClass { String parentValue; } From 1b98216287e86b23c102a68f905fca1119c97858 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 4 Apr 2019 15:26:08 +0200 Subject: [PATCH 057/342] Fix type signature in NodeManager's save method. --- .../org/springframework/data/neo4j/core/DefaultNodeManager.java | 2 +- .../java/org/springframework/data/neo4j/core/NodeManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index 15496cfa1..094b6c28a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -61,7 +61,7 @@ class DefaultNodeManager implements NodeManager { } @Override - public Object save(Object entityWithUnknownState) { + public T save(T entityWithUnknownState) { // TODO if already registered, here or in the context? this.persistenceContext.register(entityWithUnknownState); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java index d3f3e96fc..063291d47 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -56,7 +56,7 @@ public interface NodeManager { * @param entityWithUnknownState An entity that is either managed or unmanaged * @return A managed object */ - T save(T entityWithUnknownState); + T save(T entityWithUnknownState); /** * Delete an object from the persistence context and the underlying store. From 4ecdc36a0e15fc158b7a41a77f053b3bee2522ab Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 4 Apr 2019 16:22:28 +0200 Subject: [PATCH 058/342] Unify wording in PersistenceContext. --- .../neo4j/core/context/DefaultPersistenceContext.java | 9 +++------ .../data/neo4j/core/context/PersistenceContext.java | 2 +- .../core/context/DefaultPersistenceContextTest.java | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java index 54f4fd305..96a9f2fd7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java @@ -83,12 +83,13 @@ public class DefaultPersistenceContext implements PersistenceContext { } @Override - public Collection getChanges(Object... objects) { + public Collection getEntityChanges(Object... objects) { List entityChanges = new ArrayList<>(); for (Object object : objects) { - entityChanges.add(new EntityChanges(object, getEntityChangesFor(object))); + Collection changeEvents = entityTrackingStrategy.getAggregatedEntityChangeEvents(object); + entityChanges.add(new EntityChanges(object, changeEvents)); } return entityChanges; @@ -103,10 +104,6 @@ public class DefaultPersistenceContext implements PersistenceContext { return entityTrackingStrategy.getObjectIdentifier(entity); } - private Collection getEntityChangesFor(Object entity) { - return entityTrackingStrategy.getAggregatedEntityChangeEvents(entity); - } - @Getter static class EntityChanges { private final Object entity; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java index 39eb41309..17320a316 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java @@ -59,5 +59,5 @@ public interface PersistenceContext { * @param objects to calculate and return changes for * @return All change events that got registered since registration. */ - Collection getChanges(Object... objects); + Collection getEntityChanges(Object... objects); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java index 081910711..12d242095 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java @@ -90,7 +90,7 @@ class DefaultPersistenceContextTest { void triggersDeltaCalculationOnDeltaCall() { Something entity = new Something(); context.register(entity); - context.getChanges(entity); + context.getEntityChanges(entity); verify(entityTrackingStrategy).getAggregatedEntityChangeEvents(entity); } From d89607443a2cbeb9dcc64fc72dfc75d5fcab7b78 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 4 Apr 2019 17:34:28 +0200 Subject: [PATCH 059/342] Introduce untrack in EntityTrackerStrategy. --- .../context/DefaultPersistenceContext.java | 8 +++++++- .../tracking/EntityComparisonStrategy.java | 5 +++++ .../tracking/EntityTrackingStrategy.java | 8 ++++++++ .../DefaultPersistenceContextTest.java | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java index 96a9f2fd7..60e6cb9d0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java @@ -79,7 +79,13 @@ public class DefaultPersistenceContext implements PersistenceContext { @Override public void deregister(Object managedEntity) { - throw new UnsupportedOperationException("Not there yet."); + int identityOfEntity = getIdentityOf(managedEntity); + if (!registeredObjectIds.contains(identityOfEntity)) { + log.warn("Cannot deregister " + managedEntity + " because it were never registered"); + return; + } + + entityTrackingStrategy.untrack(managedEntity); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java index 36dacc214..f8eac2f76 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java @@ -51,6 +51,11 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { new EntityState(nodeDescription, entity, getDefaultObjectIdentifier())); } + @Override + public void untrack(Object entity) { + statesOfEntities.remove(getObjectIdentifier(entity)); + } + @Override public Collection getAggregatedEntityChangeEvents(Object entity) { int objectIdentifier = getObjectIdentifier(entity); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java index 65ef594e7..44742e6ce 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java @@ -46,6 +46,14 @@ public interface EntityTrackingStrategy { */ void track(NodeDescription nodeDescription, Object entity); + /** + * Remove an entity from tracking. This method should get called after an entity was deleted to have a clean change + * history state. + * + * @param entity to get removed from entity tracking + */ + void untrack(Object entity); + /** * Aggregates all changes that were registered for this entity since the start of tracking. * diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java index 12d242095..a0b00b743 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java @@ -95,6 +95,25 @@ class DefaultPersistenceContextTest { verify(entityTrackingStrategy).getAggregatedEntityChangeEvents(entity); } + @Test + void deregisterRemovesEntityFromTracking() { + Something entity = new Something(); + context.register(entity); + + context.deregister(entity); + + verify(entityTrackingStrategy).untrack(entity); + } + + @Test + void deregisterUnknownEntityDoesNotCallUntrack() { + Something entity = new Something(); + + context.deregister(entity); + + verify(entityTrackingStrategy, never()).untrack(entity); + } + class Something { String value; From 29e872cc188f505d99cfd62f4062897e5f5f94d4 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 10 Apr 2019 17:48:33 +0200 Subject: [PATCH 060/342] Replace Neo4jTemplate with Neo4jClient This commit brings * Neo4Client * ReactiveNeo4jClient both with complete implementations, tests and documentation. Based on those clients the work on mapping code can be continued. --- README.adoc | 4 +- etc/adr/adr-003.adoc | 3 +- etc/jqassistant/api.adoc | 19 + etc/jqassistant/index.adoc | 9 +- .../data/neo4j/core/DefaultNeo4jClient.java | 269 ++++++++++++ .../data/neo4j/core/DefaultNodeManager.java | 8 +- .../core/DefaultReactiveNeo4jClient.java | 272 ++++++++++++ .../data/neo4j/core/NamedParameters.java | 78 ++++ .../data/neo4j/core/Neo4jClient.java | 216 ++++++++++ .../data/neo4j/core/Neo4jOperations.java | 32 -- .../data/neo4j/core/Neo4jTemplate.java | 122 ------ .../data/neo4j/core/NodeManagerFactory.java | 27 +- .../data/neo4j/core/ReactiveNeo4jClient.java | 153 +++++++ .../core/SingleValueMappingFunction.java | 54 +++ .../context/tracking/EntityChangeEvent.java | 3 +- .../MappingContextBasedScannerImpl.java | 2 +- .../schema/IllegalSchemaChangeException.java | 2 +- .../neo4j/core/schema/Neo4jSimpleTypes.java | 27 ++ .../DefaultTransactionProvider.java | 41 -- .../ManagedTransactionProvider.java | 70 --- .../NativeTransactionProvider.java | 45 -- .../transaction/Neo4jConnectionHolder.java | 2 +- .../transaction/Neo4jTransactionUtils.java | 66 ++- .../core/transaction/NodeManagerHolder.java | 4 +- .../support/NodeManagerFactoryBean.java | 9 +- .../data/neo4j/core/NamedParametersTest.java | 97 +++++ .../data/neo4j/core/Neo4jClientTest.java | 403 ++++++++++++++++++ .../data/neo4j/core/Neo4jTemplateTest.java | 81 ---- .../neo4j/core/ReactiveNeo4jClientTest.java | 357 ++++++++++++++++ .../core/SingleValueMappingFunctionTest.java | 103 +++++ .../neo4j/core/TransactionHandlingTest.java | 218 ++++++++++ .../Neo4jTransactionManagerTest.java | 8 +- ...st.java => Neo4jTransactionUtilsTest.java} | 22 +- .../neo4j/integration/PersonRepository.java | 10 +- .../data/neo4j/integration/RepositoryIT.java | 8 +- 35 files changed, 2378 insertions(+), 466 deletions(-) create mode 100644 etc/jqassistant/api.adoc create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/{ManagedTransactionProviderTest.java => Neo4jTransactionUtilsTest.java} (88%) diff --git a/README.adoc b/README.adoc index 48c1c61d0..9f21575ab 100644 --- a/README.adoc +++ b/README.adoc @@ -105,7 +105,7 @@ Specifying `inverse` removes ambiguity here. === "Rich" relationships -There should be no means of using a relationship as aggregate root in SDN RX (like it is today the case with `@RelationshipEntity`). +There should be no means of using a relationship as aggregate root in SDN-RX (like it is today the case with `@RelationshipEntity`). Instead we suggest that properties of relationships are mapped to POJOs. This has the following requirements: On the node representing the start node, the relationship (either 1:1 or 1:n) has to be annotated with `@Relationship` specifying the type of the end node like this: @@ -304,7 +304,7 @@ and executes it through the `neo4jOperations` (`Neo4jTemplate`) class. === Dirty tracking -We considered several approaches of dirty tracking in SDN-rx: +We considered several approaches of dirty tracking in SDN-RX: . No dirty tracking at all. _Not an option when it comes to relationships._ diff --git a/etc/adr/adr-003.adoc b/etc/adr/adr-003.adoc index 45d5a09f8..08d0cd6f8 100644 --- a/etc/adr/adr-003.adoc +++ b/etc/adr/adr-003.adoc @@ -2,7 +2,7 @@ === Status -accepted, open +accepted === Context @@ -13,7 +13,6 @@ to be part of the public API. Those classes should be marked `@API(status = API.Status.INTERNAL, since = "1.0")` as well as made final to at least prevent people from inheriting from them. -We need to add architecture rules in jQAssistant. === Consequences diff --git a/etc/jqassistant/api.adoc b/etc/jqassistant/api.adoc new file mode 100644 index 000000000..0d49f644a --- /dev/null +++ b/etc/jqassistant/api.adoc @@ -0,0 +1,19 @@ +[[api:Default]] +[role=group,includesConstraints="api:internal"] + +=== Internal API + +See ADR-003. + +[[api:internal]] +[source,cypher,role=constraint] +.Public classes that are only part of internal API must be final +---- +MATCH (c:Class), + (c) - [:ANNOTATED_BY] -> (a) - [:OF_TYPE] -> (t:Type {fqn: 'org.apiguardian.api.API'}), + (a) - [:HAS] -> ({name: 'status'}) - [:IS] -> (s) +WHERE s.signature =~ ".*INTERNAL" + AND c.visibility = 'public' + AND not exists(c.final) +RETURN c.name +---- diff --git a/etc/jqassistant/index.adoc b/etc/jqassistant/index.adoc index fcd328d58..d66d080d8 100644 --- a/etc/jqassistant/index.adoc +++ b/etc/jqassistant/index.adoc @@ -3,10 +3,17 @@ The following rules are checked during a build: [[default]] -[role=group,includesGroups="naming:Default,structure:Default"] +[role=group,includesGroups="api:Default,naming:Default,structure:Default"] +- <> - <> - <> +== API + +Ensure that we publish our API in a sane and consistent way. + +include::api.adoc[] + == Naming things include::naming.adoc[] diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java new file mode 100644 index 000000000..b177013a7 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import static java.util.stream.Collectors.*; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.StatementResult; +import org.neo4j.driver.StatementRunner; +import org.neo4j.driver.summary.ResultSummary; + +/** + * Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to and interact with the database. + * TODO Micrometer hooks for statement results... + * + * @author Gerrit Meier + * @author Michael J. Simons + */ +class DefaultNeo4jClient implements Neo4jClient { + + private final Driver driver; + + DefaultNeo4jClient(Driver driver) { + + this.driver = driver; + } + + AutoCloseableStatementRunner getStatementRunner(final String targetDatabase) { + + StatementRunner statementRunner = retrieveTransaction(driver, targetDatabase) + .map(StatementRunner.class::cast) + .orElseGet(() -> driver.session(defaultSessionParameters(targetDatabase))); + + return (AutoCloseableStatementRunner) Proxy.newProxyInstance(StatementRunner.class.getClassLoader(), + new Class[] { AutoCloseableStatementRunner.class }, + new AutoCloseableStatementRunnerHandler(statementRunner)); + } + + /** + * Makes a statement runner autoclosable and aware wether it's session or a transaction + */ + interface AutoCloseableStatementRunner extends StatementRunner, AutoCloseable { + + @Override void close(); + } + + static class AutoCloseableStatementRunnerHandler implements InvocationHandler { + + private final Map cachedHandles = new ConcurrentHashMap<>(); + private final StatementRunner target; + + AutoCloseableStatementRunnerHandler(StatementRunner target) { + this.target = target; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + if ("close".equals(method.getName())) { + if (this.target instanceof Session) { + ((Session) this.target).close(); + } + return null; + } else { + return cachedHandles.computeIfAbsent(method, this::findHandleFor) + .invokeWithArguments(args); + } + } + + MethodHandle findHandleFor(Method method) { + try { + return MethodHandles.publicLookup().unreflect(method).bindTo(target); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + // Below are all the implementations (methods and classes) as defined by the contracts of Neo4jClient + + @Override + public RunnableSpec newQuery(String cypher) { + return newQuery(() -> cypher); + } + + @Override + public RunnableSpec newQuery(Supplier cypherSupplier) { + return new DefaultRunnableSpec(cypherSupplier); + } + + @Override + public OngoingDelegation delegateTo(Function> callback) { + return new DefaultRunnableDelegation<>(callback); + } + + @RequiredArgsConstructor + class DefaultRunnableSpec implements RunnableSpec { + + private final Supplier cypherSupplier; + + private final NamedParameters parameters = new NamedParameters(); + + private String targetDatabase; + + @Override + public RunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) { + this.targetDatabase = targetDatabase; + return this; + } + + @RequiredArgsConstructor + class DefaultOngoingBindSpec implements OngoingBindSpec { + + private final T value; + + @Override + public RunnableSpecTightToDatabase to(String name) { + + DefaultRunnableSpec.this.parameters.add(name, value); + return DefaultRunnableSpec.this; + } + + @Override + public RunnableSpecTightToDatabase with(Function> binder) { + return bindAll(binder.apply(value)); + } + } + + @Override + public OngoingBindSpec bind(Object value) { + return new DefaultOngoingBindSpec(value); + } + + @Override + public RunnableSpecTightToDatabase bindAll(Map newParameters) { + this.parameters.addAll(newParameters); + return this; + } + + @Override + public MappingSpec, Collection, T> fetchAs(Class targetClass) { + + return new DefaultRecordFetchSpec(this.targetDatabase, this.cypherSupplier, this.parameters, + new SingleValueMappingFunction(targetClass)); + } + + @Override + public RecordFetchSpec>, Collection>, Map> fetch() { + + return new DefaultRecordFetchSpec<>( + this.targetDatabase, + this.cypherSupplier, + this.parameters, Record::asMap); + } + + @Override + public ResultSummary run() { + + try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { + StatementResult result = statementRunner.run(cypherSupplier.get(), parameters.get()); + return result.consume(); + } + } + } + + @AllArgsConstructor + class DefaultRecordFetchSpec + implements RecordFetchSpec, Collection, T>, MappingSpec, Collection, T> { + + private final String targetDatabase; + + private final Supplier cypherSupplier; + + private final NamedParameters parameters; + + private Function mappingFunction; + + @Override + public RecordFetchSpec, Collection, T> mappedBy( + @SuppressWarnings("HiddenField") Function mappingFunction) { + + this.mappingFunction = mappingFunction; + return this; + } + + @Override + public Optional one() { + + try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { + StatementResult result = statementRunner.run(cypherSupplier.get(), parameters.get()); + return result.hasNext() ? Optional.of(mappingFunction.apply(result.single())) : Optional.empty(); + } + } + + @Override + public Optional first() { + + try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { + StatementResult result = statementRunner.run(cypherSupplier.get(), parameters.get()); + return result.stream().map(mappingFunction).findFirst(); + } + } + + @Override + public Collection all() { + + try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { + StatementResult result = statementRunner.run(cypherSupplier.get(), parameters.get()); + return result.stream().map(mappingFunction).collect(toList()); + } + } + } + + @AllArgsConstructor + @RequiredArgsConstructor + class DefaultRunnableDelegation implements RunnableDelegation, OngoingDelegation { + + private final Function> callback; + + private String targetDatabase; + + @Override + public RunnableDelegation in(@SuppressWarnings("HiddenField") String targetDatabase) { + this.targetDatabase = targetDatabase; + return this; + } + + @Override + public Optional run() { + try (AutoCloseableStatementRunner statementRunner = getStatementRunner(targetDatabase)) { + return callback.apply(statementRunner); + } + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index 094b6c28a..b1816d08f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -33,16 +33,16 @@ class DefaultNodeManager implements NodeManager { private final Schema schema; - private final Neo4jTemplate neo4jTemplate; + private final Neo4jClient neo4jClient; private final Transaction transaction; private final PersistenceContext persistenceContext; - DefaultNodeManager(Schema schema, Neo4jTemplate neo4jTemplate, @Nullable Transaction transaction) { + DefaultNodeManager(Schema schema, Neo4jClient neo4jClient, @Nullable Transaction transaction) { this.schema = schema; - this.neo4jTemplate = neo4jTemplate; + this.neo4jClient = neo4jClient; this.transaction = transaction; this.persistenceContext = new DefaultPersistenceContext(schema); @@ -57,7 +57,7 @@ class DefaultNodeManager implements NodeManager { @Override public Object executeQuery(String query) { - return neo4jTemplate.executeQuery(query); + return neo4jClient.newQuery(query).fetch().one(); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java new file mode 100644 index 000000000..959232fb1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxStatementRunner; +import org.neo4j.driver.summary.ResultSummary; +import org.reactivestreams.Publisher; +import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; +import org.springframework.data.neo4j.core.Neo4jClient.OngoingBindSpec; +import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; + +/** + * Reactive variant of the {@link Neo4jClient}. + * + * TODO Reactive transaction management is pretty obvious still open + * + * @author Michael J. Simons + * @since 1.0 + * @soundtrack Die Toten Hosen - Im Auftrag des Herrn + */ +class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { + + private final Driver driver; + + DefaultReactiveNeo4jClient(Driver driver) { + + this.driver = driver; + } + + // Internal helper methods for managing transactional state + + Mono getStatementRunner(final String targetDatabase) { + + return retrieveReactiveTransaction(driver, targetDatabase) + .map(RxStatementRunner.class::cast) + // Open the transaction inside a supplier to avoid eager initialization + .switchIfEmpty(Mono.fromSupplier(() -> driver.rxSession(defaultSessionParameters(targetDatabase)))); + } + + static Publisher asyncComplete(RxStatementRunner runner) { + + if (runner instanceof RxSession) { + return ((RxSession) runner).close(); + } + + return Mono.empty(); + } + + static Publisher asyncError(RxStatementRunner runner) { + + if (runner instanceof RxSession) { + return ((RxSession) runner).close(); + } + + return Mono.empty(); + } + + // Below are all the implementations (methods and classes) as defined by the contracts of ReactiveNeo4jClient + + @Override + public ReactiveRunnableSpec newQuery(String cypher) { + return newQuery(() -> cypher); + } + + @Override + public ReactiveRunnableSpec newQuery(Supplier cypherSupplier) { + return new DefaultReactiveRunnableSpec(cypherSupplier); + } + + @Override + public OngoingReactiveDelegation delegateTo(Function> callback) { + return new DefaultReactiveRunnableDelegation<>(callback); + } + + @RequiredArgsConstructor + class DefaultReactiveRunnableSpec implements ReactiveRunnableSpec { + + private final Supplier cypherSupplier; + + private String targetDatabase; + + private final NamedParameters parameters = new NamedParameters(); + + @Override + public ReactiveRunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) { + this.targetDatabase = targetDatabase; + return this; + } + + @RequiredArgsConstructor + class DefaultOngoingBindSpec implements OngoingBindSpec { + + private final T value; + + @Override + public ReactiveRunnableSpecTightToDatabase to(String name) { + + DefaultReactiveRunnableSpec.this.parameters.add(name, value); + return DefaultReactiveRunnableSpec.this; + } + + @Override + public ReactiveRunnableSpecTightToDatabase with(Function> binder) { + return bindAll(binder.apply(value)); + } + } + + @Override + public OngoingBindSpec bind(Object value) { + return new DefaultOngoingBindSpec(value); + } + + @Override + public ReactiveRunnableSpecTightToDatabase bindAll(Map newParameters) { + this.parameters.addAll(newParameters); + return this; + } + + @Override + public MappingSpec, Flux, R> fetchAs(Class targetClass) { + + return new DefaultReactiveRecordFetchSpec<>(this.targetDatabase, this.cypherSupplier, this.parameters, + new SingleValueMappingFunction(targetClass)); + } + + @Override + public RecordFetchSpec>, Flux>, Map> fetch() { + + return new DefaultReactiveRecordFetchSpec<>(targetDatabase, cypherSupplier, parameters, Record::asMap); + } + + @Override + public Mono run() { + + return new DefaultReactiveRecordFetchSpec<>( + this.targetDatabase, + this.cypherSupplier, + this.parameters).run(); + } + } + + @AllArgsConstructor + @RequiredArgsConstructor + class DefaultReactiveRecordFetchSpec + implements RecordFetchSpec, Flux, T>, MappingSpec, Flux, T> { + + private final String targetDatabase; + + private final Supplier cypherSupplier; + + private final NamedParameters parameters; + + private Function mappingFunction; + + @Override + public RecordFetchSpec, Flux, T> mappedBy(Function mappingFunction) { + + this.mappingFunction = mappingFunction; + return this; + } + + Mono>> prepareStatement() { + + return Mono.fromSupplier(cypherSupplier).zipWith(Mono.just(parameters.get())); + } + + Flux executeWith(Tuple2> t, RxStatementRunner runner) { + + return Flux.from(runner.run(t.getT1(), t.getT2()).records()).map(mappingFunction); + } + + @Override + public Mono one() { + + return Mono.usingWhen( + getStatementRunner(targetDatabase), + runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).single(), + DefaultReactiveNeo4jClient::asyncComplete, + DefaultReactiveNeo4jClient::asyncError + ); + } + + @Override + public Mono first() { + + return Mono.usingWhen( + getStatementRunner(targetDatabase), + runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).next(), + DefaultReactiveNeo4jClient::asyncComplete, + DefaultReactiveNeo4jClient::asyncError + ); + } + + @Override + public Flux all() { + + return Flux.usingWhen( + getStatementRunner(targetDatabase), + runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)), + DefaultReactiveNeo4jClient::asyncComplete, + DefaultReactiveNeo4jClient::asyncError + ); + } + + Mono run() { + + return Mono + .usingWhen( + getStatementRunner(targetDatabase), + runner -> prepareStatement().flatMap(t -> Mono.from(runner.run(t.getT1(), t.getT2()).summary())), + DefaultReactiveNeo4jClient::asyncComplete, + DefaultReactiveNeo4jClient::asyncError + ); + } + } + + @AllArgsConstructor + @RequiredArgsConstructor + class DefaultReactiveRunnableDelegation implements ReactiveRunnableDelegation, OngoingReactiveDelegation { + + private final Function> callback; + + private String targetDatabase; + + @Override + public ReactiveRunnableDelegation in(@SuppressWarnings("HiddenField") String targetDatabase) { + this.targetDatabase = targetDatabase; + return this; + } + + @Override + public Mono run() { + + return Mono.usingWhen( + getStatementRunner(targetDatabase), + callback::apply, + DefaultReactiveNeo4jClient::asyncComplete, + DefaultReactiveNeo4jClient::asyncError); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java new file mode 100644 index 000000000..5cdfe73bd --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apiguardian.api.API; + +/** + * @author Michael J. Simons + * @soundtrack Bananafishbones - Viva Conputa + */ +@API(status = API.Status.INTERNAL, since = "1.0") +final class NamedParameters { + + private final Map parameters = new HashMap<>(); + + /** + * Adds all of the values contained in {@code newParameters} to this list of named parameters. + * + * @param newParameters Additional parameters to add + * @return This object + * @throws IllegalStateException when any value in {@code newParameters} exists under the same name in the current parameters. + */ + NamedParameters addAll(Map newParameters) { + newParameters.forEach(this::add); + + return this; + } + + /** + * Adds a new parameter under the key {@code name} with the value {@code value}. + * + * @param name The name of the new parameter + * @param value The value of the new parameter + * @return This object + * @throws IllegalStateException when a parameter with the given name already exists + */ + NamedParameters add(String name, Object value) { + + if (this.parameters.containsKey(name)) { + Object previousValue = this.parameters.get(name); + throw new IllegalArgumentException(String.format( + "Duplicate parameter name: '%s' already in the list of named parameters with value '%s'. New value would be '%s'", + name, + previousValue == null ? "null" : previousValue.toString(), + value == null ? "null" : value.toString() + )); + } + this.parameters.put(name, value); + return this; + } + + /** + * @return An unmodifiable copy of this lists values. + */ + Map get() { + return Collections.unmodifiableMap(parameters); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java new file mode 100644 index 000000000..4ab2f8cc4 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apiguardian.api.API; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.StatementRunner; +import org.neo4j.driver.summary.ResultSummary; + +/** + * Definition of a modern Neo4j client. + * + * TODO Create examples how to use the callbacks etc. with Springs TransactionTemplate to deal with rollbacks etc. + * TODO database selection + * + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public interface Neo4jClient { + + static Neo4jClient create(Driver driver) { + + return new DefaultNeo4jClient(driver); + } + + /** + * Entrypoint for creating a new Cypher query. Doesn't matter at this point whether it's a match, merge, create or + * removal of things. + * + * @param cypher The cypher code that shall be executed + * @return A new CypherSpec + */ + RunnableSpec newQuery(String cypher); + + /** + * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at this point whether it's a match, + * merge, create or removal of things. The supplier can be an arbitrary Supplier that may provide a DSL for generating + * the Cypher statement. + * + * @param cypherSupplier A supplier of arbitrary Cypher code + * @return + */ + RunnableSpec newQuery(Supplier cypherSupplier); + + /** + * Delegates interaction with the default database to the given callback. + * + * @param callback A function receiving a statement runner for database interaction that can optionally return a result. + * @param The type of the result being produced + * @return A single result object or an empty optional if the callback didn't produce a result + */ + OngoingDelegation delegateTo(Function> callback); + + /** + * Contract for a runnable query that can be either run returning it's result, run without results or be parameterized. + */ + interface RunnableSpec extends RunnableSpecTightToDatabase { + + /** + * Pins the previously defined query to a specific database. + * + * @param targetDatabase + * @return + */ + RunnableSpecTightToDatabase in(String targetDatabase); + } + + /** + * Contract for a runnable query inside a dedicated database. + */ + interface RunnableSpecTightToDatabase extends BindSpec { + + /** + * Create a mapping for each record return to a specific type. + * + * @param targetClass The class each record should be mapped to + * @param The type of the class + * @return A mapping spec that allows specifying a mapping function + */ + MappingSpec, Collection, T> fetchAs(Class targetClass); + + /** + * Fetch all records mapped into generic maps + * + * @return A fetch specification that maps into generic maps + */ + RecordFetchSpec>, Collection>, Map> fetch(); + + /** + * Execute the query and discard the results + * + * @return + */ + ResultSummary run(); + } + + /** + * Contract for binding parameters to a query. + * + * @param This {@link BindSpec specs} own type + */ + interface BindSpec> { + + /** + * @param value The value to bind to a query + * @return An ongoing bind spec for specifying the name that {@code value} should be bound to or a binder function + */ + OngoingBindSpec bind(T value); + + S bindAll(Map parameters); + } + + /** + * Ongoing bind specification. + * + * @param This {@link OngoingBindSpec specs} own type + */ + interface OngoingBindSpec> { + + /** + * Bind one convertible object to the given name. + * + * @param name The named parameter to bind the value to + * @return + */ + S to(String name); + + /** + * Use a binder function for the previously defined value. + * + * @param binder The binder function to create a map of parameters from the given value + * @return + */ + S with(Function> binder); + } + + /** + * @param The type of the class holding zero or one result element + * @param The type of the class holding zero or more result elements + * @param The resulting type of this mapping + */ + interface MappingSpec extends RecordFetchSpec { + + RecordFetchSpec mappedBy(Function mappingFunction); + } + + /** + * @param The type of the class holding zero or one result element + * @param The type of the class holding zero or more result elements + * @param The type to which the fetched records are eventually mapped + */ + interface RecordFetchSpec { + + S one(); + + S first(); + + M all(); + } + + /** + * A contract for an ongoing delegation in the selected database. + * + * @param The type of the returned value. + */ + interface OngoingDelegation extends RunnableDelegation { + + /** + * Runs the delegation in the given target database. + * + * @param targetDatabase + * @return An ongoing delegation + */ + RunnableDelegation in(String targetDatabase); + } + + /** + * A runnable delegation. + * + * @param + */ + interface RunnableDelegation { + + /** + * Runs the stored callback. + * + * @return + */ + Optional run(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java deleted file mode 100644 index 634a06fec..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jOperations.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core; - -import org.apiguardian.api.API; - -/** - * Definition of Neo4j specific operations. Implemented by {@link Neo4jTemplate}. - * - * @author Gerrit Meier - */ -public interface Neo4jOperations { - - @API(status = API.Status.EXPERIMENTAL) - Object executeQuery(String query); -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java deleted file mode 100644 index 8e3861927..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.StatementResult; -import org.neo4j.driver.StatementRunner; -import org.springframework.data.neo4j.core.transaction.ManagedTransactionProvider; -import org.springframework.data.neo4j.core.transaction.NativeTransactionProvider; - -/** - * Default implementation of {@link Neo4jOperations}. Uses the Neo4j Java driver to connect to and interact with the - * database. - * - * @author Gerrit Meier - * @author Michael J. Simons - */ -public class Neo4jTemplate implements Neo4jOperations { - - private final Driver driver; - - private final NativeTransactionProvider transactionProvider; - - public Neo4jTemplate(Driver driver) { - - this(driver, new ManagedTransactionProvider()); - } - - Neo4jTemplate(Driver driver, NativeTransactionProvider transactionProvider) { - - this.driver = driver; - this.transactionProvider = transactionProvider; - } - - AutoCloseableStatementRunner getStatementRunner() { - - StatementRunner statementRunner = transactionProvider.retrieveTransaction(driver, null) - .map(StatementRunner.class::cast) - .orElseGet(() -> driver.session()); - - return (AutoCloseableStatementRunner) Proxy.newProxyInstance(StatementRunner.class.getClassLoader(), - new Class[] { AutoCloseableStatementRunner.class }, - new AutoCloseableStatementRunnerHandler(statementRunner)); - } - - @Override - public Object executeQuery(String query) { - - try (AutoCloseableStatementRunner statementRunner = getStatementRunner()) { - - StatementResult result = statementRunner.run(query); - - return result.list(); - } - } - - /** - * Makes a statement runner autoclosable and aware wether it's session or a transaction - */ - interface AutoCloseableStatementRunner extends StatementRunner, AutoCloseable { - - @Override void close(); - } - - private static class AutoCloseableStatementRunnerHandler implements InvocationHandler { - - private final Map cachedHandles = new ConcurrentHashMap<>(); - private final StatementRunner target; - - AutoCloseableStatementRunnerHandler(StatementRunner target) { - this.target = target; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - - if ("close".equals(method.getName())) { - if (this.target instanceof Session) { - ((Session) this.target).close(); - } - return null; - } else { - return cachedHandles.computeIfAbsent(method, this::findHandleFor) - .invokeWithArguments(args); - } - } - - MethodHandle findHandleFor(Method method) { - try { - return MethodHandles.publicLookup().unreflect(method).bindTo(target); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } -} - diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java index 65bc9fe0f..fe3fcce77 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java @@ -31,8 +31,7 @@ import org.apiguardian.api.API; import org.neo4j.driver.Driver; import org.springframework.data.neo4j.core.schema.Scanner; import org.springframework.data.neo4j.core.schema.Schema; -import org.springframework.data.neo4j.core.transaction.DefaultTransactionProvider; -import org.springframework.data.neo4j.core.transaction.NativeTransactionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; import org.springframework.lang.Nullable; /** @@ -61,11 +60,6 @@ public final class NodeManagerFactory { @Nullable private Schema schema; - /** - * A provider of native transactions, defaults to unmanaged native transactions opened with default parameters. - */ - private NativeTransactionProvider nativeTransactionProvider = new DefaultTransactionProvider(); - /** * Creates a new instance of a factory producing {@link NodeManager node managers}. When used in a transactional setup, * i.e. with the {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}, make sure to use @@ -94,21 +88,10 @@ public final class NodeManagerFactory { public NodeManager createNodeManager() { Objects.requireNonNull(schema, "A schema is required. Did you call #initialize() before using this factory?"); - return new DefaultNodeManager(schema, new Neo4jTemplate(driver, this.nativeTransactionProvider), - this.nativeTransactionProvider.retrieveTransaction(driver, null).orElse(null)); - } - - /** - * Configures a provider for extracting sessions/transactions from a Neo4j driver. This method is not to be called - * from application code and only used by internal API. - * - * @param nativeTransactionProvider A required transaction supplier - */ - @API(status = API.Status.INTERNAL, since = "1.0") - public void setNativeTransactionProvider(NativeTransactionProvider nativeTransactionProvider) { - - Objects.requireNonNull(nativeTransactionProvider, "A node manager factory requires a transaction provider."); - this.nativeTransactionProvider = nativeTransactionProvider; + // The call here to our Spring transaction shim has to be rethought in case we move this out of a Spring scope. + // I dropped all the methods to configure that in an effort to make the setup more simple. ^mjs + return new DefaultNodeManager(schema, Neo4jClient.create(driver), + Neo4jTransactionUtils.retrieveTransaction(driver, null).orElse(null)); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java new file mode 100644 index 000000000..b48581a2c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apiguardian.api.API; +import org.neo4j.driver.Driver; +import org.neo4j.driver.reactive.RxStatementRunner; +import org.neo4j.driver.summary.ResultSummary; +import org.springframework.data.neo4j.core.Neo4jClient.BindSpec; +import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; +import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; + +/** + * Reactive Neo4j client. The main difference to the {@link Neo4jClient imperative Neo4j client} is the fact that all + * operations will only be executed once something subscribes to the reactive sequence defined. + * + * @author Michael J. Simons + * @since 1.0 + * @soundtrack Die Toten Hosen - Im Auftrag des Herrn + */ +@API(status = API.Status.STABLE, since = "1.0") +public interface ReactiveNeo4jClient { + + static ReactiveNeo4jClient create(Driver driver) { + + return new DefaultReactiveNeo4jClient(driver); + } + + /** + * Entrypoint for creating a new Cypher query. Doesn't matter at this point whether it's a match, merge, create or + * removal of things. + * + * @param cypher The cypher code that shall be executed + * @return A new CypherSpec + */ + ReactiveRunnableSpec newQuery(String cypher); + + /** + * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at this point whether it's a match, + * merge, create or removal of things. The supplier can be an arbitrary Supplier that may provide a DSL for generating + * the Cypher statement. + * + * @param cypherSupplier A supplier of arbitrary Cypher code + * @return + */ + ReactiveRunnableSpec newQuery(Supplier cypherSupplier); + + /** + * Delegates interaction with the default database to the given callback. + * + * @param callback A function receiving a reactive statement runner for database interaction that can optionally return a publisher with none or exactly one element + * @param The type of the result being produced + * @return A single publisher containing none or exactly one element that will be produced by the callback + */ + OngoingReactiveDelegation delegateTo(Function> callback); + + /** + * Contract for a runnable query that can be either run returning it's result, run without results or be parameterized. + */ + interface ReactiveRunnableSpec extends ReactiveRunnableSpecTightToDatabase { + + /** + * Pins the previously defined query to a specific database. + * + * @param targetDatabase + * @return + */ + ReactiveRunnableSpecTightToDatabase in(String targetDatabase); + } + + /** + * Contract for a runnable query inside a dedicated database. + */ + interface ReactiveRunnableSpecTightToDatabase extends BindSpec { + + /** + * Create a mapping for each record return to a specific type. + * + * @param targetClass The class each record should be mapped to + * @param The type of the class + * @return A mapping spec that allows specifying a mapping function + */ + MappingSpec, Flux, T> fetchAs(Class targetClass); + + /** + * Fetch all records mapped into generic maps + * + * @return A fetch specification that maps into generic maps + */ + RecordFetchSpec>, Flux>, Map> fetch(); + + /** + * Execute the query and discard the results + * + * @return + */ + Mono run(); + } + + /** + * A contract for an ongoing delegation in the selected database. + * + * @param The type of the returned value. + */ + interface OngoingReactiveDelegation extends ReactiveRunnableDelegation { + + /** + * Runs the delegation in the given target database. + * + * @param targetDatabase + * @return An ongoing delegation + */ + ReactiveRunnableDelegation in(String targetDatabase); + } + + /** + * A runnable delegation. + * + * @param + */ + interface ReactiveRunnableDelegation { + + /** + * Runs the stored callback. + * + * @return + */ + Mono run(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java new file mode 100644 index 000000000..d2a43bdd6 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import lombok.RequiredArgsConstructor; + +import java.util.function.Function; + +import org.neo4j.driver.Record; +import org.neo4j.driver.Value; +import org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes; + +/** + * Used to automatically map single valued records to a sensible Java type based on {@link Value#asObject()}. + * + * @param + * @author Michael J. Simons + */ +@RequiredArgsConstructor +final class SingleValueMappingFunction implements Function { + + private final Class targetClass; + + @Override + public T apply(Record record) { + + if (record.size() == 0) { + throw new IllegalArgumentException("Record has no elements, cannot map nothing."); + } + + if (record.size() > 1) { + throw new IllegalArgumentException( + "Records with more than one value cannot be converted without a mapper."); + } + + return Neo4jSimpleTypes.asObject(record.get(0), targetClass); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityChangeEvent.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityChangeEvent.java index 7e0511c06..4cce80dea 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityChangeEvent.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityChangeEvent.java @@ -24,10 +24,11 @@ import org.apiguardian.api.API; /** * @author Gerrit Meier + * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") @Getter -public class EntityChangeEvent { +public final class EntityChangeEvent { private final String propertyField; private final Object value; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java index 52f606724..2e1c713de 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java @@ -42,7 +42,7 @@ import org.springframework.data.neo4j.core.schema.Schema; * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -public class MappingContextBasedScannerImpl implements Scanner { +public final class MappingContextBasedScannerImpl implements Scanner { private final Neo4jMappingContext neo4jMappingContext; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java index 76a863bfa..a8ba17691 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java @@ -26,7 +26,7 @@ import org.apiguardian.api.API; * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -public class IllegalSchemaChangeException extends IllegalArgumentException { +public final class IllegalSchemaChangeException extends IllegalArgumentException { IllegalSchemaChangeException(String s) { super(s); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java index a3de06677..01298cd7b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java @@ -18,7 +18,10 @@ */ package org.springframework.data.neo4j.core.schema; +import java.util.Optional; + import org.apiguardian.api.API; +import org.neo4j.driver.Value; /** * A list of Neo4j simple types: All attributes that can be mapped to a property. There is never a relationship @@ -29,6 +32,30 @@ import org.apiguardian.api.API; @API(status = API.Status.INTERNAL, since = "1.0") public final class Neo4jSimpleTypes { + /** + * Converts the given Neo4j driver value into the designated type. + * + * @param value The value to convert. + * @param targetClass The target class to convert into + * @param The type of the target class + * @return The converted value or {@literal null} when the values has been {@literal null} + * @throws IllegalArgumentException when the value cannot be converted into an object of the given target class + */ + public static T asObject(Value value, Class targetClass) { + + Optional o = Optional.ofNullable(value).map(Value::asObject); + if (!o.isPresent()) { + return null; + } + + // TODO Add some special treatment for Period/Duration vs IsoDuration as well as the spatial types. + + return o.filter(v -> targetClass.isAssignableFrom(v.getClass())) + .map(targetClass::cast) + .orElseThrow(() -> new IllegalArgumentException( + String.format("%s is not assignable from %s", targetClass.getName(), o.get().getClass().getName()))); + } + private Neo4jSimpleTypes() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java deleted file mode 100644 index 95d9da0f3..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultTransactionProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.transaction; - -import java.util.Optional; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Transaction; - -/** - * A transaction provider that opens a new session using default parameters and starting an explicit transaction on that. - * - * @author Michael J. Simons - */ -public class DefaultTransactionProvider implements NativeTransactionProvider { - - @Override - public Optional retrieveTransaction(Driver driver, String database) { - - Transaction nativeTransaction = driver - .session(Neo4jTransactionUtils.defaultSessionParameters(database)) - .beginTransaction(); - return Optional.of(nativeTransaction); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java deleted file mode 100644 index dc4964504..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProvider.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.transaction; - -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; - -import java.util.Optional; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Transaction; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -/** - * A hook into Springs transaction management to provide managed, native transactions. - * - * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Die Toten Hosen Live - Der Krach der Republik - */ -public class ManagedTransactionProvider implements NativeTransactionProvider { - - /** - * @param databaseName - * @param driver The driver whos transactions are bound from withing {@link TransactionSynchronizationManager}. - * @return An optional containing a managed transaction or an empty optional if the method hasn't been called inside - * an ongoing Spring transaction - */ - @Override - public Optional retrieveTransaction(final Driver driver, String databaseName) { - - if (!TransactionSynchronizationManager.isSynchronizationActive()) { - return Optional.empty(); - } - - // TODO Check existing transaction wether - // Try existing transaction - Neo4jConnectionHolder connectionHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager - .getResource(driver); - - if (connectionHolder != null) { - - return Optional.of(connectionHolder.getTransaction(databaseName)); - } - - // Manually create a new synchronization - connectionHolder = new Neo4jConnectionHolder(databaseName, driver.session(defaultSessionParameters(databaseName))); - connectionHolder.setSynchronizedWithTransaction(true); - - TransactionSynchronizationManager.registerSynchronization( - new Neo4jSessionSynchronization(connectionHolder, driver)); - - TransactionSynchronizationManager.bindResource(driver, connectionHolder); - return Optional.of(connectionHolder.getTransaction(databaseName)); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java deleted file mode 100644 index 9718de876..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NativeTransactionProvider.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.transaction; - -import java.util.Optional; - -import org.apiguardian.api.API; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Transaction; - -/** - * A shim to decouple the retrieval of managed and unmanaged native transaction from Spring infractructure. - * - * @author Michael J. Simons - * @soundtrack Die Toten Hosen - Die Toten Hosen Live - Der Krach der Republik - */ -@FunctionalInterface -@API(status = API.Status.INTERNAL, since = "1.0") -public interface NativeTransactionProvider { - - /** - * Retrieves a transaction from the driver. - * - * @param driver The driver to be used to retrieve transactions from or create new ones - * @param databaseName The name of the database to work with - * @return A transaction or an empty optional in case a transaction could not be provided - */ - Optional retrieveTransaction(Driver driver, String databaseName); -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java index 21e9e7376..00aa2df29 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java @@ -34,7 +34,7 @@ import org.springframework.util.Assert; * * @author Michael J. Simons */ -public class Neo4jConnectionHolder extends ResourceHolderSupport { +class Neo4jConnectionHolder extends ResourceHolderSupport { private final String databaseName; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index f326f9ca6..fff533aa4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -18,12 +18,17 @@ */ package org.springframework.data.neo4j.core.transaction; +import reactor.core.publisher.Mono; + import java.util.Collections; import java.util.Optional; import java.util.function.Consumer; import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; import org.neo4j.driver.SessionParametersTemplate; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.reactive.RxTransaction; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.lang.Nullable; @@ -34,7 +39,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager */ public final class Neo4jTransactionUtils { - static final String DEFAULT_DATABASE_NAME = ""; + public static final String DEFAULT_DATABASE_NAME = ""; /** * The default session uses {@link AccessMode#WRITE} and an empty list of bookmarks. @@ -43,10 +48,61 @@ public final class Neo4jTransactionUtils { * @return Session parameters to configure the default session used */ public static Consumer defaultSessionParameters(@Nullable String databaseName) { - return t -> t - .withDefaultAccessMode(AccessMode.WRITE) - .withBookmarks(Collections.EMPTY_LIST) - .withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME)); + return t -> { + t + .withDefaultAccessMode(AccessMode.WRITE) + .withBookmarks(Collections.EMPTY_LIST) + .withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME)); + }; + } + + /** + * A hook into Springs transaction management to provide managed, native transactions. + * + * @param driver The driver that has been used as a synchronization object. + * @param targetDatabase The target database + * @return An optional containing a managed transaction or an empty optional if the method hasn't been called inside + * an ongoing Spring transaction + */ + public static Optional retrieveTransaction(final Driver driver, final String targetDatabase) { + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + return Optional.empty(); + } + + // Try existing transaction + Neo4jConnectionHolder connectionHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager + .getResource(driver); + + if (connectionHolder != null) { + + return Optional.of(connectionHolder.getTransaction(targetDatabase)); + } + + // Manually create a new synchronization + connectionHolder = new Neo4jConnectionHolder(targetDatabase, + driver.session(defaultSessionParameters(targetDatabase))); + connectionHolder.setSynchronizedWithTransaction(true); + + TransactionSynchronizationManager.registerSynchronization( + new Neo4jSessionSynchronization(connectionHolder, driver)); + + TransactionSynchronizationManager.bindResource(driver, connectionHolder); + return Optional.of(connectionHolder.getTransaction(targetDatabase)); + } + + /** + * A stub to retrieve a reactive transaction that may is already synchronized with Springs context. + * + * @param driver The driver that has been used as a synchronization object. + * @param targetDatabase The target database + * @return A Mono containing a managed transaction or an empty Mono if the method hasn't been called inside + * an ongoing Spring transaction + */ + public static Mono retrieveReactiveTransaction(final Driver driver, final String targetDatabase) { + + // TODO Hook into Springs reactive transaction management (Coming 5.2 M2) + return Mono.empty(); } public static NodeManager retrieveTransactionalNodeManager(NodeManagerFactory nodeManagerFactory) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java index c6bbf1c53..d077d77b3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java @@ -30,12 +30,12 @@ import org.springframework.util.Assert; * * @author Michael J. Simons */ -public class NodeManagerHolder extends ResourceHolderSupport { +class NodeManagerHolder extends ResourceHolderSupport { @Nullable private final NodeManager nodeManager; - public NodeManagerHolder(@Nullable NodeManager nodeManager) { + NodeManagerHolder(@Nullable NodeManager nodeManager) { this.nodeManager = nodeManager; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java index 86f2f7e66..36d068ef3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java @@ -27,14 +27,11 @@ import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.data.neo4j.core.mapping.MappingContextBasedScannerImpl; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.transaction.ManagedTransactionProvider; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; /** * This is a shim that integrates a {@link NodeManagerFactory} with Spring Datas infrastructure. It takes in the factory - * that needs to be provided by the user and the mapping context provided by our infrastructure. The node manager factory - * is than augment by our {@link Neo4jTransactionUtils} so that it doesn't use unmanaged native transaction, but managed - * native transaction. Furthermore, the default noop scanner is replaced by a scanner based on our Neo4j mapping context. + * that needs to be provided by the user and the mapping context provided by our infrastructure. Springs mapping context + * replaces the default noop scanner. * * @author Gerrit Meier * @author Michael J. Simons @@ -60,8 +57,6 @@ public final class NodeManagerFactoryBean implements InitializingBean, FactoryBe @Override public void afterPropertiesSet() { - // Use managed transactions - target.setNativeTransactionProvider(new ManagedTransactionProvider()); target.setScanner(new MappingContextBasedScannerImpl(this.neo4jMappingContext)); target.initialize(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java new file mode 100644 index 000000000..a716849e8 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import static org.assertj.core.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class NamedParametersTest { + + @Test + void shouldConvertCorrectListOfParametersIntoMap() { + + NamedParameters namedParameters = new NamedParameters(); + + namedParameters.add("a", 1); + namedParameters.add("b", "Something"); + namedParameters.add("c", null); + + assertThat(namedParameters.get()) + .containsEntry("a", 1) + .containsEntry("b", "Something") + .containsEntry("c", null); + } + + @Test + void shouldNotAllowDuplicateParameters() { + + assertThatIllegalArgumentException().isThrownBy(() -> { + NamedParameters namedParameters = new NamedParameters(); + + namedParameters.add("a", 1); + namedParameters.add("b", 1); + namedParameters.add("a", 2); + } + ).withMessage( + "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be '2'"); + + assertThatIllegalArgumentException().isThrownBy(() -> { + NamedParameters namedParameters = new NamedParameters(); + namedParameters.add("a", 1); + + Map newValues = new HashMap<>(); + newValues.put("b", 1); + newValues.put("a", 2); + + namedParameters.addAll(newValues); + } + ).withMessage( + "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be '2'"); + + assertThatIllegalArgumentException().isThrownBy(() -> { + NamedParameters namedParameters = new NamedParameters(); + + namedParameters.add("a", null); + namedParameters.add("a", 2); + } + ).withMessage( + "Duplicate parameter name: 'a' already in the list of named parameters with value 'null'. New value would be '2'"); + + assertThatIllegalArgumentException().isThrownBy(() -> { + NamedParameters namedParameters = new NamedParameters(); + + namedParameters.add("a", 1); + namedParameters.add("a", null); + }).withMessage( + "Duplicate parameter name: 'a' already in the list of named parameters with value '1'. New value would be 'null'"); + } + + @Test + void shouldDealWithEmptyParameterList() { + + assertThat(new NamedParameters().get()).isEmpty(); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java new file mode 100644 index 000000000..ed74b104c --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.assertj.core.matcher.AssertionMatcher; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.StatementResult; +import org.neo4j.driver.Values; +import org.neo4j.driver.internal.SessionParameters; +import org.neo4j.driver.summary.ResultSummary; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class Neo4jClientTest { + + @Mock + private Driver driver; + + private ArgumentCaptor sessionTemplateCaptor = ArgumentCaptor.forClass(Consumer.class); + + @Mock + private SessionParameters.Template sessionParametersTemplate; + + @Mock + private Session session; + + @Mock + private StatementResult statementResult; + + @Mock + private ResultSummary resultSummary; + + @Mock + private Record record1; + + @Mock + private Record record2; + + @BeforeEach + void prepareMocks() { + + when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); + when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); + when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) + .thenReturn(sessionParametersTemplate); + + when(driver.session(any(Consumer.class))).thenReturn(session); + } + + @AfterEach + void verifyNoMoreInteractionsWithMocks() { + verifyNoMoreInteractions(driver, session, statementResult, resultSummary, record1, record2); + } + + @Test + @DisplayName("Creation of queries and binding parameters should feel natural") + void queryCreationShouldFeelGood() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.stream()).thenReturn(Stream.of(record1, record2)); + + Neo4jClient client = Neo4jClient.create(driver); + + Map parameters = new HashMap<>(); + parameters.put("bikeName", "M.*"); + parameters.put("location", "Sweden"); + + String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) - [:USED_ON] -> (t:Trip) " + + "WHERE t.takenOn > $aDate " + + " AND b.name =~ $bikeName " + + " AND t.location = $location " + // TODO Nice place to add coordinates + "RETURN b"; + + Collection> usedBikes = client + .newQuery(cypher) + .bind("michael").to("name") + .bindAll(parameters) + .bind(LocalDate.of(2019, 1, 1)).to("aDate") + .fetch() + .all(); + + assertThat(usedBikes).hasSize(2); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + Map expectedParameters = new HashMap<>(); + expectedParameters.putAll(parameters); + expectedParameters.put("name", "michael"); + expectedParameters.put("aDate", LocalDate.of(2019, 1, 1)); + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + + verify(statementResult).stream(); + verify(record1).asMap(); + verify(record2).asMap(); + verify(session).close(); + } + + @Test + void databaseSelectionShouldBePossibleOnlyOnce() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.stream()).thenReturn(Stream.of(record1, record2)); + + Neo4jClient client = Neo4jClient.create(driver); + + String cypher = "MATCH (u:User) WHERE u.name =~ $name"; + + Optional> firstMatchingUser = client + .newQuery(cypher) + .in("bikingDatabase") + .bind("Someone.*").to("name") + .fetch() + .first(); + + assertThat(firstMatchingUser).isPresent(); + + verifyDatabaseSelection("bikingDatabase"); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Someone.*"); + + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(statementResult).stream(); + verify(record1).asMap(); + verify(session).close(); + } + + @Nested + @DisplayName("Callback handling should feel good") + class CallbackHandlingShouldFeelGood { + + @Test + void withDefaultDatabase() { + + Neo4jClient client = Neo4jClient.create(driver); + Optional result = client + .delegateTo(runner -> Optional.of(42)) + .run(); + + assertThat(result).isPresent().hasValue(42); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + verify(session).close(); + } + + @Test + void withDatabase() { + + Neo4jClient client = Neo4jClient.create(driver); + Optional result = client + .delegateTo(runner -> Optional.of(42)) + .in("aDatabase") + .run(); + + assertThat(result).isPresent().hasValue(42); + + verifyDatabaseSelection("aDatabase"); + + verify(session).close(); + } + } + + @Nested + @DisplayName("Mapping should feel good") + class MappingShouldFeelGood { + + @Test + void reading() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.stream()).thenReturn(Stream.of(record1)); + when(record1.get("name")).thenReturn(Values.value("michael")); + + Neo4jClient client = Neo4jClient.create(driver); + + String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike)" + + "RETURN o, collect(b) as bikes"; + + BikeOwnerReader mappingFunction = new BikeOwnerReader(); + Collection bikeOwners = client + .newQuery(cypher) + .bind("michael").to("name") + .fetchAs(BikeOwner.class).mappedBy(mappingFunction) + .all(); + + assertThat(bikeOwners).hasSize(1).first() + .hasFieldOrPropertyWithValue("name", "michael"); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "michael"); + + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(statementResult).stream(); + verify(record1).get("name"); + verify(session).close(); + } + + @Test + void writing() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.consume()).thenReturn(resultSummary); + + Neo4jClient client = Neo4jClient.create(driver); + + BikeOwner michael = new BikeOwner("Michael", Arrays.asList(new Bike("Road"), new Bike("MTB"))); + String cypher = "MERGE (u:User {name: 'Michael'}) " + + "WITH u UNWIND $bikes as bike " + + "MERGE (b:Bike {name: bike}) " + + "MERGE (u) - [o:OWNS] -> (b) "; + ResultSummary summary = client + .newQuery(cypher) + .bind(michael).with(new BikeOwnerBinder()) + .run(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Michael"); + + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(statementResult).consume(); + verify(session).close(); + } + + @Test + @DisplayName("Some automatic conversion is ok") + void automaticConversion() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.hasNext()).thenReturn(true); + when(statementResult.single()).thenReturn(record1); + when(record1.size()).thenReturn(1); + when(record1.get(0)).thenReturn(Values.value(23L)); + + Neo4jClient client = Neo4jClient.create(driver); + + String cypher = "MATCH (b:Bike) RETURN count(b)"; + Optional numberOfBikes = client + .newQuery(cypher) + .fetchAs(Long.class) + .one(); + + assertThat(numberOfBikes).isPresent().hasValue(23L); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + verify(session).run(eq(cypher), anyMap()); + verify(statementResult).hasNext(); + verify(statementResult).single(); + verify(session).close(); + } + } + + @Test + @DisplayName("Queries that return nothing should fit in") + void queriesWithoutResultShouldFitInAsWell() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.consume()).thenReturn(resultSummary); + + Neo4jClient client = Neo4jClient.create(driver); + + String cypher = "DETACH DELETE (b) WHERE name = $name"; + + client + .newQuery(cypher) + .bind("fixie").to("name") + .run(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "fixie"); + + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(statementResult).consume(); + verify(session).close(); + } + + static class BikeOwner { + + private final String name; + + private final List bikes; + + BikeOwner(String name, List bikes) { + this.name = name; + this.bikes = new ArrayList<>(bikes); + } + + public String getName() { + return name; + } + + public List getBikes() { + return Collections.unmodifiableList(bikes); + } + } + + static class Bike { + + private final String name; + + Bike(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + static class BikeOwnerReader implements Function { + + @Override + public BikeOwner apply(Record record) { + return new BikeOwner(record.get("name").asString(), Collections.emptyList()); + } + } + + static class BikeOwnerBinder implements Function> { + + @Override + public Map apply(BikeOwner bikeOwner) { + + Map mappedValues = new HashMap<>(); + + mappedValues.put("name", bikeOwner.getName()); + return mappedValues; + } + } + + void verifyDatabaseSelection(String targetDatabase) { + verify(driver).session(sessionTemplateCaptor.capture()); + sessionTemplateCaptor.getValue().accept(sessionParametersTemplate); + verify(sessionParametersTemplate).withDatabase(targetDatabase); + } + + static class MapAssertionMatcher extends AssertionMatcher> { + private final Map expectedParameters; + + MapAssertionMatcher(Map expectedParameters) { + this.expectedParameters = expectedParameters; + } + + @Override + public void assertion(Map actual) { + assertThat(actual).containsAllEntriesOf(expectedParameters); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java deleted file mode 100644 index 8d5b49acc..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jTemplateTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core; - -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -import java.util.Optional; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.Transaction; -import org.springframework.data.neo4j.core.Neo4jTemplate.AutoCloseableStatementRunner; - -/** - * @author Michael J. Simons - */ -@ExtendWith(MockitoExtension.class) -class Neo4jTemplateTest { - - @Mock - private Driver driver; - @Mock - private Session session; - @Mock - private Transaction transaction; - - @Nested - class AutoCloseableStatementRunnerHandlerTest { - - @Test - public void shouldCallCloseOnSession() { - - when(driver.session()).thenReturn(session); - - // Make template acquire session - Neo4jTemplate neo4jTemplate = new Neo4jTemplate(driver, (d, n) -> Optional.empty()); - try (AutoCloseableStatementRunner s = neo4jTemplate.getStatementRunner()) { - s.run("MATCH (n) RETURN n"); - } - - verify(driver).session(); - verify(session).run(any(String.class)); - verify(session).close(); - verifyNoMoreInteractions(driver, session, transaction); - } - - @Test - public void shouldNotInvokeCloseOnTransaction() { - - Neo4jTemplate neo4jTemplate = new Neo4jTemplate(driver, (d, n) -> Optional.of(transaction)); - try (AutoCloseableStatementRunner s = neo4jTemplate.getStatementRunner()) { - s.run("MATCH (n) RETURN n"); - } - - verify(transaction).run(any(String.class)); - verifyNoMoreInteractions(driver, session, transaction); - } - } -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java new file mode 100644 index 000000000..2221d193f --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Values; +import org.neo4j.driver.internal.SessionParameters; +import org.neo4j.driver.reactive.RxResult; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.summary.ResultSummary; +import org.springframework.data.neo4j.core.Neo4jClientTest.Bike; +import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwner; +import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwnerBinder; +import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwnerReader; +import org.springframework.data.neo4j.core.Neo4jClientTest.MapAssertionMatcher; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class ReactiveNeo4jClientTest { + + @Mock + private Driver driver; + + private ArgumentCaptor sessionTemplateCaptor = ArgumentCaptor.forClass(Consumer.class); + + @Mock + private SessionParameters.Template sessionParametersTemplate; + + @Mock + private RxSession session; + + @Mock + private RxResult statementResult; + + @Mock + private ResultSummary resultSummary; + + @Mock + private Record record1; + + @Mock + private Record record2; + + @BeforeEach + void prepareMocks() { + + when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); + when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); + when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) + .thenReturn(sessionParametersTemplate); + + when(driver.rxSession(any(Consumer.class))).thenReturn(session); + + when(session.close()).thenReturn(Mono.empty()); + } + + @AfterEach + void verifyNoMoreInteractionsWithMocks() { + verifyNoMoreInteractions(driver, session, statementResult, resultSummary, record1, record2); + } + + + @Test + @DisplayName("Creation of queries and binding parameters should feel natural") + void queryCreationShouldFeelGood() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.records()).thenReturn(Flux.just(record1, record2)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + + Map parameters = new HashMap<>(); + parameters.put("bikeName", "M.*"); + parameters.put("location", "Sweden"); + + String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) - [:USED_ON] -> (t:Trip) " + + "WHERE t.takenOn > $aDate " + + " AND b.name =~ $bikeName " + + " AND t.location = $location " + + "RETURN b"; + + Flux> usedBikes = client + .newQuery(cypher) + .bind("michael").to("name") + .bindAll(parameters) + .bind(LocalDate.of(2019, 1, 1)).to("aDate") + .fetch() + .all(); + + StepVerifier.create(usedBikes) + .expectNextCount(2L) + .verifyComplete(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + Map expectedParameters = new HashMap<>(); + expectedParameters.putAll(parameters); + expectedParameters.put("name", "michael"); + expectedParameters.put("aDate", LocalDate.of(2019, 1, 1)); + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + + verify(statementResult).records(); + verify(record1).asMap(); + verify(record2).asMap(); + verify(session).close(); + } + + @Test + void databaseSelectionShouldBePossibleOnlyOnce() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.records()).thenReturn(Flux.just(record1, record2)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + + String cypher = "MATCH (u:User) WHERE u.name =~ $name"; + Mono> firstMatchingUser = client + .newQuery(cypher) + .in("bikingDatabase") + .bind("Someone.*").to("name") + .fetch() + .first(); + + StepVerifier.create(firstMatchingUser) + .expectNextCount(1L) + .verifyComplete(); + + verifyDatabaseSelection("bikingDatabase"); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Someone.*"); + + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(statementResult).records(); + verify(record1).asMap(); + verify(session).close(); + } + + @Nested + @DisplayName("Callback handling should feel good") + class CallbackHandlingShouldFeelGood { + + @Test + void withDefaultDatabase() { + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + Mono result = client + .delegateTo(runner -> Mono.just(21)) + .run(); + + StepVerifier.create(result) + .expectNext(21) + .verifyComplete(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + verify(session).close(); + } + + @Test + void withDatabase() { + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + Mono result = client + .delegateTo(runner -> Mono.just(21)) + .in("aDatabase") + .run(); + + StepVerifier.create(result) + .expectNext(21) + .verifyComplete(); + + verifyDatabaseSelection("aDatabase"); + + verify(session).close(); + } + } + + @Nested + @DisplayName("Mapping should feel good") + class MappingShouldFeelGood { + + @Test + void reading() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.records()).thenReturn(Flux.just(record1)); + when(record1.get("name")).thenReturn(Values.value("michael")); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + + String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike)" + + "RETURN o, collect(b) as bikes"; + + BikeOwnerReader mappingFunction = new BikeOwnerReader(); + Flux bikeOwners = client + .newQuery(cypher) + .bind("michael").to("name") + .fetchAs(BikeOwner.class).mappedBy(mappingFunction) + .all(); + + StepVerifier.create(bikeOwners) + .expectNextMatches(o -> o.getName().equals("michael")) + .verifyComplete(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "michael"); + + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(statementResult).records(); + verify(record1).get("name"); + verify(session).close(); + } + + @Test + void writing() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.summary()).thenReturn(Mono.just(resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + + BikeOwner michael = new BikeOwner("Michael", Arrays.asList(new Bike("Road"), new Bike("MTB"))); + String cypher = "MERGE (u:User {name: 'Michael'}) " + + "WITH u UNWIND $bikes as bike " + + "MERGE (b:Bike {name: bike}) " + + "MERGE (u) - [o:OWNS] -> (b) "; + + Mono summary = client + .newQuery(cypher) + .bind(michael).with(new BikeOwnerBinder()) + .run(); + + StepVerifier.create(summary) + .expectNext(resultSummary) + .verifyComplete(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "Michael"); + + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(statementResult).summary(); + verify(session).close(); + } + + @Test + @DisplayName("Some automatic conversion is ok") + void automaticConversion() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.records()).thenReturn(Flux.just(record1)); + when(record1.size()).thenReturn(1); + when(record1.get(0)).thenReturn(Values.value(23L)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + + String cypher = "MATCH (b:Bike) RETURN count(b)"; + Mono numberOfBikes = client + .newQuery(cypher) + .fetchAs(Long.class) + .one(); + + StepVerifier.create(numberOfBikes) + .expectNext(23L) + .verifyComplete(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + verify(session).run(eq(cypher), anyMap()); + verify(session).close(); + } + } + + @Test + @DisplayName("Queries that return nothing should fit in") + void queriesWithoutResultShouldFitInAsWell() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.summary()).thenReturn(Mono.just(resultSummary)); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + + String cypher = "DETACH DELETE (b) WHERE name = $name"; + + Mono deletionResult = client + .newQuery(cypher) + .bind("fixie").to("name") + .run(); + + StepVerifier.create(deletionResult) + .expectNext(resultSummary) + .verifyComplete(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + Map expectedParameters = new HashMap<>(); + expectedParameters.put("name", "fixie"); + + verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(statementResult).summary(); + verify(session).close(); + } + + void verifyDatabaseSelection(String targetDatabase) { + verify(driver).rxSession(sessionTemplateCaptor.capture()); + sessionTemplateCaptor.getValue().accept(sessionParametersTemplate); + verify(sessionParametersTemplate).withDatabase(targetDatabase); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java new file mode 100644 index 000000000..d874cd35a --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDate; +import java.time.Period; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.Record; +import org.neo4j.driver.Values; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class SingleValueMappingFunctionTest { + + private final Record record; + + SingleValueMappingFunctionTest(@Mock Record record) { + this.record = record; + } + + @Nested + class ShouldCheckForRecordSize { + + @Test + void shouldNotMapNothing() { + + when(record.size()).thenReturn(0); + + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(String.class); + assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(record)) + .withMessage("Record has no elements, cannot map nothing."); + } + + @Test + void shouldNotMapAmbiguousThings() { + + when(record.size()).thenReturn(23); + + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(String.class); + assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(record)) + .withMessage("Records with more than one value cannot be converted without a mapper."); + } + } + + @Test + void shouldWorkWithNullValues() { + + when(record.size()).thenReturn(1); + when(record.get(0)).thenReturn(Values.NULL); + + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(String.class); + assertThat(mappingFunction.apply(record)).isNull(); + } + + @Test + void shouldCheckReturnType() { + + when(record.size()).thenReturn(1); + when(record.get(0)).thenReturn(Values.value("Guten Tag.")); + + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(Period.class); + assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(record)) + .withMessage("java.time.Period is not assignable from java.lang.String"); + } + + @Test + void mappingShouldWorkForSupportedTypes() { + + LocalDate aDate = LocalDate.of(2019, 4, 10); + + when(record.size()).thenReturn(1); + when(record.get(0)).thenReturn(Values.value(aDate)); + + SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(LocalDate.class); + assertThat(mappingFunction.apply(record)).isEqualTo(aDate); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java new file mode 100644 index 000000000..568bdd9d8 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.internal.SessionParameters; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxStatementRunner; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +/** + * Ensure correct behaviour of both imperative and reactive clients in and outside Springs transaction management. + * + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class TransactionHandlingTest { + + @Mock + private Driver driver; + + @Mock + private SessionParameters.Template sessionParametersTemplate; + + @Nested + class Neo4jClientTest { + + @Mock + private Session session; + + @Mock + private Transaction transaction; + + @Nested + class AutoCloseableStatementRunnerHandlerTest { + + @Test + public void shouldCallCloseOnSession() { + + ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); + when(driver.session(any(Consumer.class))).thenReturn(session); + + when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); + when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); + when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) + .thenReturn(sessionParametersTemplate); + + // Make template acquire session + DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient(driver); + try (DefaultNeo4jClient.AutoCloseableStatementRunner s = neo4jClient.getStatementRunner("aDatabase")) { + s.run("MATCH (n) RETURN n"); + } + + verify(driver).session(consumerCaptor.capture()); + consumerCaptor.getValue().accept(sessionParametersTemplate); + verify(sessionParametersTemplate).withDatabase("aDatabase"); + + verify(session).run(any(String.class)); + verify(session).close(); + + verifyNoMoreInteractions(driver, sessionParametersTemplate, session, transaction); + } + + @Test + public void shouldNotInvokeCloseOnTransaction() { + + AtomicBoolean transactionIsOpen = new AtomicBoolean(true); + + when(driver.session(any(Consumer.class))).thenReturn(session); + when(session.isOpen()).thenReturn(true); + when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); + // Mock closing of the transaction + doAnswer(invocation -> { + transactionIsOpen.set(false); + return null; + }).when(transaction).close(); + when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get()); + + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient(driver); + txTemplate.execute(tx -> { + try (DefaultNeo4jClient.AutoCloseableStatementRunner s = neo4jClient.getStatementRunner(null)) { + s.run("MATCH (n) RETURN n"); + } + return null; + }); + + verify(transaction, times(2)).isOpen(); + verify(transaction).run(anyString()); + // Called by the transaction manager + verify(transaction).success(); + verify(transaction).close(); + verify(session).isOpen(); + verify(session).close(); + verifyNoMoreInteractions(driver, session, transaction); + } + } + } + + @Nested + class ReactiveNeo4jClientTest { + + @Mock + private RxSession session; + + @Test + public void shouldNotOpenTransactionsWithoutSubscription() { + + DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); + + @SuppressWarnings("unused") + Mono runner = neo4jClient.getStatementRunner("aDatabase"); + + verify(driver, never()).rxSession(any(Consumer.class)); + verifyZeroInteractions(driver, session); + } + + // TODO The same like shouldNotOpenTransactionsWithoutSubscription but with managed transactions + + @Test + public void shouldFallbackToImplicitTransaction() { + + when(driver.rxSession(any(Consumer.class))).thenReturn(session); + + DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); + + @SuppressWarnings("unused") + Mono runner = neo4jClient.getStatementRunner("aDatabase"); + + StepVerifier.create(runner) + .expectNextCount(1L) + .verifyComplete(); + + verify(driver).rxSession(any(Consumer.class)); + verifyZeroInteractions(driver, session); + } + + @Test + public void shouldCloseUnmanagedSessionOnComplete() { + + when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(session.close()).thenReturn(Mono.empty()); + + DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); + + Mono sequence = Mono + .usingWhen(neo4jClient.getStatementRunner("aDatabase"), r -> Mono.just("A node"), + DefaultReactiveNeo4jClient::asyncComplete, DefaultReactiveNeo4jClient::asyncError); + + StepVerifier.create(sequence) + .expectNext("A node") + .verifyComplete(); + + verify(driver).rxSession(any(Consumer.class)); + verify(session).close(); + verifyZeroInteractions(driver, session); + } + + @Test + public void shouldCloseUnmanagedSessionOnError() { + + when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(session.close()).thenReturn(Mono.empty()); + + DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); + + Mono sequence = Mono + .usingWhen(neo4jClient.getStatementRunner("aDatabase"), r -> Mono.error(new RuntimeException()), + DefaultReactiveNeo4jClient::asyncComplete, DefaultReactiveNeo4jClient::asyncError); + + StepVerifier.create(sequence) + .expectError() + .verify(); + + verify(driver).rxSession(any(Consumer.class)); + verify(session).close(); + verifyZeroInteractions(driver, session); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java index f1735ad15..03f4c1e93 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java @@ -31,7 +31,7 @@ import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; -import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; @@ -55,15 +55,15 @@ class Neo4jTransactionManagerTest { when(driver.session(any(Consumer.class))).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); - when(transaction.run(anyString())).thenReturn(statementResult); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(session.isOpen()).thenReturn(true); when(transaction.isOpen()).thenReturn(true, false); Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); - Neo4jTemplate template = new Neo4jTemplate(driver); - template.executeQuery("RETURN 1"); + Neo4jClient client = Neo4jClient.create(driver); + client.newQuery("RETURN 1").run(); txManager.commit(txStatus); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java similarity index 88% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java index 5d68c0669..3f110f6a2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ManagedTransactionProviderTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java @@ -52,7 +52,7 @@ import org.springframework.transaction.support.TransactionTemplate; */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class ManagedTransactionProviderTest { +class Neo4jTransactionUtilsTest { private String databaseName = Neo4jTransactionUtils.DEFAULT_DATABASE_NAME; @@ -89,7 +89,7 @@ class ManagedTransactionProviderTest { @Test void shouldWorkWithoutSynchronizations() { - Optional optionalTransaction = new ManagedTransactionProvider().retrieveTransaction(driver, databaseName); + Optional optionalTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); assertThat(optionalTransaction).isEmpty(); @@ -101,8 +101,6 @@ class ManagedTransactionProviderTest { @Test void shouldOpenNewTransaction() { - final ManagedTransactionProvider transactionProvider = new ManagedTransactionProvider(); - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); TransactionTemplate txTemplate = new TransactionTemplate(txManager); @@ -115,7 +113,7 @@ class ManagedTransactionProviderTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); - Optional optionalTransaction = transactionProvider.retrieveTransaction(driver, databaseName); + Optional optionalTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); assertThat(optionalTransaction).isPresent(); transactionStatus.setRollbackOnly(); @@ -136,8 +134,6 @@ class ManagedTransactionProviderTest { @Test void shouldParticipateInOngoingTransaction() { - final ManagedTransactionProvider transactionProvider = new ManagedTransactionProvider(); - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); TransactionTemplate txTemplate = new TransactionTemplate(txManager); @@ -146,7 +142,7 @@ class ManagedTransactionProviderTest { @Override protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { - Optional outerNativeTransaction = transactionProvider.retrieveTransaction(driver, databaseName); + Optional outerNativeTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); assertThat(outerNativeTransaction).isPresent(); assertThat(outerStatus.isNewTransaction()).isTrue(); @@ -157,7 +153,7 @@ class ManagedTransactionProviderTest { assertThat(innerStatus.isNewTransaction()).isFalse(); - Optional innerNativeTransaction = transactionProvider + Optional innerNativeTransaction = Neo4jTransactionUtils .retrieveTransaction(driver, databaseName); assertThat(innerNativeTransaction).isPresent(); } @@ -188,8 +184,6 @@ class ManagedTransactionProviderTest { when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); - final ManagedTransactionProvider transactionProvider = new ManagedTransactionProvider(); - JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); TransactionTemplate txTemplate = new TransactionTemplate(txManager); @@ -202,7 +196,7 @@ class ManagedTransactionProviderTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - Optional nativeTransaction = transactionProvider.retrieveTransaction(driver, databaseName); + Optional nativeTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); assertThat(nativeTransaction).isPresent(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); @@ -228,8 +222,6 @@ class ManagedTransactionProviderTest { when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); - final ManagedTransactionProvider transactionProvider = new ManagedTransactionProvider(); - JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); TransactionTemplate txTemplate = new TransactionTemplate(txManager); @@ -242,7 +234,7 @@ class ManagedTransactionProviderTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - Optional nativeTransaction = transactionProvider.retrieveTransaction(driver, databaseName); + Optional nativeTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); assertThat(nativeTransaction).isPresent(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index c3b0f643e..b0738e84d 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -19,17 +19,21 @@ package org.springframework.data.neo4j.integration; import java.util.List; +import java.util.Map; -import org.neo4j.driver.Record; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.transaction.annotation.Transactional; +/** + * @author Gerrit Meier + * @author Michael J. Simon + */ public interface PersonRepository extends Neo4jRepository { @Transactional @Query("RETURN 1") - List customQuery(); + List> customQuery(); - List findByName(); + List> findByName(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index fc01c4541..a6bef207f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -21,13 +21,13 @@ package org.springframework.data.neo4j.integration; import static org.assertj.core.api.Assertions.*; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; -import org.neo4j.driver.Record; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -63,12 +63,12 @@ class RepositoryIT { @Test void callCustomCypher() { - List actual = repository.customQuery(); + List> actual = repository.customQuery(); assertThat(actual) .isNotNull() .isNotEmpty() - .first().extracting(record -> record.get("1").asInt()) - .isEqualTo(1); + .first().extracting(record -> record.get("1")) + .isEqualTo(1L); } @Test From f6b1081acdc62fb28a8675d73b2ea5fdcf349387 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 11 Apr 2019 11:32:46 +0200 Subject: [PATCH 061/342] Add more rules and concepts to jQAssistant API definition. --- etc/jqassistant/api.adoc | 48 +++++++++++++++---- .../data/neo4j/core/NodeManager.java | 4 -- .../data/neo4j/core/NodeManagerFactory.java | 3 +- .../transaction/Neo4jTransactionManager.java | 2 +- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/etc/jqassistant/api.adoc b/etc/jqassistant/api.adoc index 0d49f644a..07ca9e282 100644 --- a/etc/jqassistant/api.adoc +++ b/etc/jqassistant/api.adoc @@ -1,19 +1,51 @@ [[api:Default]] -[role=group,includesConstraints="api:internal"] +[role=group,includesConstraints="api:*"] + +=== General considerations + +We use https://github.com/apiguardian-team/apiguardian[@API Guardian] to keep track of what we expose as public or internal API. +To keep things both clear and concise, we restrict the usage of those annotations to interfaces, classes and annotations. + +[[api:api-guardian-usage]] +[source,cypher,role="constraint"] +.@API Guardian annotations must not be used on fields +---- +MATCH (c:Java) - [:ANNOTATED_BY] -> (a) - [:OF_TYPE] -> (t:Type {fqn: 'org.apiguardian.api.API'}), + (p) - [:DECLARES] -> (c) +WHERE c:Member +RETURN p.fqn, c.name +---- + +Public interfaces, classes or annotations are either part of internal or public API and have a status. + +[[api:api-guardian-api-concept]] +[source,cypher,role="concept"] +.Define which Java artifacts are part of internal or public API +---- +MATCH (c:Java) - [:ANNOTATED_BY] -> (a) - [:OF_TYPE] -> (t:Type {fqn: 'org.apiguardian.api.API'}), + (a) - [:HAS] -> ({name: 'status'}) - [:IS] -> (s) +WHERE ANY (label IN labels(c) WHERE label in ['Interface', 'Class', 'Annotation']) +WITH c, trim(split(s.signature, ' ')[1]) AS status +WITH c, status, + CASE status + WHEN 'INTERNAL' THEN 'Internal' + ELSE 'Public' + END AS type +MERGE (a:Api {type: type, status: status}) +MERGE (c) - [:IS_PART_OF] -> (a) +RETURN c,a +---- === Internal API See ADR-003. [[api:internal]] -[source,cypher,role=constraint] +[source,cypher,role="constraint",requiresConcepts="api:api-guardian-api-concept"] .Public classes that are only part of internal API must be final ---- -MATCH (c:Class), - (c) - [:ANNOTATED_BY] -> (a) - [:OF_TYPE] -> (t:Type {fqn: 'org.apiguardian.api.API'}), - (a) - [:HAS] -> ({name: 'status'}) - [:IS] -> (s) -WHERE s.signature =~ ".*INTERNAL" - AND c.visibility = 'public' - AND not exists(c.final) +MATCH (c:Class) - [:IS_PART_OF] -> (:Api {type: 'Internal'}) +WHERE c.visibility = 'public' + AND NOT exists(c.final) RETURN c.name ---- diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java index 063291d47..bc56f97b7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -44,7 +44,6 @@ public interface NodeManager { @Nullable Transaction getTransaction(); - @API(status = API.Status.EXPERIMENTAL) Object executeQuery(String query); /** @@ -64,7 +63,4 @@ public interface NodeManager { * @param managedEntity Object to be removed */ void delete(Object managedEntity); - - - } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java index fe3fcce77..d54c3d840 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java @@ -41,7 +41,7 @@ import org.springframework.lang.Nullable; */ @API(status = API.Status.STABLE, since = "1.0") @Slf4j -public final class NodeManagerFactory { +public final class NodeManagerFactory { /** * Driver that is used to create new sessions, either by directly invoking it or through Springs transactional utils. @@ -100,7 +100,6 @@ public final class NodeManagerFactory { * * @param scanner */ - @API(status = API.Status.INTERNAL, since = "1.0") public void setScanner(Scanner scanner) { Objects.requireNonNull(scanner, "A node manager factory requires a scanner."); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index 0ae41211e..facd2a9cb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -55,6 +55,7 @@ import org.springframework.util.Assert; * * @author Michael J. Simons */ +@API(status = API.Status.STABLE, since = "1.0") @Slf4j public class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements BeanFactoryAware { @@ -63,7 +64,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager @Nullable private NodeManagerFactory nodeManagerFactory; - @API(status = API.Status.STABLE, since = "1.0") public Neo4jTransactionManager(Driver driver) { this.driver = driver; } From 4f59ccb355f9931065cdc7c5778229543c777f71 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 12 Apr 2019 11:27:38 +0200 Subject: [PATCH 062/342] =?UTF-8?q?Define=20the=20Neo4j=20as=20interface,?= =?UTF-8?q?=20make=20the=20Neo4jMappingContext=20it=E2=80=99s=20premier=20?= =?UTF-8?q?implementation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/neo4j/core/NodeManagerFactory.java | 46 +++---- .../tracking/EntityComparisonStrategy.java | 9 +- .../mapping/DefaultNeo4jPersistentEntity.java | 74 ++++++++++- .../DefaultNeo4jPersistentProperty.java | 39 ++++++ .../DefaultRelationshipDescription.java} | 24 ++-- .../MappingContextBasedScannerImpl.java | 120 ----------------- .../core/mapping/Neo4jMappingContext.java | 124 +++++++++++++++++- .../core/mapping/Neo4jPersistentEntity.java | 15 ++- .../core/mapping/Neo4jPersistentProperty.java | 4 +- ...ion.java => GraphPropertyDescription.java} | 36 +++-- .../data/neo4j/core/schema/IdDescription.java | 2 +- .../neo4j/core/schema/NodeDescription.java | 78 ++--------- .../core/schema/RelationshipDescription.java | 23 ++-- .../data/neo4j/core/schema/Scanner.java | 41 ------ .../data/neo4j/core/schema/Schema.java | 64 +++------ ...Neo4jRepositoryConfigurationExtension.java | 2 +- .../support/NodeManagerFactoryBean.java | 3 +- .../DefaultPersistenceContextTest.java | 19 ++- .../EntityComparisonStrategyTest.java | 35 +++-- ...Test.java => Neo4jMappingContextTest.java} | 39 +++--- .../data/neo4j/core/schema/SchemaTest.java | 41 ------ 21 files changed, 388 insertions(+), 450 deletions(-) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/{schema/IllegalSchemaChangeException.java => mapping/DefaultRelationshipDescription.java} (60%) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/{PropertyDescription.java => GraphPropertyDescription.java} (56%) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/{MappingContextBasedScannerImplTest.java => Neo4jMappingContextTest.java} (67%) delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java index d54c3d840..bea2767f7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java @@ -21,15 +21,13 @@ package org.springframework.data.neo4j.core; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import org.apiguardian.api.API; import org.neo4j.driver.Driver; -import org.springframework.data.neo4j.core.schema.Scanner; import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; import org.springframework.lang.Nullable; @@ -43,20 +41,19 @@ import org.springframework.lang.Nullable; @Slf4j public final class NodeManagerFactory { + private final AtomicBoolean initialized = new AtomicBoolean(false); + /** * Driver that is used to create new sessions, either by directly invoking it or through Springs transactional utils. */ private final Driver driver; /** - * The initial set of classes that will be scanned in {@link #initialize()} to build the schema for node managers + * The initial set of classes that will be registered with the schema {@link #initialize()} to build the schema for node managers * belonging to this factory. */ private final Set> initialPersistentClasses; - /** The scanner used to scan the initial set of persistent classes for creating a schema. */ - private Scanner scanner = new NoopScanner(); - @Nullable private Schema schema; @@ -67,7 +64,7 @@ public final class NodeManagerFactory { *

* Spring Boots autoconfiguration for SDN RX will make sure that the same driver is used for both concerns. * - * @param driver The driver used to obtain statement runners from when creating instances of node managers. + * @param driver The driver used to obtain statement runners from when creating instances of node managers. * @param initialPersistentClasses The set of classes that should be initially scanned */ public NodeManagerFactory(Driver driver, Class... initialPersistentClasses) { @@ -87,7 +84,10 @@ public final class NodeManagerFactory { */ public NodeManager createNodeManager() { - Objects.requireNonNull(schema, "A schema is required. Did you call #initialize() before using this factory?"); + if (!initialized.get()) { + throw new IllegalStateException( + "This factory has not been correctly initialized. Please provider a schema and register your persistent classes."); + } // The call here to our Spring transaction shim has to be rethought in case we move this out of a Spring scope. // I dropped all the methods to configure that in an effort to make the setup more simple. ^mjs return new DefaultNodeManager(schema, Neo4jClient.create(driver), @@ -95,15 +95,12 @@ public final class NodeManagerFactory { } /** - * Configures the scanner used to build a schema for domain objects. This method is not to be called from application - * code and only used by internal API. + * Provides the schema for this node manager factory. The schemas has to be set before a node manager is retrieved from this factory. * - * @param scanner + * @param schema */ - public void setScanner(Scanner scanner) { - - Objects.requireNonNull(scanner, "A node manager factory requires a scanner."); - this.scanner = scanner; + public void setSchema(@Nullable Schema schema) { + this.schema = schema; } /** @@ -112,19 +109,10 @@ public final class NodeManagerFactory { */ public void initialize() { - log.info("Initializing schema with {} persistent classes", this.initialPersistentClasses.size()); - this.schema = scanner.scan(Collections.unmodifiableSet(this.initialPersistentClasses)); - } - - /** - * A noop implementation of a Schema scanner, only provided to create instances of a {@link NodeManagerFactory} that - * are in a valid state without booting up a whole context. - */ - private static class NoopScanner implements Scanner { - - @Override - public Schema scan(Collection> persistentClasses) { - return new Schema(); + if (this.initialized.compareAndSet(false, true)) { + Objects.requireNonNull(schema, "A schema is required. Did you provide one with #setSchema() beforehand?"); + log.info("Initializing schema with {} persistent classes", this.initialPersistentClasses.size()); + this.schema.register(this.initialPersistentClasses); } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java index f8eac2f76..bd65b1c12 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java @@ -31,8 +31,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.PropertyDescription; /** * Dirty tracking strategy based on property comparison. The strategy can compare two states of the same instance on @@ -40,6 +40,7 @@ import org.springframework.data.neo4j.core.schema.PropertyDescription; * will get compared. * * @author Gerrit Meier + * @author Michael J. Simons */ public class EntityComparisonStrategy implements EntityTrackingStrategy { @@ -74,7 +75,7 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { EntityState(NodeDescription nodeDescription, Object entity, Function objectIdentifyingFuction) { - Collection properties = nodeDescription.getProperties(); + Collection properties = nodeDescription.getGraphProperties(); this.objectFields = getFieldsFromProperties(entity, properties); this.objectIdentifyingFuction = objectIdentifyingFuction; @@ -105,13 +106,13 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { return changes; } - private List getFieldsFromProperties(Object entity, Collection properties) { + private List getFieldsFromProperties(Object entity, Collection properties) { List fields = new ArrayList<>(); Class entityClass = entity.getClass(); Map classFields = retrieveAllFields(entityClass); - for (PropertyDescription property : properties) { + for (GraphPropertyDescription property : properties) { fields.add(classFields.get(property.getFieldName())); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index 47fe9489e..3efb55a79 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -18,8 +18,19 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; /** @@ -30,19 +41,70 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity idDescription; + + private final Lazy> graphProperties; + + /** + * A view on all simple properties stored on a node. + */ + private Lazy> properties; + DefaultNeo4jPersistentEntity(TypeInformation information) { super(information); - Node nodeAnnotation = this.findAnnotation(Node.class); - if (nodeAnnotation == null || nodeAnnotation.labels().length != 1) { - primaryLabel = this.getType().getSimpleName(); - } else { - primaryLabel = nodeAnnotation.labels()[0]; - } + this.primaryLabel = computePrimaryLabel(); + this.idDescription = Lazy.of(() -> computeIdDescription()); + this.graphProperties = Lazy.of(() -> computeGraphProperties()); } @Override public String getPrimaryLabel() { return primaryLabel; } + + @Override + public Class getUnderlyingClass() { + return getType(); + } + + @Override + public IdDescription getIdDescription() { + return this.idDescription.get(); + } + + @Override + public Collection getGraphProperties() { + return this.graphProperties.get(); + } + + String computePrimaryLabel() { + + Node nodeAnnotation = this.findAnnotation(Node.class); + if (nodeAnnotation == null || nodeAnnotation.labels().length != 1) { + return this.getType().getSimpleName(); + } else { + return nodeAnnotation.labels()[0]; + } + } + + IdDescription computeIdDescription() { + + final Neo4jPersistentProperty idProperty = this.getRequiredIdProperty(); + final Optional optionalIdAnnotation = Optional + .ofNullable(AnnotatedElementUtils.findMergedAnnotation(idProperty.getField(), Id.class)); + return optionalIdAnnotation + .map(idAnnotation -> new IdDescription(idAnnotation.strategy(), idAnnotation.generator())) + .orElseGet(() -> new IdDescription()); + } + + Collection computeGraphProperties() { + + final List computedGraphProperties = new ArrayList<>(); + + doWithProperties( + (Neo4jPersistentProperty property) -> computedGraphProperties.add(property)); + + return Collections.unmodifiableCollection(computedGraphProperties); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java index d3988047e..c5b6ffd90 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java @@ -18,11 +18,15 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.Optional; + import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.Lazy; /** * @author Michael J. Simons @@ -30,6 +34,8 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty implements Neo4jPersistentProperty { + private final Lazy> graphPropertyName; + /** * Creates a new {@link AnnotationBasedPersistentProperty}. * @@ -42,6 +48,8 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty computeGraphPropertyName()); } @Override @@ -55,5 +63,36 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty computeGraphPropertyName() { + if (this.isAssociation()) { + return Optional.empty(); + } + + org.springframework.data.neo4j.core.schema.Property propertyAnnotation = + this.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); + + String targetName = this.getName(); + if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty() + && propertyAnnotation.name().trim().length() != 0) { + targetName = propertyAnnotation.name().trim(); + } + + return Optional.of(targetName); + } + + @Override + public String getFieldName() { + return this.getName(); + } + + @Override + public String getPropertyName() { + return this.graphPropertyName.get().orElseThrow(() -> new MappingException("This property is not mapped to a Graph property!")); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java similarity index 60% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java index a8ba17691..2e35b22ae 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java @@ -16,19 +16,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.schema; +package org.springframework.data.neo4j.core.mapping; -import org.apiguardian.api.API; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import org.springframework.data.neo4j.core.schema.RelationshipDescription; /** - * Exception to be thrown when any change to a {@link Schema} would lead to an inconsistent state. - * * @author Michael J. Simons */ -@API(status = API.Status.INTERNAL, since = "1.0") -public final class IllegalSchemaChangeException extends IllegalArgumentException { +@RequiredArgsConstructor +@Getter +@ToString +@EqualsAndHashCode(of = { "type", "target" }) +class DefaultRelationshipDescription implements RelationshipDescription { - IllegalSchemaChangeException(String s) { - super(s); - } + private final String type; + + private final String target; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java deleted file mode 100644 index 2e1c713de..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImpl.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.mapping; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -import org.apiguardian.api.API; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.SimpleAssociationHandler; -import org.springframework.data.mapping.SimplePropertyHandler; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.IdDescription; -import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.PropertyDescription; -import org.springframework.data.neo4j.core.schema.Relationship; -import org.springframework.data.neo4j.core.schema.RelationshipDescription; -import org.springframework.data.neo4j.core.schema.Scanner; -import org.springframework.data.neo4j.core.schema.Schema; - -/** - * @author Michael J. Simons - */ -@API(status = API.Status.INTERNAL, since = "1.0") -public final class MappingContextBasedScannerImpl implements Scanner { - - private final Neo4jMappingContext neo4jMappingContext; - - public MappingContextBasedScannerImpl(Neo4jMappingContext neo4jMappingContext) { - this.neo4jMappingContext = neo4jMappingContext; - } - - @Override - public Schema scan(Collection> persistentClasses) { - - final Schema schema = new Schema(); - persistentClasses.forEach(clazz -> { - Neo4jPersistentEntity m = neo4jMappingContext.getPersistentEntity(clazz); - schema.registerNodeDescription(describeAsNode(m)); - }); - - return schema; - } - - private NodeDescription describeAsNode(Neo4jPersistentEntity entity) { - - List properties = new ArrayList<>(); - - // TODO break this up into separate methods. - entity.doWithProperties(new SimplePropertyHandler() { - @Override - public void doWithPersistentProperty(PersistentProperty persistentProperty) { - org.springframework.data.neo4j.core.schema.Property propertyAnnotation = - persistentProperty.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); - - String propertyName = persistentProperty.getName(); - if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty() - && propertyAnnotation.name().trim().length() != 0) { - propertyName = propertyAnnotation.name().trim(); - } - - properties.add(new PropertyDescription(persistentProperty.getName(), propertyName)); - } - }); - - List relationships = new ArrayList<>(); - entity.doWithAssociations(new SimpleAssociationHandler() { - @Override - public void doWithAssociation(Association> association) { - - Neo4jPersistentEntity obverseOwner = neo4jMappingContext - .getPersistentEntity(association.getInverse().getAssociationTargetType()); - - Relationship outgoingRelationship = association.getInverse().findAnnotation(Relationship.class); - String type; - if (outgoingRelationship != null && outgoingRelationship.type() != null) { - type = outgoingRelationship.type(); - } else { - type = association.getInverse().getName(); - } - relationships.add(new RelationshipDescription(type, obverseOwner.getPrimaryLabel())); - } - }); - - final Neo4jPersistentProperty idProperty = entity.getRequiredIdProperty(); - final Optional optionalIdAnnotation = Optional - .ofNullable(AnnotatedElementUtils.findMergedAnnotation(idProperty.getField(), Id.class)); - final IdDescription idDescription = optionalIdAnnotation - .map(idAnnotation -> new IdDescription(idAnnotation.strategy(), idAnnotation.generator())) - .orElseGet(() -> new IdDescription()); - - return NodeDescription.builder() - .underlyingClass(entity.getType()) - .primaryLabel(entity.getPrimaryLabel()) - .idDescription(idDescription) - .properties(properties) - .relationships(relationships) - .build(); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 25660c23a..2e829092a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -18,17 +18,47 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Predicate; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.neo4j.core.schema.RelationshipDescription; +import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.data.util.TypeInformation; /** - * {@link org.springframework.data.mapping.context.MappingContext} implementation for Neo4j. + * An implementation of both a {@link Schema} as well as a Neo4j version of Spring Datas + * {@link org.springframework.data.mapping.context.MappingContext}. It is recommended to provide + * the initial set of classes through {@link #setInitialEntitySet(Set)}. * * @author Michael J. Simons */ -public class Neo4jMappingContext extends AbstractMappingContext, Neo4jPersistentProperty> { +public class Neo4jMappingContext + extends AbstractMappingContext, Neo4jPersistentProperty> implements Schema { + + /** + * A lookup of entities based on their primary label. We depend on the locking mechanism provided by the + * {@link AbstractMappingContext}, so this lookup is not synchronized further. + */ + private final Map> nodeDescriptionsByPrimaryLabel = new HashMap<>(); + + private final ConcurrentMap> relationshipsByPrimaryLabel = new ConcurrentHashMap<>(); /* * (non-Javadoc) @@ -37,7 +67,37 @@ public class Neo4jMappingContext extends AbstractMappingContext Neo4jPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - return new DefaultNeo4jPersistentEntity<>(typeInformation); + final DefaultNeo4jPersistentEntity newEntity = new DefaultNeo4jPersistentEntity<>(typeInformation); + String primaryLabel = newEntity.getPrimaryLabel(); + + if (this.nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel)) { + // @formatter:off + throw new MappingException( + String.format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s", + primaryLabel)); + // @formatter:on + } + + if (this.nodeDescriptionsByPrimaryLabel.containsValue(newEntity)) { + Optional label = this.nodeDescriptionsByPrimaryLabel.entrySet().stream() + .filter(e -> e.getValue().equals(newEntity)).map( + Map.Entry::getKey).findFirst(); + + throw new MappingException( + String.format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s", + newEntity, label.orElse("n/a"))); + } + + this.getNodeDescription(newEntity.getUnderlyingClass()).ifPresent(existingDescription -> { + + throw new MappingException(String.format(Locale.ENGLISH, + "The schema already contains description with the underlying class %s under the primary label %s", + newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel())); + }); + + this.nodeDescriptionsByPrimaryLabel.put(primaryLabel, newEntity); + + return newEntity; } /* @@ -50,4 +110,62 @@ public class Neo4jMappingContext extends AbstractMappingContext> entityClasses) { + + this.setInitialEntitySet(entityClasses); + this.initialize(); + } + + @Override + public Optional> getNodeDescription(String primaryLabel) { + return Optional.ofNullable(this.nodeDescriptionsByPrimaryLabel.get(primaryLabel)); + } + + NodeDescription getRequiredNodeDescription(String primaryLabel) { + return this.getNodeDescription(primaryLabel) + .orElseThrow( + () -> new MappingException( + String.format("Required node description not found with primary label '%s'", primaryLabel))); + } + + @Override + public Optional> getNodeDescription(Class underlyingClass) { + + Predicate underlyingClassMatches = n -> n.getUnderlyingClass().equals(underlyingClass); + return this.nodeDescriptionsByPrimaryLabel.values().stream().filter(underlyingClassMatches).findFirst(); + } + + @Override + public Collection getRelationshipsOf(String primaryLabel) { + + return this.relationshipsByPrimaryLabel.computeIfAbsent(primaryLabel, this::thing); + } + + Collection thing(String primaryLabel) { + + NodeDescription nodeDescription = getRequiredNodeDescription(primaryLabel); + + final List relationships = new ArrayList<>(); + + Neo4jPersistentEntity entity = this.getPersistentEntity(nodeDescription.getUnderlyingClass()); + entity.doWithAssociations((Association association) -> { + + Neo4jPersistentEntity obverseOwner = this + .getPersistentEntity(association.getInverse().getAssociationTargetType()); + + Relationship outgoingRelationship = association.getInverse().findAnnotation(Relationship.class); + String type; + if (outgoingRelationship != null && outgoingRelationship.type() != null) { + type = outgoingRelationship.type(); + } else { + type = association.getInverse().getName(); + } + + relationships.add(new DefaultRelationshipDescription(type, obverseOwner.getPrimaryLabel())); + }); + + return Collections.unmodifiableCollection(relationships); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java index 980ea0b1f..1b6c3035a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java @@ -19,18 +19,21 @@ package org.springframework.data.neo4j.core.mapping; import org.apiguardian.api.API; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.MutablePersistentEntity; +import org.springframework.data.neo4j.core.schema.NodeDescription; /** * A {@link org.springframework.data.mapping.PersistentEntity} interface with additional methods for metadata related to Neo4j. * + * Both Spring Data methods {@link #doWithProperties(PropertyHandler)} and {@link #doWithAssociations(AssociationHandler)} are + * aware which field of a class is meant to be mapped as a property of a node or a relationship or if it is a relationship + * (in Spring Data terms: if it is an association). + * * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -public interface Neo4jPersistentEntity extends MutablePersistentEntity { - - /** - * @return The primary label of this entity inside Neo4j. - */ - String getPrimaryLabel(); +public interface Neo4jPersistentEntity + extends MutablePersistentEntity, NodeDescription { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java index 39129c81a..4eddb37b1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.core.mapping; import org.apiguardian.api.API; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; /** * A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related to Neo4j. @@ -27,5 +28,6 @@ import org.springframework.data.mapping.PersistentProperty; * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -public interface Neo4jPersistentProperty extends PersistentProperty { +public interface Neo4jPersistentProperty + extends PersistentProperty, GraphPropertyDescription { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java similarity index 56% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java index 52632fdb5..c6544fc06 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java @@ -21,29 +21,25 @@ package org.springframework.data.neo4j.core.schema; import org.apiguardian.api.API; /** + * Provides minimal information how to map class attributes to the properties of a node or a relationship. + *

+ * Spring Datas persistent properties have slightly different semantics. They have an entity centric approach of properties. + * Spring Data properties contain - if not marked otherwise - also associations. + *

+ * Associations between different node types can be queried on the {@link Schema} itself. + * * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class PropertyDescription { +public interface GraphPropertyDescription { - private final String fieldName; + /** + * @return The name of the attribute of the mapped class + */ + String getFieldName(); - private final String propertyName; - - // TODO basically all the properties from Springs PersistentProperty are needed. - // Two options: Turn that classes ending in XXXDescription into interfaces and let Neo4jPersistentProperty extend - // from it as well, or copy needed stuff into a format that fits our needs best. - - public PropertyDescription(String fieldName, String propertyName) { - this.fieldName = fieldName; - this.propertyName = propertyName; - } - - public String getFieldName() { - return fieldName; - } - - public String getPropertyName() { - return propertyName; - } + /** + * @return The name of the property as stored in the graph. + */ + String getPropertyName(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java index eec45b8e4..4f36f4b8e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java @@ -21,7 +21,7 @@ package org.springframework.data.neo4j.core.schema; import org.apiguardian.api.API; /** - * Description howto generate Ids for entities. + * Description how to generate Ids for entities. * * @author Michael J. Simons */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java index 86385f80a..6db8bb213 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -18,16 +18,7 @@ */ package org.springframework.data.neo4j.core.schema; -import lombok.Builder; - -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; import org.apiguardian.api.API; @@ -35,78 +26,29 @@ import org.apiguardian.api.API; * Describes how a class is mapped to a node inside the database. It provides navigable links to relationships and * access to the nodes properties. * + * @param The type of the underlying class * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class NodeDescription { +public interface NodeDescription { /** - * The class being mapped to the node. + * @return The primary label of this entity inside Neo4j. */ - private final Class underlyingClass; + String getPrimaryLabel(); /** - * The primary label of this node. + * @return The concrete class to which a node with the given {@link #getPrimaryLabel()} is mapped to */ - private final String primaryLabel; - - private final IdDescription idDescription; - - private final Map propertiesByFieldName; - - private final List relationships; - - @Builder - private NodeDescription(Class underlyingClass, String primaryLabel, IdDescription idDescription, - List properties, - List relationships) { - - this.underlyingClass = underlyingClass; - this.primaryLabel = primaryLabel; - this.idDescription = idDescription; - this.propertiesByFieldName = properties.stream() - .collect(Collectors.toMap(PropertyDescription::getFieldName, Function.identity())); - this.relationships = new ArrayList<>(relationships); - } - - public Class getUnderlyingClass() { - return underlyingClass; - } + Class getUnderlyingClass(); /** - * @return The primary label of this node + * @return A description how to determine primary ids for nodes fitting this description */ - public String getPrimaryLabel() { - return primaryLabel; - } - - public IdDescription getIdDescription() { - return idDescription; - } + IdDescription getIdDescription(); /** - * @return The properties of this node + * @return A collection of persistent properties that are mapped to graph properties and not to relationships */ - public Collection getProperties() { - return Collections.unmodifiableCollection(propertiesByFieldName.values()); - } - - /** - * Retrieves a properties description by its field name. - * - * @param fieldName The field name under which the node is described - * @return The description if any - */ - public Optional getPropertyDescription(String fieldName) { - return Optional.ofNullable(this.propertiesByFieldName.get(fieldName)); - } - - /** - * This returns the outgoing relationships this node has to other nodes directions. - * - * @return The relationships defined by instances of this node. - */ - public Collection getRelationships() { - return Collections.unmodifiableCollection(relationships); - } + Collection getGraphProperties(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java index 95a690765..c78b16373 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java @@ -18,26 +18,27 @@ */ package org.springframework.data.neo4j.core.schema; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; -import lombok.ToString; - import org.apiguardian.api.API; /** * Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction * is maybe defined on the {@link NodeDescription} reachable in the {@link Schema} via it's primary label defined by - * {@link #target}. + * {@link #getTarget}. * * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -@RequiredArgsConstructor -@ToString -@EqualsAndHashCode(of = { "type", "target" }) -public final class RelationshipDescription { +public interface RelationshipDescription { - private final String type; + /** + * @return The type of this relationship + */ + String getType(); - private final String target; + /** + * The target of this relationship is described by the primary label of the node in question. + * + * @return The target of this relationship + */ + String getTarget(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java deleted file mode 100644 index 17526df6e..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Scanner.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.schema; - -import java.util.Collection; - -import org.apiguardian.api.API; - -/** - * A scanner produces an instance of schema. The default scanner is based on all the information we can get from - * a Spring Mapping context (in our case, the {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext}. - * - * @author Michael J. Simons - */ -@API(status = API.Status.STABLE, since = "1.0") -public interface Scanner { - - /** - * Scans the relevant classes and creates a schema. - * - * @param persistentClasses The classes to scan - * @return The new schema. - */ - Schema scan(Collection> persistentClasses); -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java index 7b95362eb..793fff766 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -18,60 +18,26 @@ */ package org.springframework.data.neo4j.core.schema; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; +import java.util.Collection; import java.util.Optional; -import java.util.function.Predicate; +import java.util.Set; import org.apiguardian.api.API; /** * Contains the descriptions of all nodes, their properties and relationships known to SDN-RX. * - * The schema is currently designed to be mutual. + * @author Michael J. Simons */ @API(status = API.Status.STABLE, since = "1.0") -public final class Schema { - - private final Map nodeDescriptionsByPrimaryLabel = new HashMap<>(); +public interface Schema { /** - * Registers a node description under it's primary label. + * Registers and scans the given set of classes to be available as Neo4j domain entities. * - * @param newDescription The new node description. - * @return This schema. - * @throws IllegalSchemaChangeException when the description is already registered (under any label) + * @param entityClasses The additional set of classes to register with this schema */ - public Schema registerNodeDescription(NodeDescription newDescription) { - - String primaryLabel = newDescription.getPrimaryLabel(); - if (this.nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel)) { - throw new IllegalSchemaChangeException(String - .format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s", - primaryLabel)); - } - - if (this.nodeDescriptionsByPrimaryLabel.containsValue(newDescription)) { - Optional label = this.nodeDescriptionsByPrimaryLabel.entrySet().stream() - .filter(e -> e.getValue().equals(newDescription)).map( - Map.Entry::getKey).findFirst(); - - throw new IllegalSchemaChangeException(String - .format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s", - newDescription, label.orElse("n/a"))); - } - - this.getNodeDescription(newDescription.getUnderlyingClass()).ifPresent(existingDescription -> { - throw new IllegalSchemaChangeException(String - .format(Locale.ENGLISH, - "The schema already contains description with the underlying class %s under the primary label %s", - newDescription.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel())); - }); - - this.nodeDescriptionsByPrimaryLabel.put(primaryLabel, newDescription); - return this; - } + void register(Set> entityClasses); /** * Retrieves a nodes description by its primary label. @@ -79,9 +45,7 @@ public final class Schema { * @param primaryLabel The primary label under which the node is described * @return The description if any */ - public Optional getNodeDescription(String primaryLabel) { - return Optional.ofNullable(this.nodeDescriptionsByPrimaryLabel.get(primaryLabel)); - } + Optional> getNodeDescription(String primaryLabel); /** * Retrieves a nodes description by its underlying class. @@ -89,9 +53,13 @@ public final class Schema { * @param underlyingClass The underlying class of the node description to be retrieved * @return The description if any */ - public Optional getNodeDescription(Class underlyingClass) { + Optional> getNodeDescription(Class underlyingClass); - Predicate underlyingClassMatches = n -> n.getUnderlyingClass().equals(underlyingClass); - return this.nodeDescriptionsByPrimaryLabel.values().stream().filter(underlyingClassMatches).findFirst(); - } + /** + * This returns the outgoing relationships this node has to other nodes. + * + * @param primaryLabel The primary label of the node whos relationships should be retrieved + * @return The relationships defined by instances of this node. + */ + Collection getRelationshipsOf(String primaryLabel); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index de603666c..f8a2271d1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -111,7 +111,7 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati String nameOfMappingContextBean = registerWithSourceAndGeneratedBeanName( neo4jMappingContextBeanDefinition, registry, config); - // Augmented node manager factory (creating injectable, shared instances of NodeManager) + // Augmented node manager factory (creating injectable, shared instances of NodeManager that are aware of the mapping context). String nameOfNodeManagerFactory = config.getAttribute("nodeManagerFactoryRef") .orElse(DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME); AbstractBeanDefinition sharedSessionCreatorBeanDefinition = BeanDefinitionBuilder diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java index 36d068ef3..5a265f7a2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java @@ -25,7 +25,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.NodeManagerFactory; -import org.springframework.data.neo4j.core.mapping.MappingContextBasedScannerImpl; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; /** @@ -57,7 +56,7 @@ public final class NodeManagerFactoryBean implements InitializingBean, FactoryBe @Override public void afterPropertiesSet() { - target.setScanner(new MappingContextBasedScannerImpl(this.neo4jMappingContext)); + target.setSchema(this.neo4jMappingContext); target.initialize(); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java index a0b00b743..71f987f7a 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java @@ -18,18 +18,19 @@ */ package org.springframework.data.neo4j.core.context; -import static java.util.Collections.*; import static org.mockito.Mockito.*; +import java.util.Arrays; +import java.util.HashSet; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy; -import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.PropertyDescription; -import org.springframework.data.neo4j.core.schema.Schema; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; /** * @author Gerrit Meier + * @author Michael J. Simons */ class DefaultPersistenceContextTest { @@ -42,13 +43,9 @@ class DefaultPersistenceContextTest { when(entityTrackingStrategy.getObjectIdentifier(any())) .thenAnswer(invocation -> System.identityHashCode(invocation.getArguments()[0])); - Schema schema = new Schema(); - PropertyDescription valuePropertyDescription = new PropertyDescription("value", "value"); - NodeDescription somethingNodeDescription = NodeDescription.builder() - .properties(singletonList(valuePropertyDescription)).relationships(emptyList()).underlyingClass(Something.class) - .build(); - - schema.registerNodeDescription(somethingNodeDescription); + Neo4jMappingContext schema = new Neo4jMappingContext(); + schema.setInitialEntitySet(new HashSet>(Arrays.asList(Something.class))); + schema.initialize(); // override method to return a verifiable mock context = new DefaultPersistenceContext(schema) { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java index b729e4031..54b1a3f72 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java @@ -18,8 +18,8 @@ */ package org.springframework.data.neo4j.core.context.tracking; -import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import java.util.ArrayList; import java.util.Arrays; @@ -29,23 +29,42 @@ import java.util.List; import java.util.Objects; import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.PropertyDescription; /** * @author Gerrit Meier */ class EntityComparisonStrategyTest { - private PropertyDescription valuePropertyDescription = new PropertyDescription("value", "value"); + private final GraphPropertyDescription valuePropertyDescription; - private PropertyDescription informationPropertyDescription = new PropertyDescription("information", "information"); + private final GraphPropertyDescription informationPropertyDescription; - private PropertyDescription parentPropertyDescription = new PropertyDescription("parentValue", "parentValue"); + private final GraphPropertyDescription parentPropertyDescription; - private NodeDescription description = NodeDescription.builder().primaryLabel("Something") - .properties(Arrays.asList(valuePropertyDescription, informationPropertyDescription, parentPropertyDescription)) - .relationships(emptyList()).underlyingClass(Something.class).build(); + private final NodeDescription description; + + EntityComparisonStrategyTest() { + this.valuePropertyDescription = mock(GraphPropertyDescription.class); + when(this.valuePropertyDescription.getFieldName()).thenReturn("value"); + when(this.valuePropertyDescription.getPropertyName()).thenReturn("value"); + + this.informationPropertyDescription = mock(GraphPropertyDescription.class); + when(this.informationPropertyDescription.getFieldName()).thenReturn("information"); + when(this.informationPropertyDescription.getPropertyName()).thenReturn("information"); + + this.parentPropertyDescription = mock(GraphPropertyDescription.class); + when(this.parentPropertyDescription.getFieldName()).thenReturn("parentValue"); + when(this.parentPropertyDescription.getPropertyName()).thenReturn("parentValue"); + + this.description = mock(NodeDescription.class); + when(this.description.getPrimaryLabel()).thenReturn("Something"); + when(this.description.getGraphProperties()).thenReturn(Arrays + .asList(this.valuePropertyDescription, this.informationPropertyDescription, + this.parentPropertyDescription)); + when(this.description.getUnderlyingClass()).thenReturn(Something.class); + } @Test void trackSimplePropertyChange() { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java similarity index 67% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index d02a0d224..4a5d41df1 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/MappingContextBasedScannerImplTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -21,37 +21,33 @@ package org.springframework.data.neo4j.core.mapping; import static org.assertj.core.api.Assertions.*; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Property; -import org.springframework.data.neo4j.core.schema.PropertyDescription; import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.core.schema.RelationshipDescription; -import org.springframework.data.neo4j.core.schema.Scanner; -import org.springframework.data.neo4j.core.schema.Schema; /** * @author Michael J. Simons */ -class MappingContextBasedScannerImplTest { +public class Neo4jMappingContextTest { @Test void initializationOfSchemaShouldWork() { - Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); - neo4jMappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class))); - neo4jMappingContext.initialize(); + Neo4jMappingContext schema = new Neo4jMappingContext(); + schema.setInitialEntitySet(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class))); + schema.initialize(); - final Scanner scanner = new MappingContextBasedScannerImpl(neo4jMappingContext); - Schema schema = scanner.scan(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class))); - - Optional optionalUserNodeDescription = schema.getNodeDescription("User"); + Optional> optionalUserNodeDescription = schema.getNodeDescription("User"); assertThat(optionalUserNodeDescription) .isPresent() .hasValueSatisfying(description -> { @@ -60,16 +56,16 @@ class MappingContextBasedScannerImplTest { assertThat(description.getIdDescription().getIdStrategy()) .isEqualTo(Id.Strategy.INTERNAL); - assertThat(description.getProperties()) - .extracting(PropertyDescription::getFieldName) + assertThat(description.getGraphProperties()) + .extracting(GraphPropertyDescription::getFieldName) .containsExactlyInAnyOrder("id", "name", "first_name"); - assertThat(description.getProperties()) - .extracting(PropertyDescription::getPropertyName) + assertThat(description.getGraphProperties()) + .extracting(GraphPropertyDescription::getPropertyName) .containsExactlyInAnyOrder("id", "name", "firstName"); }); - Optional optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); + Optional> optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); assertThat(optionalBikeNodeDescription) .isPresent() .hasValueSatisfying(description -> { @@ -78,10 +74,13 @@ class MappingContextBasedScannerImplTest { assertThat(description.getIdDescription().getIdStrategy()) .isEqualTo(Id.Strategy.ASSIGNED); - assertThat(description.getRelationships()) - .containsExactlyInAnyOrder( - new RelationshipDescription("owner", "User"), - new RelationshipDescription("renter", "User")); + Collection expectedRelationships = Arrays.asList("[:owner] -> (:User)", "[:renter] -> (:User)"); + + Collection relationships = schema + .getRelationshipsOf(description.getPrimaryLabel()); + assertThat(relationships) + .allMatch(d -> expectedRelationships + .contains(String.format("[:%s] -> (:%s)", d.getType(), d.getTarget()))); }); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java deleted file mode 100644 index 749a5a0a7..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.schema; - -import static org.assertj.core.api.Assertions.*; - -import java.util.Collections; - -import org.junit.jupiter.api.Test; - -/** - * @author Michael J. Simons - */ -class SchemaTest { - - @Test - void shouldGetNodeDescription() { - NodeDescription description = NodeDescription.builder().primaryLabel("aLabel") - .properties(Collections.emptyList()).relationships(Collections.emptyList()).build(); - - Schema schema = new Schema().registerNodeDescription(description); - assertThat(schema.getNodeDescription("aLabel")).isPresent().contains(description); - assertThat(schema.getNodeDescription("anotherLabel")).isNotPresent(); - } -} From c28739741e8748ae0bfab3e7c53ce9f94d526f9e Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 24 Apr 2019 10:58:41 +0200 Subject: [PATCH 063/342] Use explicit transactions to be able to rollback. --- .../core/DefaultReactiveNeo4jClient.java | 46 ++++++++++++++----- .../neo4j/core/ReactiveNeo4jClientTest.java | 41 +++++++++++------ .../neo4j/core/TransactionHandlingTest.java | 18 ++++++-- 3 files changed, 78 insertions(+), 27 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 959232fb1..22a14e4a6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -34,6 +34,7 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; +import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.summary.ResultSummary; import org.reactivestreams.Publisher; import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; @@ -61,15 +62,25 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { // Internal helper methods for managing transactional state Mono getStatementRunner(final String targetDatabase) { - return retrieveReactiveTransaction(driver, targetDatabase) - .map(RxStatementRunner.class::cast) - // Open the transaction inside a supplier to avoid eager initialization - .switchIfEmpty(Mono.fromSupplier(() -> driver.rxSession(defaultSessionParameters(targetDatabase)))); + .switchIfEmpty( + Mono.using(() -> driver.rxSession(defaultSessionParameters(targetDatabase)), + session -> Mono.from(session.beginTransaction()), RxSession::close) + ) + .map(RxStatementRunner.class::cast); + } + + static boolean isSpringTransactionManagementActive() { + // TODO Something along the lines of if TransactionSynchronizationManager#isSynchronizationActive but for reactive + return false; } static Publisher asyncComplete(RxStatementRunner runner) { + if (runner instanceof RxTransaction && !isSpringTransactionManagementActive()) { + return ((RxTransaction) runner).commit(); + } + if (runner instanceof RxSession) { return ((RxSession) runner).close(); } @@ -77,10 +88,18 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return Mono.empty(); } + static Publisher asyncCancel(RxStatementRunner runner) { + return asyncError(runner); + } + static Publisher asyncError(RxStatementRunner runner) { - if (runner instanceof RxSession) { - return ((RxSession) runner).close(); + if (runner instanceof RxTransaction && !isSpringTransactionManagementActive()) { + return ((RxTransaction) runner).rollback(); + } + + if (runner instanceof RxTransaction) { + ((RxTransaction) runner).commit(); } return Mono.empty(); @@ -207,7 +226,8 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { getStatementRunner(targetDatabase), runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).single(), DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError + DefaultReactiveNeo4jClient::asyncError, + DefaultReactiveNeo4jClient::asyncCancel ); } @@ -218,7 +238,8 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { getStatementRunner(targetDatabase), runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).next(), DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError + DefaultReactiveNeo4jClient::asyncError, + DefaultReactiveNeo4jClient::asyncCancel ); } @@ -229,7 +250,8 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { getStatementRunner(targetDatabase), runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)), DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError + DefaultReactiveNeo4jClient::asyncError, + DefaultReactiveNeo4jClient::asyncCancel ); } @@ -240,7 +262,8 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { getStatementRunner(targetDatabase), runner -> prepareStatement().flatMap(t -> Mono.from(runner.run(t.getT1(), t.getT2()).summary())), DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError + DefaultReactiveNeo4jClient::asyncError, + DefaultReactiveNeo4jClient::asyncCancel ); } } @@ -266,7 +289,8 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { getStatementRunner(targetDatabase), callback::apply, DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError); + DefaultReactiveNeo4jClient::asyncError, + DefaultReactiveNeo4jClient::asyncCancel); } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java index 2221d193f..5daf07564 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java @@ -49,6 +49,7 @@ import org.neo4j.driver.Values; import org.neo4j.driver.internal.SessionParameters; import org.neo4j.driver.reactive.RxResult; import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.summary.ResultSummary; import org.springframework.data.neo4j.core.Neo4jClientTest.Bike; import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwner; @@ -76,6 +77,9 @@ class ReactiveNeo4jClientTest { @Mock private RxResult statementResult; + @Mock + private RxTransaction transaction; + @Mock private ResultSummary resultSummary; @@ -94,13 +98,16 @@ class ReactiveNeo4jClientTest { .thenReturn(sessionParametersTemplate); when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(session.beginTransaction()).thenReturn(Mono.just(transaction)); + + when(transaction.commit()).thenReturn(Mono.empty()); when(session.close()).thenReturn(Mono.empty()); } @AfterEach void verifyNoMoreInteractionsWithMocks() { - verifyNoMoreInteractions(driver, session, statementResult, resultSummary, record1, record2); + verifyNoMoreInteractions(driver, session, transaction, statementResult, resultSummary, record1, record2); } @@ -108,7 +115,7 @@ class ReactiveNeo4jClientTest { @DisplayName("Creation of queries and binding parameters should feel natural") void queryCreationShouldFeelGood() { - when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -141,18 +148,19 @@ class ReactiveNeo4jClientTest { expectedParameters.putAll(parameters); expectedParameters.put("name", "michael"); expectedParameters.put("aDate", LocalDate.of(2019, 1, 1)); - verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(transaction).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).records(); verify(record1).asMap(); verify(record2).asMap(); + verify(transaction).commit(); verify(session).close(); } @Test void databaseSelectionShouldBePossibleOnlyOnce() { - when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -174,9 +182,10 @@ class ReactiveNeo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "Someone.*"); - verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(transaction).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).records(); verify(record1).asMap(); + verify(transaction).commit(); verify(session).close(); } @@ -198,6 +207,7 @@ class ReactiveNeo4jClientTest { verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verify(transaction).commit(); verify(session).close(); } @@ -216,6 +226,7 @@ class ReactiveNeo4jClientTest { verifyDatabaseSelection("aDatabase"); + verify(transaction).commit(); verify(session).close(); } } @@ -227,7 +238,7 @@ class ReactiveNeo4jClientTest { @Test void reading() { - when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.records()).thenReturn(Flux.just(record1)); when(record1.get("name")).thenReturn(Values.value("michael")); @@ -252,16 +263,17 @@ class ReactiveNeo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "michael"); - verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(transaction).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).records(); verify(record1).get("name"); + verify(transaction).commit(); verify(session).close(); } @Test void writing() { - when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.summary()).thenReturn(Mono.just(resultSummary)); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -286,8 +298,9 @@ class ReactiveNeo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "Michael"); - verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(transaction).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).summary(); + verify(transaction).commit(); verify(session).close(); } @@ -295,7 +308,7 @@ class ReactiveNeo4jClientTest { @DisplayName("Some automatic conversion is ok") void automaticConversion() { - when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.records()).thenReturn(Flux.just(record1)); when(record1.size()).thenReturn(1); when(record1.get(0)).thenReturn(Values.value(23L)); @@ -314,7 +327,8 @@ class ReactiveNeo4jClientTest { verifyDatabaseSelection(DEFAULT_DATABASE_NAME); - verify(session).run(eq(cypher), anyMap()); + verify(transaction).run(eq(cypher), anyMap()); + verify(transaction).commit(); verify(session).close(); } } @@ -323,7 +337,7 @@ class ReactiveNeo4jClientTest { @DisplayName("Queries that return nothing should fit in") void queriesWithoutResultShouldFitInAsWell() { - when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.summary()).thenReturn(Mono.just(resultSummary)); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -344,8 +358,9 @@ class ReactiveNeo4jClientTest { Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "fixie"); - verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); + verify(transaction).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).summary(); + verify(transaction).commit(); verify(session).close(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java index 568bdd9d8..a236849d5 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java @@ -41,6 +41,7 @@ import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.internal.SessionParameters; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; +import org.neo4j.driver.reactive.RxTransaction; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.transaction.support.TransactionTemplate; @@ -141,6 +142,9 @@ class TransactionHandlingTest { @Mock private RxSession session; + @Mock + private RxTransaction transaction; + @Test public void shouldNotOpenTransactionsWithoutSubscription() { @@ -159,6 +163,7 @@ class TransactionHandlingTest { public void shouldFallbackToImplicitTransaction() { when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(session.beginTransaction()).thenReturn(Mono.just(transaction)); DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); @@ -170,13 +175,16 @@ class TransactionHandlingTest { .verifyComplete(); verify(driver).rxSession(any(Consumer.class)); - verifyZeroInteractions(driver, session); + verify(session).close(); + verifyZeroInteractions(driver, session, transaction); } @Test public void shouldCloseUnmanagedSessionOnComplete() { when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(session.beginTransaction()).thenReturn(Mono.just(transaction)); + when(transaction.commit()).thenReturn(Mono.empty()); when(session.close()).thenReturn(Mono.empty()); DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); @@ -190,14 +198,17 @@ class TransactionHandlingTest { .verifyComplete(); verify(driver).rxSession(any(Consumer.class)); + verify(transaction).commit(); verify(session).close(); - verifyZeroInteractions(driver, session); + verifyZeroInteractions(driver, session, transaction); } @Test public void shouldCloseUnmanagedSessionOnError() { when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(session.beginTransaction()).thenReturn(Mono.just(transaction)); + when(transaction.rollback()).thenReturn(Mono.empty()); when(session.close()).thenReturn(Mono.empty()); DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); @@ -211,8 +222,9 @@ class TransactionHandlingTest { .verify(); verify(driver).rxSession(any(Consumer.class)); + verify(transaction).rollback(); verify(session).close(); - verifyZeroInteractions(driver, session); + verifyZeroInteractions(driver, session, transaction); } } } From 03497c5bd39a4f275d6e73858f07b53f08847785 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 24 Apr 2019 14:28:50 +0200 Subject: [PATCH 064/342] =?UTF-8?q?Don=E2=80=99t=20rollback=20on=20cancel.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes the previous commit after discussions with Pivotal. The cancel signals the flux to be cancelled, not the query. The convincing argument here is: If a request goes away after n items, the requestor probably expect those n items to be committed, so we cannot rollback everything. --- .../neo4j/core/DefaultReactiveNeo4jClient.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 22a14e4a6..105037d79 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -22,6 +22,7 @@ import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUt import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -50,6 +51,7 @@ import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; * @since 1.0 * @soundtrack Die Toten Hosen - Im Auftrag des Herrn */ +@Slf4j class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { private final Driver driver; @@ -81,15 +83,14 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return ((RxTransaction) runner).commit(); } - if (runner instanceof RxSession) { - return ((RxSession) runner).close(); - } - return Mono.empty(); } static Publisher asyncCancel(RxStatementRunner runner) { - return asyncError(runner); + + log.debug("Request has been cancelled, ongoing transactions will be committed."); + + return asyncComplete(runner); } static Publisher asyncError(RxStatementRunner runner) { @@ -98,10 +99,6 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return ((RxTransaction) runner).rollback(); } - if (runner instanceof RxTransaction) { - ((RxTransaction) runner).commit(); - } - return Mono.empty(); } From 8e3271977d6dd95405c4765da92ec9fec4423db5 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 24 Apr 2019 15:49:09 +0200 Subject: [PATCH 065/342] Provide an internal Cypher DSL as foundation for query generation. Squashed commit of the following: commit baef5ae66f059a4bc598746bff44011cd70c33f1 Author: Michael Simons Date: Wed Apr 24 15:45:52 2019 +0200 Final round of polishing. commit cb7dbb968c04039294e217a11ec83b30500a092c Author: Gerrit Meier Date: Wed Apr 24 13:00:46 2019 +0200 Polishing. commit 3d13eb7a544465e65a4321079acf7213b59580c7 Author: Michael Simons Date: Tue Apr 23 18:21:28 2019 +0200 Second round of polishing. commit dafea7939cd51c0387d475f11588298438d18e1c Author: Michael Simons Date: Tue Apr 23 12:05:31 2019 +0200 First round of polishing. commit 7423bb8187de475ff0f99c36f4f86dd122345a78 Author: Michael Simons Date: Thu Apr 18 17:18:14 2019 +0200 Add support for relationships. commit 7d8ba42eb79d5880ed4ecc1a09a316984e81a134 Author: Michael Simons Date: Thu Apr 18 12:08:58 2019 +0200 Babysteps towards relations. commit 30345fcbec00eaf6ad407b195743fc57ee294a6d Author: Michael Simons Date: Wed Apr 17 16:24:45 2019 +0200 Clean up and make checkstyle happy. commit c76069452e5030b5dcf003238f98648e1a7faaa7 Author: Michael Simons Date: Tue Apr 16 22:46:31 2019 +0200 rewrite all the stuff commit f2ef2d93db66d03ae06a52343ef019683871e844 Author: Michael Simons Date: Mon Apr 15 17:18:16 2019 +0200 Add super simple condition. commit faf9705255423f40c7ece18ad0e7ce66083a460e Author: Michael J. Simons Date: Sat Apr 13 21:14:31 2019 +0200 Fix at least the test. commit b97b48375939c255022fdae9588f01aca7c5c714 Author: Michael Simons Date: Fri Apr 12 17:55:20 2019 +0200 First naiv drop of a simple Cypher AST + renderer. --- .../data/neo4j/core/context/package-info.java | 2 +- .../data/neo4j/core/cypher/Aliased.java | 36 +++ .../neo4j/core/cypher/AliasedExpression.java | 71 +++++ .../data/neo4j/core/cypher/Comparison.java | 68 +++++ .../data/neo4j/core/cypher/Condition.java | 30 ++ .../data/neo4j/core/cypher/Conditions.java | 46 ++++ .../data/neo4j/core/cypher/Cypher.java | 77 ++++++ .../core/cypher/DefaultStatementBuilder.java | 93 +++++++ .../data/neo4j/core/cypher/Expression.java | 43 +++ .../data/neo4j/core/cypher/Literal.java | 44 +++ .../data/neo4j/core/cypher/Match.java | 50 ++++ .../data/neo4j/core/cypher/Named.java | 32 +++ .../data/neo4j/core/cypher/Node.java | 258 ++++++++++++++++++ .../data/neo4j/core/cypher/Pattern.java | 39 +++ .../neo4j/core/cypher/PatternElement.java | 28 ++ .../data/neo4j/core/cypher/Property.java | 68 +++++ .../data/neo4j/core/cypher/ReadingClause.java | 28 ++ .../data/neo4j/core/cypher/RegularQuery.java | 28 ++ .../data/neo4j/core/cypher/Relationship.java | 122 +++++++++ .../neo4j/core/cypher/RelationshipDetail.java | 69 +++++ .../data/neo4j/core/cypher/Relationships.java | 61 +++++ .../data/neo4j/core/cypher/Return.java | 34 +++ .../data/neo4j/core/cypher/ReturnItem.java | 46 ++++ .../neo4j/core/cypher/SinglePartQuery.java | 51 ++++ .../data/neo4j/core/cypher/Statement.java | 44 +++ .../neo4j/core/cypher/StatementBuilder.java | 82 ++++++ .../data/neo4j/core/cypher/StringLiteral.java | 36 +++ .../data/neo4j/core/cypher/SymbolicName.java | 41 +++ .../data/neo4j/core/cypher/Where.java | 47 ++++ .../data/neo4j/core/cypher/package-info.java | 9 + .../core/cypher/renderer/CypherRenderer.java | 45 +++ .../core/cypher/renderer/RenderUtils.java | 45 +++ .../neo4j/core/cypher/renderer/Renderer.java | 30 ++ .../cypher/renderer/RenderingVisitor.java | 179 ++++++++++++ .../core/cypher/renderer/package-info.java | 4 + .../cypher/support/ReflectiveVisitor.java | 124 +++++++++ .../core/cypher/support/TypedSubtree.java | 51 ++++ .../neo4j/core/cypher/support/Visitable.java | 57 ++++ .../neo4j/core/cypher/support/Visitor.java | 42 +++ .../core/cypher/support/package-info.java | 4 + .../neo4j/core/cypher/ComparisonTest.java | 44 +++ .../data/neo4j/core/cypher/CypherTest.java | 110 ++++++++ .../cypher/DefaultStatementBuilderTest.java | 63 +++++ .../data/neo4j/core/cypher/NodeTest.java | 51 ++++ .../data/neo4j/core/cypher/PropertyTest.java | 43 +++ .../neo4j/core/cypher/RelationshipTest.java | 38 +++ .../core/cypher/renderer/RenderUtilsTest.java | 43 +++ 47 files changed, 2655 insertions(+), 1 deletion(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationships.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnItem.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtils.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/package-info.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitable.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitor.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/package-info.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtilsTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java index 7def88a09..0673137de 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java @@ -1,5 +1,5 @@ /** - * Contains all necessary infrastructur for the context of a session, i.e. dirty tracking and related. + * Contains all necessary infrastructure for the context of a session, i.e. dirty tracking and related. * * @author Michael J. Simons */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java new file mode 100644 index 000000000..c4d9ec494 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +/** + * An element with an alias. An alias has a subtle difference to a symbolic name in cypher. Nodes and relationships can + * have symbolic names which in turn can be aliased as well. + *

+ * Therefor, the Cypher generator needs both {@code Named} and {@code Aliased}. + * + * @author Michael J. Simons + * @since 1.0 + */ +public interface Aliased { + + /** + * @return the alias. + */ + String getAlias(); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java new file mode 100644 index 000000000..4b8b69c7b --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.Optional; + +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; + +/** + * An aliased expression, that deals with named expressions when accepting visitors. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class AliasedExpression implements Aliased, Expression { + + private final Expression delegate; + + private final String alias; + + AliasedExpression(Expression delegate, String alias) { + + this.delegate = delegate; + this.alias = alias; + } + + public String getAlias() { + return alias; + } + + /** + * This takes the originally aliased expression and re-aliases it. Aliases are not nested. + * + * @param newAlias The new alias to use + * @return A new aliased, expression. + */ + @Override + public AliasedExpression as(String newAlias) { + + Assert.hasText(newAlias, "The alias may not be null or empty."); + return new AliasedExpression(this.delegate, newAlias); + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + // Make sure we deal nicely with symbolic names. + Optional.of(delegate).filter(Named.class::isInstance).map(Named.class::cast).flatMap(Named::getSymbolicName) + .map(Expression.class::cast) + .orElse(delegate).accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java new file mode 100644 index 000000000..962be6b84 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; + +/** + * A concrete condition representing a comparision between two expressions. + * + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +public class Comparison implements Condition { + + static Comparison create(Expression lhs, String comparator, Expression rhs) { + + Assert.notNull(lhs, "Left expression must not be null."); + Assert.hasText(comparator, "Comparator must not be empty."); + Assert.notNull(rhs, "Right expression must not be null."); + + return new Comparison(lhs, comparator, rhs); + } + + private final Expression left; + private final String comparator; + private final Expression right; + + private Comparison(Expression left, String comparator, Expression right) { + + this.left = left; + this.comparator = comparator; + this.right = right; + } + + public String getComparator() { + return comparator; + } + + @Override + public void accept(Visitor visitor) { + + left.accept(visitor); + + visitor.enter(this); + visitor.leave(this); + + right.accept(visitor); + } +} + diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java new file mode 100644 index 000000000..6d98c1fa1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * Shared interface for all conditions. + * + * @author Michael J. Simons + * @since 1.0 + */ +public interface Condition extends Visitable { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java new file mode 100644 index 000000000..55eea4073 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +/** + * Builder for various conditions. Used internally from properties and other expressions that should take part in conditions. + * + * @author Michael J. Simons + * @since 1.0 + */ +public final class Conditions { + + /** + * Creates a condition that matches if the right hand side is a regular expression that matches the the left hand side via + * {@code =~}. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparions + * @return A "matches" comparision + */ + static Condition matches(Expression lhs, Expression rhs) { + return Comparison.create(lhs, "=~", rhs); + } + + /** + * Not to be instantiated. + */ + private Conditions() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java new file mode 100644 index 000000000..d7001f06f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; +import org.springframework.lang.Nullable; + +/** + * The main entry point into the Cypher DSL. + * + * The Cypher Builder API is intended for framework usage to produce Cypher statements required for database operations. + * + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Cypher { + + /** + * Create a new Node representation with at least one label, the "primary" label. This is required. All other labels + * are optional. + * + * @param primaryLabel The primary label this node is identified by. + * @param additionalLabels Additional labels + * @return A new node representation + */ + public static Node node(String primaryLabel, String... additionalLabels) { + + return Node.create(primaryLabel, additionalLabels); + } + + /** + * Creates a new {@link StringLiteral} from the {@code content}. + * + * @param content the literal content. + * @return a new {@link StringLiteral}. + */ + public static StringLiteral literalOf(@Nullable CharSequence content) { + return new StringLiteral(content); + } + + /** + * Starts building a statement based on a match clause. Use {@link Cypher#node(String, String...)} and related to + * retrieve a node or a relationship, which both are pattern elements. + * + * @param pattern The patterns to match + * @return An ongoing match that is used to specify an optional where and a required return clause + */ + public static OngoingMatch match(PatternElement... pattern) { + + return Statement.builder().match(pattern); + } + + /** + * Not to be instantiated. + */ + private Cypher() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java new file mode 100644 index 000000000..9f19c56a8 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static java.util.stream.Collectors.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndReturn; +import org.springframework.util.Assert; + +/** + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +class DefaultStatementBuilder + implements StatementBuilder, OngoingMatch, OngoingMatchAndReturn { + + private List matchList = new ArrayList<>(); + private List returnList = new ArrayList<>(); + private Condition where; + + @Override + public OngoingMatch match(PatternElement... pattern) { + + Assert.notNull(pattern, "Patterns to match are required."); + Assert.notEmpty(pattern, "At least one pattern to match is required."); + + this.matchList.addAll(Arrays.asList(pattern)); + return this; + } + + @Override + public OngoingMatchAndReturn returning(Expression... expressions) { + + Assert.notNull(expressions, "Expressions to return are required."); + Assert.notEmpty(expressions, "At least one expressions to return is required."); + + this.returnList.addAll(Arrays.stream(expressions) + .map(expression -> expression instanceof Named ? + ((Named) expression).getSymbolicName().map(Expression.class::cast).orElse(expression) : + expression) + .collect(toList())); + return this; + } + + @Override + public OngoingMatchAndReturn returning(Node... nodes) { + + Assert.notNull(nodes, "Nodes to return are required."); + Assert.notEmpty(nodes, "At least one node to return is required."); + + this.returnList.addAll(Arrays.stream(nodes) + .map(node -> node.getSymbolicName().map(Expression.class::cast).orElse(node)) + .collect(toList())); + return this; + } + + @Override + public OngoingMatch where(Condition condition) { + + this.where = condition; + return this; + } + + @Override + public Statement build() { + + Pattern pattern = new Pattern(this.matchList); + Match match = new Match(pattern, this.where == null ? null : new Where(this.where)); + return new SinglePartQuery(match, new Return(returnList.stream().map(ReturnItem::new).collect(toList()))); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java new file mode 100644 index 000000000..48aaa2a34 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.util.Assert; + +/** + * An expression can be used in many places, i.e. in return statements, pattern elements etc. + * + * @author Michael J. Simons + * @since 1.0 + */ +public interface Expression extends Visitable { + + /** + * Creates an expression with an alias. This expression does not track which or how many aliases have been created. + * + * @param alias The alias to use + * @return An aliased expression. + */ + default AliasedExpression as(String alias) { + + Assert.hasText(alias, "The alias may not be null or empty."); + return new AliasedExpression(this, alias); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java new file mode 100644 index 000000000..3fbc2e989 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.lang.Nullable; + +/** + * Represents a literal with an optional content. + * + * @author Michael J. Simons + * @since 1.0 + */ +public abstract class Literal implements Expression { + + private @Nullable T content; + + Literal(@Nullable T content) { + this.content = content; + } + + public @Nullable T getContent() { + return content; + } + + @Override + public abstract String toString(); +} + diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java new file mode 100644 index 000000000..bcfc4800b --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; + +/** + * See Match. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class Match implements ReadingClause { + + private final Pattern pattern; + + private @Nullable final Where optionalWhere; + + Match(Pattern pattern, @Nullable Where optionalWhere) { + this.pattern = pattern; + this.optionalWhere = optionalWhere; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + this.pattern.accept(visitor); + Visitable.visitIfNotNull(optionalWhere, visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java new file mode 100644 index 000000000..1f464deb2 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.Optional; + +/** + * A named thing exposes an optional {@link #getSymbolicName() symbolic name}. + * + * @author Michael J. Simons + * @since 1.0 + */ +public interface Named { + + Optional getSymbolicName(); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java new file mode 100644 index 000000000..48e97fec2 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.neo4j.core.cypher.Relationship.Direction; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * See NodePattern. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class Node implements PatternElement, Named, Expression { + + static Node create(String primaryLabel, String... additionalLabels) { + + Assert.hasText(primaryLabel, "A primary label is required."); + + for (String additionalLabel : additionalLabels) { + Assert.hasText(additionalLabel, "An empty label is not allowed."); + } + + return new Node(primaryLabel, additionalLabels); + } + + private @Nullable final SymbolicName symbolicName; + + private final List labels; + + Node(String primaryLabel, String... additionalLabels) { + + this.symbolicName = null; + + this.labels = new ArrayList<>(); + this.labels.add(primaryLabel); + this.labels.addAll(Arrays.asList(additionalLabels)); + } + + Node(SymbolicName symbolicName, List labels) { + + this.symbolicName = symbolicName; + this.labels = new ArrayList<>(labels); + } + + /** + * Creates a copy of that node with a new symbolic name. + * + * @param newSymbolicName the new symbolic name. + * @return The new node. + */ + public Node named(String newSymbolicName) { + + Assert.hasText(newSymbolicName, "Symbolic name is required."); + return new Node(new SymbolicName(newSymbolicName), labels); + } + + public Optional getSymbolicName() { + return Optional.ofNullable(symbolicName); + } + + public List getLabels() { + return Collections.unmodifiableList(labels); + } + + public boolean isLabeled() { + return !this.labels.isEmpty(); + } + + /** + * Creates a new {@link Property} associated with this property container.. + *

+ * Note: The property container does not track property creation and there is no possibility to enumerate all + * properties that have been created for this node. + * + * @param name property name, must not be {@literal null} or empty. + * @return a new {@link Property} associated with this {@link Node}. + */ + public Property property(String name) { + + return Property.create(this, name); + } + + public OngoingRelationshipDefinition outgoingRelationShipTo(Node other) { + return new DefaultOngoingRelationshipDefinition(this, Direction.LTR, other); + } + + public OngoingRelationshipDefinition incomingRelationShipFrom(Node other) { + return new DefaultOngoingRelationshipDefinition(this, Direction.RTR, other); + } + + public OngoingRelationshipDefinition relationshipWith(Node other) { + return new DefaultOngoingRelationshipDefinition(this, Direction.UNI, other); + } + + /** + * Exposes {@code withType) and terminal operations for creating a relationship. + * + * @param

The final thing to be defined + */ + public interface OngoingRelationshipDefinition

+ extends OngoingRelationshipDefinitionWithType

, OngoingRelationshipDefinitionWithSymbolicName

{ + + /** + * Specifies the type of the relationship. + * + * @param types Zero or more types for this relationship + * @return The ongoing relationship definition + */ + OngoingRelationshipDefinitionWithType

withType(String... types); + } + + /** + * Exposes {@code withType} to specify types for the relationship or the the last element of a chain of relationships, + * {@code as} to specify a symbolic name for the last element in the chain as well as terminal operations + * to create the chain or continue it with the next hop. + * + * @param

The final thing to be defined + */ + public interface OngoingRelationshipDefinitionWithType

extends OngoingRelationshipDefinitionWithSymbolicName

{ + + /** + * Gives the relationship a new, symbolic name. + * + * @param symbolicName The symbolic name to use. + * @return The ongoing relationship definition + */ + OngoingRelationshipDefinitionWithSymbolicName

named(String symbolicName); + } + + /** + * @param

The final thing to be defined + */ + public interface OngoingRelationshipDefinitionWithSymbolicName

{ + + /** + * Creates the chain of relationships. + * + * @return A chain of relationship with at least one element. + */ + P create(); + + /** + * Adds a new link to this chain of relationships. + * + * @param tripNode The end node of the next link + * @return The ongoing relationship definition + */ + OngoingRelationshipDefinition outgoingRelationShipTo(Node tripNode); + } + + @RequiredArgsConstructor + private static class DefaultOngoingRelationshipDefinition + implements OngoingRelationshipDefinition { + + private final Node left; + + private final Direction direction; + + private final Node right; + + private String[] types = new String[0]; + + private String symbolicName; + + @Override + public OngoingRelationshipDefinitionWithType withType(@SuppressWarnings("HiddenField") String... types) { + this.types = types; + return this; + } + + @Override + public Relationship create() { + return Relationship.create(left, direction, right, symbolicName, types); + } + + @Override + public OngoingRelationshipDefinitionWithSymbolicName named(@SuppressWarnings("HiddenField") String symbolicName) { + this.symbolicName = symbolicName; + return this; + } + + @Override + public OngoingRelationshipDefinition outgoingRelationShipTo(Node next) { + return new DefaultOngoingRelationshipsDefinition(this.create(), + new DefaultOngoingRelationshipDefinition(right, Direction.LTR, next)); + } + } + + private static class DefaultOngoingRelationshipsDefinition + implements OngoingRelationshipDefinition { + + private final List chain; + + private OngoingRelationshipDefinition nextElement; + + DefaultOngoingRelationshipsDefinition( + Relationship firstElement, + OngoingRelationshipDefinition nextElement) { + this.chain = new ArrayList<>(); + this.chain.add(firstElement); + this.nextElement = nextElement; + } + + @Override + public OngoingRelationshipDefinitionWithType withType(String... types) { + this.nextElement.withType(types); + return this; + } + + @Override + public OngoingRelationshipDefinitionWithSymbolicName named(String symbolicName) { + this.nextElement.named(symbolicName); + return this; + } + + @Override + public Relationships create() { + this.chain.add(this.nextElement.create()); + return new Relationships(this.chain); + } + + @Override + public OngoingRelationshipDefinition outgoingRelationShipTo(Node next) { + Relationship lastRelationship = this.nextElement.create(); + this.chain.add(lastRelationship); + this.nextElement = + new DefaultOngoingRelationshipDefinition(lastRelationship.getRight(), Direction.LTR, next); + + return this; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java new file mode 100644 index 000000000..cb3b7d6a8 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.List; + +import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; + +/** + * A pattern is something that can be matched. It consists of one or more pattern elements. Those can be nodes or chains + * of nodes and relationships. + *

+ * See Pattern. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class Pattern extends TypedSubtree { + + Pattern(List patternElements) { + super(patternElements); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java new file mode 100644 index 000000000..d56b57ed5 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +public interface PatternElement extends Visitable { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java new file mode 100644 index 000000000..fe5eb807f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.util.Assert; + +/** + * A property that belongs to a property container (either Node or Relationship). + * + * @author Michael J. Simons + * @since 1.0 + */ +public class Property implements Expression { + + static Property create(Node parentContainer, String name) { + + Assert.state(parentContainer.getSymbolicName().isPresent(), + "A property derived from a node needs a parent with a symbolic name."); + Assert.hasText(name, "The properties name is required."); + + return new Property(parentContainer, name); + } + + /** + * The property container this property belongs to. + */ + private final Node parentContainer; + + /** + * The name of this property. + */ + private final String name; + + Property(Node parentContainer, String name) { + + this.parentContainer = parentContainer; + this.name = name; + } + + public String getParentAlias() { + return parentContainer.getSymbolicName().get().getName(); + } + + public String getName() { + return name; + } + + public Condition matches(String s) { + + return Conditions.matches(this, Cypher.literalOf(s)); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java new file mode 100644 index 000000000..0ac176e4a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +public interface ReadingClause extends Visitable { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java new file mode 100644 index 000000000..3a3270fac --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +/** + * See RegularQuery. + * + * @author Michael J. Simons + * @since 1.0 + */ +public interface RegularQuery { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java new file mode 100644 index 000000000..be08e761d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.Arrays; +import java.util.Optional; + +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * See RelationshipPattern. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class Relationship implements PatternElement, Named, Expression { + + /** + * While the direction in the schema package is centered around the node, the direction here is the direction between two nodes. + */ + public enum Direction { + /** + * Left to right + */ + LTR("-", "->"), + /** + * Right to left + */ + RTR("<-", "-"), + /** + * None + */ + UNI("-", "-"); + + Direction(String symbolLeft, String symbolRight) { + this.symbolLeft = symbolLeft; + this.symbolRight = symbolRight; + } + + private final String symbolLeft; + + private final String symbolRight; + + public String getSymbolLeft() { + return symbolLeft; + } + + public String getSymbolRight() { + return symbolRight; + } + } + + static Relationship create(Node left, + @Nullable Direction direction, Node right, @Nullable String symbolicName, String... types) { + + Assert.notNull(left, "Left node is required."); + Assert.notNull(right, "Right node is required."); + + RelationshipDetail details = new RelationshipDetail( + Optional.ofNullable(direction).orElse(Direction.UNI), + Optional.ofNullable(symbolicName).map(SymbolicName::new).orElse(null), Arrays.asList(types)); + return new Relationship(left, details, right); + } + + private final Node left; + + private final Node right; + + private final RelationshipDetail details; + + Relationship(Node left, RelationshipDetail details, Node right) { + this.left = left; + this.right = right; + this.details = details; + } + + public Node getLeft() { + return left; + } + + public Node getRight() { + return right; + } + + public RelationshipDetail getDetails() { + return details; + } + + public Optional getSymbolicName() { + return details.getSymbolicName(); + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + + left.accept(visitor); + details.accept(visitor); + right.accept(visitor); + + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java new file mode 100644 index 000000000..51e7efaef --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.neo4j.core.cypher.Relationship.Direction; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.lang.Nullable; + +/** + * See RelationshipDetail + * + * @author Michael J. Simons + * @since 1.0 + */ +public class RelationshipDetail implements Visitable { + + /** + * The direction between the nodes of the relationship. + */ + private final Direction direction; + + private @Nullable final SymbolicName symbolicName; + + private final List types; + + RelationshipDetail(Direction direction, + @Nullable SymbolicName symbolicName, List types) { + this.direction = direction; + this.symbolicName = symbolicName; + this.types = new ArrayList<>(types); + } + + public Direction getDirection() { + return direction; + } + + public Optional getSymbolicName() { + return Optional.ofNullable(symbolicName); + } + + public List getTypes() { + return Collections.unmodifiableList(types); + } + + public boolean isTyped() { + return !this.types.isEmpty(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationships.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationships.java new file mode 100644 index 000000000..1be13d98b --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationships.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.springframework.data.neo4j.core.cypher.support.Visitable.*; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * Represents a chain of relationships. The chain is meant to be in order and the right node of an element is related to + * the left node of the next element. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class Relationships implements PatternElement { + + private List relationships; + + Relationships(List relationships) { + this.relationships = new ArrayList(relationships); + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + + Node lastNode = null; + for (Relationship relationship : relationships) { + + relationship.getLeft().accept(visitor); + relationship.getDetails().accept(visitor); + + lastNode = relationship.getRight(); + } + + visitIfNotNull(lastNode, visitor); + + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java new file mode 100644 index 000000000..8e77a418e --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.List; + +import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +public class Return extends TypedSubtree { + + Return(List returnItems) { + super(returnItems); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnItem.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnItem.java new file mode 100644 index 000000000..2a9a2d346 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnItem.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * This is a wrapper around an expression. + *

+ * See ReturnItem. + * + * @author Michael J. Simons + * @since 1.0 + */ +class ReturnItem implements Visitable { + + private final Expression expression; + + ReturnItem(Expression expression) { + this.expression = expression; + } + + @Override + public void accept(Visitor visitor) { + visitor.enter(this); + expression.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java new file mode 100644 index 000000000..450bdc83c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; + +/** + * See SinglePartQuery. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class SinglePartQuery implements SingleQuery { + + private @Nullable final ReadingClause readingClause; + + private final Return aReturn; + + SinglePartQuery(@Nullable ReadingClause readingClause, Return aReturn) { + this.readingClause = readingClause; + this.aReturn = aReturn; + } + + @Override + public void accept(Visitor visitor) { + + if (readingClause != null) { + readingClause.accept(visitor); + } + + aReturn.accept(visitor); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java new file mode 100644 index 000000000..4b73b8e84 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * Shall be the common interfaces for queries that we support. + *

+ * For reference see: Cypher. + * We have skipped the intermediate "Query" structure so a statement in the context of this generator is either a + * {@link RegularQuery} or a {@code StandaloneCall}. + * + * @author Michael J. Simons + * @since 1.0 + */ +public interface Statement extends Visitable { + static StatementBuilder builder() { + + return new DefaultStatementBuilder(); + } + + interface RegularQuery extends Statement { + } + + interface SingleQuery extends RegularQuery { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java new file mode 100644 index 000000000..373a945f9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +interface StatementBuilder { + + /** + * See {@link Cypher#match(PatternElement...)}. + * + * @param pattern + * @return + */ + OngoingMatch match(PatternElement... pattern); + + /** + * A match that knows what to return and which is ready to be build. + */ + interface OngoingMatchAndReturn extends BuildableMatch { + } + + /** + * A match that exposes {@code returning} and {@code where} methods to add required information. + */ + interface OngoingMatch { + + /** + * Create a match that returns one or more expressions. + * + * @param expressions The expressions to be returned. Must not be null and be at least one expression. + * @return A match that can be build now + */ + OngoingMatchAndReturn returning(Expression... expressions); + + /** + * Creates a match that returns one ore more nodes. If the nodes have a symbolic name, that symbolic name is + * used in the return clause, otherwise the whole node pattern. + * + * @param nodes The nodes to be returned. Must not be null and be at least one node. + * @return A match that can be build now + */ + OngoingMatchAndReturn returning(Node... nodes); + + /** + * Adds a where clause to this match. + * + * @param condition The new condition + * @return A match restricted by a where clause with no return items yet. + */ + OngoingMatch where(Condition condition); + } + + /** + * A match that has all information required to be build. + */ + interface BuildableMatch { + + /** + * @return The statement ready to be used, i.e. in a renderer. + */ + Statement build(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java new file mode 100644 index 000000000..8322620d9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +public class StringLiteral extends Literal { + + StringLiteral(CharSequence content) { + super(content); + } + + @Override + public String toString() { + // TODO escape all the things + return "'" + getContent() + "'"; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java new file mode 100644 index 000000000..6872dc224 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +/** + * A symbolic name to identify nodes and relationships. + *

+ * See SchemaName + * SymbolicName + * + * @author Michael J. Simons + * @since 1.0 + */ +public class SymbolicName implements Expression { + + private final String name; + + SymbolicName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java new file mode 100644 index 000000000..cfee360d7 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * Roughly corresponding to Where. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class Where implements Visitable { + + private final Condition condition; + + Where(Condition condition) { + this.condition = condition; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + + this.condition.accept(visitor); + + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java new file mode 100644 index 000000000..f59f496f0 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java @@ -0,0 +1,9 @@ +/** + * Contains an internal DSL for creating Cypher. It is part of our internal API and may change without further notice. + * + * @author Michael J. Simons + */ +@NonNullApi +package org.springframework.data.neo4j.core.cypher; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java new file mode 100644 index 000000000..38be62588 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.renderer; + +import org.springframework.data.neo4j.core.cypher.Statement; + +/** + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +public class CypherRenderer implements Renderer { + + private CypherRenderer() { + } + + public static Renderer create() { + return new CypherRenderer(); + } + + @Override + public String render(Statement statement) { + + RenderingVisitor renderingVisitor = new RenderingVisitor(); + statement.accept(renderingVisitor); + + return renderingVisitor.getRenderedContent(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtils.java new file mode 100644 index 000000000..61c7c065d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.renderer; + +import java.util.Locale; + +/** + * Helper methods dealing with the formatting of various Cypher nodes and such. + * + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +final class RenderUtils { + + /** + * Escapes a symbolic name. Such a symbolic name is either used for a nodes label, the type of a relationship or a + * variable. + * + * @param unescapedName + * @return The correctly escaped name, safe to be used in statements. + */ + static CharSequence escapeName(String unescapedName) { + return String.format(Locale.ENGLISH, "`%s`", unescapedName.replaceAll("`", "``")); + } + + private RenderUtils() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java new file mode 100644 index 000000000..520801bf3 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.renderer; + +import org.springframework.data.neo4j.core.cypher.Statement; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +public interface Renderer { + + String render(Statement statement); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java new file mode 100644 index 000000000..6a84ae013 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.renderer; + +import static java.util.stream.Collectors.*; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.neo4j.core.cypher.*; +import org.springframework.data.neo4j.core.cypher.Relationship.Direction; +import org.springframework.data.neo4j.core.cypher.support.ReflectiveVisitor; +import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * This is a simple (some would call it naive) implementation of a visitor to the Cypher AST created by the Cypher builder + * based on the {@link ReflectiveVisitor reflective visitor}. + *

+ * It takes care of separating elements of sub trees containing the element type with a separator and provides pairs of + * {@code enter} / {@code leave} for the structuring elements of the Cypher AST as needed. + *

+ * This rendering visitor is not meant to be used outside framework code and we don't give any guarantees on the format + * being output apart from that it works within the constraints of SDN-RX. + * + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +class RenderingVisitor extends ReflectiveVisitor { + + private static final String LABEL_SEPARATOR = ":"; + private static final String TYPE_SEPARATOR = ":"; + + /** + * Target of all rendering. + */ + private final StringBuilder builder = new StringBuilder(); + + /** + * Optional separator between elements. + */ + private String separator = null; + + /** + * This keeps track on which level of the tree a separator is needed. + */ + private final Set separatorOnLevel = new HashSet<>(); + + /** + * The current level in the tree of cypher elements. + */ + private int currentLevel = 0; + + private void enableSeparator(int level, boolean on) { + if (on) { + separatorOnLevel.add(level); + } else { + separatorOnLevel.remove(level); + } + this.separator = null; + } + + private boolean needsSeparator() { + return separatorOnLevel.contains(currentLevel); + } + + @Override + protected void preEnter(Visitable visitable) { + + int nextLevel = ++currentLevel + 1; + if (visitable instanceof TypedSubtree) { + enableSeparator(nextLevel, true); + } + + if (needsSeparator() && separator != null) { + builder.append(separator); + separator = null; + } + } + + @Override + protected void postLeave(Visitable visitable) { + + if (needsSeparator()) { + separator = ", "; + } + + if (visitable instanceof TypedSubtree) { + enableSeparator(currentLevel + 1, false); + } + + --currentLevel; + } + + void enter(Match match) { + builder.append("MATCH "); + } + + void leave(Match match) { + builder.append(" "); + } + + void enter(Where where) { + builder.append(" WHERE "); + } + + void enter(Return returning) { + builder.append("RETURN "); + } + + void leave(AliasedExpression aliased) { + builder.append(" AS ").append(aliased.getAlias()); + } + + void enter(Property property) { + builder + .append(property.getParentAlias()) + .append(".") + .append(property.getName()); + } + + void enter(Comparison comparison) { + builder + .append(" ") + .append(comparison.getComparator()) + .append(" "); + } + + void enter(StringLiteral expression) { + builder.append(expression.toString()); + } + + void enter(Node node) { + builder.append("(") + .append(node.getSymbolicName().map(SymbolicName::getName).orElse("")) + .append(node.isLabeled() ? LABEL_SEPARATOR : "") + .append(node.getLabels().stream().map(RenderUtils::escapeName).collect(joining(LABEL_SEPARATOR))) + .append(")"); + } + + void enter(SymbolicName symbolicName) { + builder.append(symbolicName.getName()); + } + + void enter(RelationshipDetail details) { + + Direction direction = details.getDirection(); + builder.append(direction.getSymbolLeft()); + builder + .append("[") + .append(details.getSymbolicName().map(SymbolicName::getName).orElse("")) + .append(details.isTyped() ? TYPE_SEPARATOR : "") + .append(details.getTypes().stream().map(RenderUtils::escapeName).collect(joining(TYPE_SEPARATOR))) + .append("]"); + builder.append(direction.getSymbolRight()); + } + + public String getRenderedContent() { + return this.builder.toString(); + } +} + diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/package-info.java new file mode 100644 index 000000000..796c5a552 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/package-info.java @@ -0,0 +1,4 @@ +/** + * A renderer for {@link org.springframework.data.neo4j.core.cypher.Statement statements} in the simplest form possible. + */ +package org.springframework.data.neo4j.core.cypher.renderer; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java new file mode 100644 index 000000000..1232aa082 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.support; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This is a convenience class implementing a {@link Visitor} and it takes care of choosing the right methods + * to dispatch the {@link Visitor#enter(Visitable)} and {@link Visitor#leave(Visitable)} calls to. + *

+ * Classes extending this visitor need to provide corresponding {@code enter} and {@code leave} methods taking exactly + * one argument of the type of {@link Visitable} they are interested it. + *

+ * The type must be an exact match, this support class doesn't try to find a close match up in the class hierarchy if it + * doesn't find an exact match. + * + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +public abstract class ReflectiveVisitor implements Visitor { + + /** + * Private enum to specify a visiting phase. + */ + private enum Phase { + ENTER("enter"), + LEAVE("leave"); + + final String methodName; + + Phase(String methodName) { + this.methodName = methodName; + } + } + + private final Map> cachedHandles = new ConcurrentHashMap<>(); + + /** + * This is a hook that is called with the uncasted, raw visitable just before entering a visitable. + *

+ * The hook is called regardless wither a matching {@code enter} is found or not. + * + * @param visitable The visitable that is passed on to a matching enter after this call. + */ + protected abstract void preEnter(Visitable visitable); + + /** + * This is a hook that is called with the uncasted, raw visitable just after leaving the visitable. + *

+ * The hook is called regardless wither a matching {@code leave} is found or not. + * + * @param visitable + */ + protected abstract void postLeave(Visitable visitable); + + @Override + public final void enter(Visitable visitable) { + preEnter(visitable); + executeConcreteMethodIn(new TargetAndPhase(visitable.getClass(), Phase.ENTER), visitable); + } + + @Override + public final void leave(Visitable visitable) { + executeConcreteMethodIn(new TargetAndPhase(visitable.getClass(), Phase.LEAVE), visitable); + postLeave(visitable); + } + + private void executeConcreteMethodIn(TargetAndPhase targetAndPhase, Visitable onVisitable) { + Optional optionalHandle = this.cachedHandles.computeIfAbsent(targetAndPhase, this::findHandleFor); + optionalHandle.ifPresent(handle -> { + try { + handle.invoke(onVisitable); + } catch (Throwable throwable) { + } + }); + } + + private Optional findHandleFor(TargetAndPhase targetAndPhase) { + + try { + // Using MethodHandles.lookup().findVirtual() doesn't allow to make a protected method accessible. + Method method = this.getClass() + .getDeclaredMethod(targetAndPhase.phase.methodName, targetAndPhase.classOfVisitable); + method.setAccessible(true); + return Optional.of(MethodHandles.lookup().in(this.getClass()).unreflect(method).bindTo(this)); + } catch (IllegalAccessException | NoSuchMethodException e) { + // We don't do anything if the method doesn't exists + return Optional.empty(); + } + } + + @RequiredArgsConstructor + @EqualsAndHashCode + private static class TargetAndPhase { + private final Class classOfVisitable; + + private final Phase phase; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java new file mode 100644 index 000000000..e06c7e984 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.support; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +public abstract class TypedSubtree implements Visitable { + + private final List children; + + protected TypedSubtree(T... children) { + + this.children = Arrays.asList(children); + + } + + protected TypedSubtree(List children) { + + this.children = new ArrayList<>(children); + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + this.children.forEach(child -> child.accept(visitor)); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitable.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitable.java new file mode 100644 index 000000000..3a6f1f53f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitable.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.support; + +import org.springframework.lang.Nullable; + +/** + * Interface for implementations that accepts {@link Visitor visitors}. + * + * @see Visitor + * + * @author Michael Simons + * @since 1.0 + */ +public interface Visitable { + + /** + * A helper method that presents the {@code visitor} to the {@code visitable} if the visitable is not null. + * Not meant to be overridden. + * + * @param visitable The visitable to visit if not null + * @param visitor The visitor to use + */ + static void visitIfNotNull(@Nullable Visitable visitable, Visitor visitor) { + + if (visitable != null) { + visitable.accept(visitor); + } + } + + /** + * Accept a {@link Visitor} visiting this {@link Visitable} and its nested {@link Visitable}s if applicable. + * + * @param visitor the visitor to notify, must not be {@literal null}. + */ + default void accept(Visitor visitor) { + + visitor.enter(this); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitor.java new file mode 100644 index 000000000..85e269354 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitor.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.support; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +@FunctionalInterface +public interface Visitor { + + /** + * Enter a {@link Visitable}. + * + * @param segment the segment to visit. + */ + void enter(Visitable segment); + + /** + * Leave a {@link Visitable}. + * + * @param segment the visited segment. + */ + default void leave(Visitable segment) { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/package-info.java new file mode 100644 index 000000000..8647fa100 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/package-info.java @@ -0,0 +1,4 @@ +/** + * Support for describing an abstract syntax tree. + */ +package org.springframework.data.neo4j.core.cypher.support; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java new file mode 100644 index 000000000..1d34d968a --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + * @author Gerrit Meier + */ +class ComparisonTest { + + @Test + void preconditionsShouldBeAsserted() { + + Expression expression = Cypher.literalOf("Arbitrary expression"); + assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(null, "=", expression)) + .withMessage("Left expression must not be null."); + assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(expression, "=", null)) + .withMessage("Right expression must not be null."); + assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(expression, null, expression)) + .withMessage("Comparator must not be empty."); + assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(expression, "", expression)) + .withMessage("Comparator must not be empty."); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java new file mode 100644 index 000000000..960206bce --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; +import org.springframework.data.neo4j.core.cypher.renderer.Renderer; + +/** + * @author Michael J. Simons + */ +public class CypherTest { + + private final Renderer cypherRenderer = CypherRenderer.create(); + private final Node bikeNode = Cypher.node("Bike").named("b"); + private final Node userNode = Cypher.node("User").named("u"); + + @Nested + class SingleQuerySinglePart { + + @Nested + class ReadingAndReturn { + + @Test + void unrelatedNodes() { + Statement statement = Cypher.match(bikeNode, userNode, Cypher.node("U").named("o")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`), (u:`User`), (o:`U`) RETURN b, u"); + } + + @Test + void aliasedExpressionsInReturn() { + Node unnamedNode = Cypher.node("ANode"); + Node namedNode = Cypher.node("AnotherNode").named("o"); + Statement statement = Cypher.match(unnamedNode, namedNode) + .returning(unnamedNode.as("theNode"), namedNode.as("theOtherNode")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (:`ANode`), (o:`AnotherNode`) RETURN (:`ANode`) AS theNode, o AS theOtherNode"); + } + + @Test + void simpleRelationship() { + Statement statement = Cypher + .match(userNode.outgoingRelationShipTo(bikeNode).withType("OWNS").create()) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) RETURN b, u"); + } + + @Test + void simpleRelationshipWithReturn() { + Relationship owns = userNode + .outgoingRelationShipTo(bikeNode).withType("OWNS").named("o").create(); + + Statement statement = Cypher + .match(owns) + .returning(bikeNode, userNode, owns) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[o:`OWNS`]->(b:`Bike`) RETURN b, u, o"); + } + + @Test + void chainedRelations() { + Node tripNode = Cypher.node("Trip").named("u"); + Statement statement = Cypher + .match(userNode + .outgoingRelationShipTo(bikeNode).withType("OWNS").named("r1") + .outgoingRelationShipTo(tripNode).withType("USED_ON").named("r2") + .create() + ) + .where(userNode.property("name").matches(".*aName")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`)-[r1:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(u:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); + } + } + + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java new file mode 100644 index 000000000..d1281e773 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class DefaultStatementBuilderTest { + + @Test + void matchPreconditionsShouldBeAsserted() { + + DefaultStatementBuilder builder = new DefaultStatementBuilder(); + assertThatIllegalArgumentException().isThrownBy(() -> builder.match(null)) + .withMessage("Patterns to match are required."); + assertThatIllegalArgumentException().isThrownBy(() -> builder.match()) + .withMessage("At least one pattern to match is required."); + } + + @Nested + class returningPreconditionsShouldBeAsserted { + @Test + void forExpressions() { + + DefaultStatementBuilder builder = new DefaultStatementBuilder(); + assertThatIllegalArgumentException().isThrownBy(() -> builder.returning((Expression[]) null)) + .withMessage("Expressions to return are required."); + assertThatIllegalArgumentException().isThrownBy(() -> builder.returning(new Expression[0])) + .withMessage("At least one expressions to return is required."); + } + + @Test + void forNodes() { + + DefaultStatementBuilder builder = new DefaultStatementBuilder(); + assertThatIllegalArgumentException().isThrownBy(() -> builder.returning((Node[]) null)) + .withMessage("Nodes to return are required."); + assertThatIllegalArgumentException().isThrownBy(() -> builder.returning(new Node[0])) + .withMessage("At least one node to return is required."); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java new file mode 100644 index 000000000..aeb2f46ce --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class NodeTest { + + @Test + void preconditionsShouldBeAsserted() { + String expectedMessage = "A primary label is required."; + + assertThatIllegalArgumentException().isThrownBy(() -> Node.create("")).withMessage(expectedMessage); + assertThatIllegalArgumentException().isThrownBy(() -> Node.create(" \t")).withMessage(expectedMessage); + } + + @Test + void shouldNotAddEmptyAdditionalLabels() { + + assertThatIllegalArgumentException().isThrownBy(() -> Node.create("primary", " ", "\t ")) + .withMessage("An empty label is not allowed."); + } + + @Test + void shouldCreateNodes() { + + Node node = Node.create("primary", "secondary"); + assertThat(node.getLabels()).contains("primary", "secondary"); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java new file mode 100644 index 000000000..6424ccbd1 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class PropertyTest { + + @Test + void preconditionsShouldBeAsserted() { + + assertThatIllegalStateException().isThrownBy(() -> Property.create(Node.create("a"), "")) + .withMessage("A property derived from a node needs a parent with a symbolic name."); + String expectedMessage = "The properties name is required."; + assertThatIllegalArgumentException().isThrownBy(() -> Property.create(Node.create("a").named("s"), null)) + .withMessage( + expectedMessage); + assertThatIllegalArgumentException().isThrownBy(() -> Property.create(Node.create("a").named("s"), "\t")) + .withMessage( + expectedMessage); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java new file mode 100644 index 000000000..f3192b628 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class RelationshipTest { + + @Test + void preconditionsShouldBeAsserted() { + + assertThatIllegalArgumentException().isThrownBy(() -> Relationship.create(null, null, Node.create("a"), null)) + .withMessage("Left node is required."); + assertThatIllegalArgumentException().isThrownBy(() -> Relationship.create(Node.create("a"), null, null, null)) + .withMessage("Right node is required."); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtilsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtilsTest.java new file mode 100644 index 000000000..7b4f8204e --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtilsTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher.renderer; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * @author Michael J. Simons + */ +class RenderUtilsTest { + + @ParameterizedTest + @CsvSource({ + "ALabel, `ALabel`", + "A Label, `A Label`", + "A `Label, `A ``Label`", + "`A `Label, ```A ``Label`", + "Spring Data Neo4j⚡️RX, `Spring Data Neo4j⚡️RX`" + }) + void shouldCorrectlyEscapeNames(String name, String expectedEscapedName) { + + assertThat(RenderUtils.escapeName(name)).isEqualTo(expectedEscapedName); + } +} From 6ac44358138c8c3b5c878ad287896dc335506ffd Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 24 Apr 2019 16:11:29 +0200 Subject: [PATCH 066/342] Add documentation to RecordFetchSpec. --- .../data/neo4j/core/Neo4jClient.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java index 4ab2f8cc4..23adacaa5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -176,10 +176,26 @@ public interface Neo4jClient { */ interface RecordFetchSpec { + /** + * Fetches exactly one record and throws an exception if there are zero or more than one records. + * + * @return The one and only record. + * @throws org.neo4j.driver.exceptions.NoSuchRecordException + */ S one(); + /** + * Fetches only the first record. Returns an empty holder if there are no records. + * + * @return The first record if any. + */ S first(); + /** + * Fetches all records. + * + * @return All records. + */ M all(); } From db4e3a5bf30e33bc6498168d0b6a957fc8ef19be Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 18 Apr 2019 17:16:42 +0200 Subject: [PATCH 067/342] Incoming type mapping (Part 1). Plain reflection mapping w/o any relationships. --- .../data/neo4j/core/DefaultNodeManager.java | 118 ++++++++++++++++++ .../data/neo4j/core/NodeManager.java | 8 ++ .../query/StringBasedNeo4jQuery.java | 10 +- .../neo4j/integration/PersonRepository.java | 34 ++++- .../integration/PersonWithAllConstructor.java | 40 ++++++ ...rson.java => PersonWithNoConstructor.java} | 12 +- .../neo4j/integration/PersonWithWither.java | 50 ++++++++ .../data/neo4j/integration/RepositoryIT.java | 96 ++++++++++++-- 8 files changed, 351 insertions(+), 17 deletions(-) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{Person.java => PersonWithNoConstructor.java} (84%) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithWither.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index b1816d08f..2909f4817 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -18,15 +18,30 @@ */ package org.springframework.data.neo4j.core; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; + import org.apiguardian.api.API; +import org.neo4j.driver.Record; import org.neo4j.driver.Transaction; +import org.neo4j.driver.Value; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.neo4j.core.context.DefaultPersistenceContext; import org.springframework.data.neo4j.core.context.PersistenceContext; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; +import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.lang.Nullable; /** * @author Michael J. Simons + * @author Gerrit Meier */ @API(status = API.Status.INTERNAL, since = "1.0") class DefaultNodeManager implements NodeManager { @@ -60,6 +75,43 @@ class DefaultNodeManager implements NodeManager { return neo4jClient.newQuery(query).fetch().one(); } + @Override + public Optional executeTypedQueryForObject(String query, Class returnedType) { + Optional> nodeDescriptionOptional = schema.getNodeDescription(returnedType); + // returned type can be sth. like a raw result or scalar + if (!nodeDescriptionOptional.isPresent()) { + return getTypedMappingSpec(query, returnedType).one(); + } + + return getTypedFetchSpec(query, returnedType, (NodeDescription) nodeDescriptionOptional.get()).one(); + } + + @Override + public Collection executeTypedQueryForObjects(String query, Class returnedType) { + + Optional> nodeDescriptionOptional = schema.getNodeDescription(returnedType); + // returned type can be sth. like a raw result or scalar + if (!nodeDescriptionOptional.isPresent()) { + return getTypedMappingSpec(query, returnedType).all(); + } + + return getTypedFetchSpec(query, returnedType, (NodeDescription) nodeDescriptionOptional.get()).all(); + } + + private Neo4jClient.RecordFetchSpec, Collection, T> getTypedFetchSpec(String query, + Class returnedType, NodeDescription nodeDescription) { + + Function mappingFunction = new ReflectionBasedMappingFunction(nodeDescription); + + return getTypedMappingSpec(query, returnedType).mappedBy(mappingFunction); + } + + private Neo4jClient.MappingSpec, Collection, T> getTypedMappingSpec(String query, + Class returnedType) { + + return neo4jClient.newQuery(query).fetchAs(returnedType); + } + @Override public T save(T entityWithUnknownState) { @@ -76,4 +128,70 @@ class DefaultNodeManager implements NodeManager { throw new UnsupportedOperationException("Not there yet."); } + + private class ReflectionBasedMappingFunction implements Function { + + private final NodeDescription nodeDescription; + + private ReflectionBasedMappingFunction(NodeDescription nodeDescription) { + this.nodeDescription = nodeDescription; + } + + @Override + public T apply(Record record) { + + try { + + for (Value value : record.values()) { + // todo + // 1. determine type of returned value: node or relationship + // 2. find most fitting label value.asNode().labels().forEach(); + // strictly assume just to work with nodes for now and every matching type label is the root node + if (value.asNode().hasLabel(nodeDescription.getPrimaryLabel())) { + + if (!(nodeDescription instanceof Neo4jPersistentEntity)) { + return null; + } + + Neo4jPersistentEntity entity = (Neo4jPersistentEntity) nodeDescription; + PreferredConstructor persistenceConstructor = entity + .getPersistenceConstructor(); + + // todo check because there was "something" wrong with the usage of EntityInstantiators in SDN + EntityInstantiators instantiators = new EntityInstantiators(); + + // todo: neo4jPersistentEntity.requiresPropertyPopulation() + + T instance = instantiators.getInstantiatorFor(entity).createInstance(entity, + new ParameterValueProvider() { + @Override + public Object getParameterValue(PreferredConstructor.Parameter parameter) { + return value.get(parameter.getName()).asObject(); + } + }); + + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(instance); + if (entity.requiresPropertyPopulation()) { + entity.doWithProperties((SimplePropertyHandler) property -> { + if (persistenceConstructor.isConstructorParameter(property)) { + return; + } + + propertyAccessor.setProperty(property, value.get(property.getName()).asObject()); + + }); + instance = propertyAccessor.getBean(); + } + + return instance; + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java index bc56f97b7..d1e41bfa3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -18,6 +18,9 @@ */ package org.springframework.data.neo4j.core; +import java.util.Collection; +import java.util.Optional; + import org.apiguardian.api.API; import org.neo4j.driver.Transaction; import org.springframework.lang.Nullable; @@ -46,6 +49,10 @@ public interface NodeManager { Object executeQuery(String query); + Collection executeTypedQueryForObjects(String query, Class returnedType); + + Optional executeTypedQueryForObject(String query, Class returnedType); + /** * Saves an entity. When the entity is not yet managed in this instance of the NodeManager, and will be registered as * a managed instance. In either way, the state of the entity will be written to the underlying store afterwards. @@ -63,4 +70,5 @@ public interface NodeManager { * @param managedEntity Object to be removed */ void delete(Object managedEntity); + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index 641c3e6e3..a3fc40f7a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -41,7 +41,15 @@ public class StringBasedNeo4jQuery extends AbstractNeo4jQuery { @Override public Object execute(Object[] parameters) { - return nodeManager.executeQuery(queryMethod.getAnnotatedQuery()); + + Class returnedType = queryMethod.getReturnedObjectType(); + boolean collectionQuery = queryMethod.isCollectionQuery(); + + if (collectionQuery) { + return nodeManager.executeTypedQueryForObjects(queryMethod.getAnnotatedQuery(), returnedType); + } else { + return nodeManager.executeTypedQueryForObject(queryMethod.getAnnotatedQuery(), returnedType); + } } @Override diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index b0738e84d..6727977bb 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.integration; import java.util.List; import java.util.Map; +import java.util.Optional; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; @@ -29,11 +30,38 @@ import org.springframework.transaction.annotation.Transactional; * @author Gerrit Meier * @author Michael J. Simon */ -public interface PersonRepository extends Neo4jRepository { +public interface PersonRepository extends Neo4jRepository { @Transactional @Query("RETURN 1") - List> customQuery(); + Long customQuery(); - List> findByName(); + @Query("MATCH (n:PersonWithAllConstructor) return n") + List getAllPersonsViaQuery(); + + @Query("MATCH (n:PersonWithAllConstructor{name:'Test'}) return n") + PersonWithAllConstructor getOnePersonViaQuery(); + + @Query("MATCH (n:PersonWithAllConstructor{name:'Test'}) return n") + Optional getOptionalPersonsViaQuery(); + + @Query("MATCH (n:PersonWithNoConstructor) return n") + List getAllPersonsWithNoConstructorViaQuery(); + + @Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n") + PersonWithNoConstructor getOnePersonWithNoConstructorViaQuery(); + + @Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n") + Optional getOptionalPersonsWithNoConstructorViaQuery(); + + @Query("MATCH (n:PersonWithWither) return n") + List getAllPersonsWithWitherViaQuery(); + + @Query("MATCH (n:PersonWithWither{name:'Test'}) return n") + PersonWithWither getOnePersonWithWitherViaQuery(); + + @Query("MATCH (n:PersonWithWither{name:'Test'}) return n") + Optional getOptionalPersonsWithWitherViaQuery(); + + List> findByName(String name); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java new file mode 100644 index 000000000..b53214c37 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +@Getter +@Setter +@Node +@ToString +@AllArgsConstructor +public class PersonWithAllConstructor { + + @Id + private final Long id; + + private final String name; +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/Person.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java similarity index 84% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/Person.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java index 8c065af36..c3bc4b38c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/Person.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java @@ -18,14 +18,20 @@ */ package org.springframework.data.neo4j.integration; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; +@Getter +@Setter @Node -public class Person { +@ToString +public class PersonWithNoConstructor { - @Id - private Long id; + @Id private Long id; private String name; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithWither.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithWither.java new file mode 100644 index 000000000..a9894d7a6 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithWither.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +@Getter +@Setter +@Node +@ToString +public class PersonWithWither { + + @Id private final Long id; + + private final String name; + + private PersonWithWither(Long id, String name) { + this.id = id; + this.name = name; + } + + public PersonWithWither withId(Long newId) { + return new PersonWithWither(newId, this.name); + } + + public PersonWithWither withName(String newName) { + return new PersonWithWither(this.id, newName); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index a6bef207f..ed43bd011 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -21,13 +21,15 @@ package org.springframework.data.neo4j.integration; import static org.assertj.core.api.Assertions.*; import java.util.List; -import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Transaction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -51,9 +53,86 @@ class RepositoryIT { private static Neo4jContainer neo4jContainer = new Neo4jContainer().withoutAuthentication(); private final PersonRepository repository; + private final Driver driver; + + @Autowired + RepositoryIT(PersonRepository repository, Driver driver) { - RepositoryIT(@Autowired PersonRepository repository) { this.repository = repository; + this.driver = driver; + } + + @BeforeEach + void setupDb() { + + Transaction transaction = driver.session().beginTransaction(); + transaction.run("MATCH (n) detach delete n"); + transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = 'Test'"); + transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = 'Test'"); + transaction.run("CREATE (n:PersonWithWither) SET n.name = 'Test'"); + transaction.success(); + transaction.close(); + + } + + @Test + void loadAllPersonsWithAllConstructor() { + List persons = repository.getAllPersonsViaQuery(); + + assertThat(persons).anyMatch(person -> person.getName().equals("Test")); + } + + @Test + void loadOnePersonWithAllConstructor() { + PersonWithAllConstructor person = repository.getOnePersonViaQuery(); + assertThat(person.getName()).isEqualTo("Test"); + } + + @Test + void loadOptionalPersonWithAllConstructor() { + Optional person = repository.getOptionalPersonsViaQuery(); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo("Test"); + } + + @Test + void loadAllPersonsWithNoConstructor() { + List persons = repository.getAllPersonsWithNoConstructorViaQuery(); + + assertThat(persons).anyMatch(person -> person.getName().equals("Test")); + } + + @Test + void loadOnePersonWithNoConstructor() { + PersonWithNoConstructor person = repository.getOnePersonWithNoConstructorViaQuery(); + assertThat(person.getName()).isEqualTo("Test"); + } + + @Test + void loadOptionalPersonWithNoConstructor() { + Optional person = repository.getOptionalPersonsWithNoConstructorViaQuery(); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo("Test"); + } + + @Test + void loadAllPersonsWithWither() { + List persons = repository.getAllPersonsWithWitherViaQuery(); + + assertThat(persons).anyMatch(person -> person.getName().equals("Test")); + } + + @Test + void loadOnePersonWithWither() { + PersonWithWither person = repository.getOnePersonWithWitherViaQuery(); + assertThat(person.getName()).isEqualTo("Test"); + } + + @Test + void loadOptionalPersonWithWither() { + Optional person = repository.getOptionalPersonsWithWitherViaQuery(); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo("Test"); } @Test @@ -63,17 +142,13 @@ class RepositoryIT { @Test void callCustomCypher() { - List> actual = repository.customQuery(); - assertThat(actual) - .isNotNull() - .isNotEmpty() - .first().extracting(record -> record.get("1")) - .isEqualTo(1L); + Long fixedLong = repository.customQuery(); + assertThat(fixedLong).isEqualTo(1L); } @Test void findBySimpleProperty() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findByName()); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findByName("Test")); } @Configuration @@ -91,7 +166,8 @@ class RepositoryIT { @Bean public NodeManagerFactory nodeManagerFactory(Driver driver) { - return new NodeManagerFactory(driver, Person.class); + return new NodeManagerFactory(driver, PersonWithAllConstructor.class, PersonWithNoConstructor.class, + PersonWithWither.class); } @Bean From 09342e4fc564dbb50ac2de6990ba5b1689c155f8 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 3 May 2019 16:01:10 +0200 Subject: [PATCH 068/342] Add prelimary support for derived finder methods. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit f02640db4192a125fe6dcd8e63601e46b108a025 Author: Michael Simons Date: Fri May 3 15:59:35 2019 +0200 Remove local db connection. commit 4381f6764d8dde962ef9b1f1858321ba7514c787 Author: Michael Simons Date: Fri May 3 15:44:47 2019 +0200 Remove Strings and move stuff to the correct places. commit d6204f6570344b95736d964dc547e798ad936e4a Author: Michael Simons Date: Fri May 3 15:34:44 2019 +0200 Optimize renderer commit caffef0a4b66bef290ed10335a15518158ac4d84 Author: Michael Simons Date: Fri May 3 15:09:25 2019 +0200 Streamline sort. commit d226b8240647ed8024a30544f99574205b898798 Author: Gerrit Meier Date: Fri May 3 14:04:25 2019 +0200 Make found Node classes eligible for entity usage. commit 50cac181053c3bf4784486a74cc8495a11e943f8 Author: Gerrit Meier Date: Fri May 3 12:56:41 2019 +0200 Cleanup missing implementation tests. commit 54eda53c3121341ea812badc87e19d7b61b3d332 Author: Gerrit Meier Date: Fri May 3 12:47:55 2019 +0200 Support count, exists, all with pagination with example. commit d8ed59ae32c530b07824931f0ffe5325f83a0090 Author: Michael Simons Date: Fri May 3 12:23:44 2019 +0200 Some polishing in the repo. commit cb8a55bd39174275d1a03949777205717ae1cc81 Author: Michael Simons Date: Fri May 3 12:07:00 2019 +0200 First round of polishing Cypher dsl. commit af8bf36bbd7e8297279528c909cb42dc9f972981 Author: Gerrit Meier Date: Thu May 2 17:41:07 2019 +0200 Cleanup after conflict. commit 0f032ea898214f1307c658c7ca9e4b724863c3fa Author: Gerrit Meier Date: Thu May 2 17:37:16 2019 +0200 Extract Sort creation and support Example and Sort for findAll. commit 27b8866636bf15b516bdc77adcddf3787b0b513b Author: Michael Simons Date: Thu May 2 17:33:17 2019 +0200 Visit direction for some closure. commit 86c245917cfcbab6fc68d82c49e34ae6c413e758 Author: Michael Simons Date: Thu May 2 17:26:42 2019 +0200 Second reiteration of conditions. commit 5a2f5807ef265b5f86b4629d5f25937a30b4782e Author: Gerrit Meier Date: Thu May 2 15:50:38 2019 +0200 Find by example for all and one. commit e03960ba31dfd652c53bf45ddf1a0fe134fd9f9b Author: Gerrit Meier Date: Thu May 2 13:57:08 2019 +0200 Extract example handling. commit d0500cfd891c021d48b6dae979fc4bd7aa085c82 Author: Michael Simons Date: Thu May 2 13:00:03 2019 +0200 Polishing. commit 433a3034a187aa89d399ceaf56c3ecbfbeaf2d0f Author: Michael Simons Date: Thu May 2 12:57:53 2019 +0200 Use a symbol on the tree element for direction. commit af345a92a4dd4b5422ac58f6544a4f478e1e8b8f Author: Michael Simons Date: Thu May 2 12:55:53 2019 +0200 First reiteration of conditions. commit 8aea898af5cc9f944875954e5d90b31cfc900b4c Author: Gerrit Meier Date: Thu May 2 09:53:45 2019 +0200 Support for multiple fields on example object. commit 8f6a9f8269a3a10a18cde1750380779995072096 Author: Gerrit Meier Date: Tue Apr 30 11:42:34 2019 +0200 Simple, straight forward implementation of find by example. For one field right now. commit 946f3420bf0d63659260f487682d0a04a81686a7 Author: Gerrit Meier Date: Mon Apr 29 13:06:32 2019 +0200 Support for pagination finder method. commit 991728eccecfabb5b603d7a603947bd312b92dae Author: Gerrit Meier Date: Mon Apr 29 11:05:59 2019 +0200 Support for sort finder method. commit c85c67af39b947da14501037b5016e6851bb54f0 Author: Michael Simons Date: Fri Apr 26 16:08:03 2019 +0200 There is a sort direction, it’s just not defined. commit 4db4db861a6aef16c587cc8aef96b514732ce570 Author: Michael Simons Date: Fri Apr 26 16:07:31 2019 +0200 Document functions, escape string literals. commit cdd050e8faf7bac4db559cd897fa282152fd62b7 Author: Gerrit Meier Date: Fri Apr 26 14:57:14 2019 +0200 Sort for return clauses. commit 107066dc37b79fa1af6df3efec221889bf4ace32 Author: Michael Simons Date: Fri Apr 26 12:57:53 2019 +0200 Introduce list of expressions. commit 67768fc0d006c0f07438d96af78d39c303da795f Author: Michael Simons Date: Fri Apr 26 12:34:52 2019 +0200 Fix handling of named things. commit 221c88ae8317f93ecd88f65c154ca2735d41ff74 Author: Gerrit Meier Date: Fri Apr 26 12:35:11 2019 +0200 Sort and Order class structures. commit 67538b176d695dfda385ffe5aeadc27e9e6e3afc Author: Michael Simons Date: Fri Apr 26 11:21:52 2019 +0200 wip order/sort commit 20c9ad9980bae7bf8ec9423d40ec243f30efc8c8 Author: Michael Simons Date: Fri Apr 26 11:09:01 2019 +0200 Introduce FunctionInvocation and Functions. commit d5090740a90c5455ef1d861cdaf5dff8c0ce2195 Author: Gerrit Meier Date: Thu Apr 25 16:45:38 2019 +0200 Enable count and implement Count for Cypher DSL. commit 0ff705a1fcfb21ad00e82dc8dc1300929cd8b6ad Author: Gerrit Meier Date: Thu Apr 25 16:21:49 2019 +0200 Enable findAllByIds with Cypher DSL and introduce ListLiteral for DSL. commit 916166cf00458cb76253e4fed6392d1846628ee8 Author: Gerrit Meier Date: Thu Apr 25 15:24:36 2019 +0200 Enable existsById and findById with Cypher DSL. commit 834531dec020735c3ea50961f48de9afbc3db789 Author: Gerrit Meier Date: Thu Apr 25 14:47:56 2019 +0200 Support for id queries. commit 9e588f8206538dc69d655f8397f9d279ed9f1f70 Author: Gerrit Meier Date: Thu Apr 25 12:46:43 2019 +0200 Enable findAll with Cypher DSL. commit de0d101260754ad5ffa5dc3af3a05b50d06222ff Author: Gerrit Meier Date: Thu Apr 25 11:47:07 2019 +0200 First take. --- .../data/neo4j/core/DefaultNodeManager.java | 9 +- .../data/neo4j/core/NodeManager.java | 3 + .../data/neo4j/core/NodeManagerFactory.java | 11 +- .../neo4j/core/cypher/AliasedExpression.java | 7 +- .../neo4j/core/cypher/CompoundCondition.java | 162 ++++++ .../data/neo4j/core/cypher/Condition.java | 17 + .../data/neo4j/core/cypher/Conditions.java | 43 +- .../data/neo4j/core/cypher/Cypher.java | 53 +- .../core/cypher/DefaultStatementBuilder.java | 107 +++- .../data/neo4j/core/cypher/Expression.java | 15 + .../neo4j/core/cypher/ExpressionList.java | 49 ++ .../data/neo4j/core/cypher/Expressions.java | 44 ++ .../neo4j/core/cypher/FunctionInvocation.java | 52 ++ .../data/neo4j/core/cypher/Functions.java | 93 ++++ .../data/neo4j/core/cypher/IsNull.java | 73 +++ .../cypher/{ReturnItem.java => Limit.java} | 22 +- .../data/neo4j/core/cypher/ListLiteral.java | 42 ++ .../data/neo4j/core/cypher/Literal.java | 16 +- .../data/neo4j/core/cypher/Node.java | 5 + .../data/neo4j/core/cypher/NotCondition.java | 47 ++ .../data/neo4j/core/cypher/NumberLiteral.java | 37 ++ .../RenderUtils.java => ObjectLiteral.java} | 27 +- .../data/neo4j/core/cypher/Order.java | 37 ++ .../data/neo4j/core/cypher/Property.java | 40 ++ .../data/neo4j/core/cypher/Return.java | 40 +- .../data/neo4j/core/cypher/Skip.java | 44 ++ .../data/neo4j/core/cypher/SortItem.java | 82 +++ .../neo4j/core/cypher/StatementBuilder.java | 161 +++++- .../data/neo4j/core/cypher/StringLiteral.java | 45 +- .../cypher/renderer/RenderingVisitor.java | 103 +++- .../cypher/support/ReflectiveVisitor.java | 39 +- .../core/cypher/support/TypedSubtree.java | 17 +- .../support/Neo4jRepositoryFactory.java | 2 +- .../support/SimpleNeo4jRepository.java | 175 ++++++- .../data/neo4j/core/cypher/CypherTest.java | 495 ++++++++++++++++++ .../cypher/DefaultStatementBuilderTest.java | 10 - .../data/neo4j/core/cypher/FunctionsTest.java | 72 +++ .../neo4j/core/cypher/StringLiteralTest.java | 63 +++ ...ilsTest.java => RenderingVisitorTest.java} | 11 +- .../integration/PersonWithAllConstructor.java | 8 + .../data/neo4j/integration/RepositoryIT.java | 170 +++++- .../support/SimpleNeo4jRepositoryTest.java | 102 +--- 42 files changed, 2393 insertions(+), 257 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/{ReturnItem.java => Limit.java} (69%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/{renderer/RenderUtils.java => ObjectLiteral.java} (55%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/StringLiteralTest.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/{RenderUtilsTest.java => RenderingVisitorTest.java} (82%) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index 2909f4817..635ddda7a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -72,7 +72,7 @@ class DefaultNodeManager implements NodeManager { @Override public Object executeQuery(String query) { - return neo4jClient.newQuery(query).fetch().one(); + return neo4jClient.newQuery(query).fetch().all(); } @Override @@ -129,6 +129,12 @@ class DefaultNodeManager implements NodeManager { throw new UnsupportedOperationException("Not there yet."); } + // todo I don't like this exposure here + @Override + public Schema getSchema() { + return schema; + } + private class ReflectionBasedMappingFunction implements Function { private final NodeDescription nodeDescription; @@ -194,4 +200,5 @@ class DefaultNodeManager implements NodeManager { } } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java index d1e41bfa3..1c7629686 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -23,6 +23,7 @@ import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.driver.Transaction; +import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.lang.Nullable; /** @@ -71,4 +72,6 @@ public interface NodeManager { */ void delete(Object managedEntity); + Schema getSchema(); + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java index bea2767f7..5fb402d6f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.Set; @@ -68,11 +69,15 @@ public final class NodeManagerFactory { * @param initialPersistentClasses The set of classes that should be initially scanned */ public NodeManagerFactory(Driver driver, Class... initialPersistentClasses) { + this(driver, Arrays.asList(initialPersistentClasses)); + } + /** + * @see NodeManagerFactory#NodeManagerFactory(Driver, Class...) + */ + public NodeManagerFactory(Driver driver, Collection> initialPersistentClasses) { this.driver = driver; - this.initialPersistentClasses = new HashSet<>(); - - Arrays.stream(initialPersistentClasses).forEach(this.initialPersistentClasses::add); + this.initialPersistentClasses = new HashSet<>(initialPersistentClasses); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java index 4b8b69c7b..0965f7500 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java @@ -18,7 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; -import java.util.Optional; +import static org.springframework.data.neo4j.core.cypher.Expressions.*; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.util.Assert; @@ -62,10 +62,7 @@ public class AliasedExpression implements Aliased, Expression { public void accept(Visitor visitor) { visitor.enter(this); - // Make sure we deal nicely with symbolic names. - Optional.of(delegate).filter(Named.class::isInstance).map(Named.class::cast).flatMap(Named::getSymbolicName) - .map(Expression.class::cast) - .orElse(delegate).accept(visitor); + nameOrExpression(this.delegate).accept(visitor); visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java new file mode 100644 index 000000000..5f50cddab --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.springframework.data.neo4j.core.cypher.CompoundCondition.LogicalOperator.*; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A condition that consists of one or two {@link Condition conditions} connected by a + * Logical connective (operator). + * + * @author Michael J. Simons + */ +public class CompoundCondition implements Condition { + + /** + * The empty, compound condition. + */ + static final CompoundCondition EMPTY_CONDITION = new CompoundCondition(null); + + static CompoundCondition create(Condition left, LogicalOperator operator, Condition right) { + + Assert.notNull(left, "Left hand side condition is required."); + Assert.notNull(operator, "Operator is required."); + Assert.notNull(right, "Right hand side condition is required."); + + return new CompoundCondition(operator) + .add(operator, left) + .add(operator, right); + } + + static CompoundCondition empty() { + + return EMPTY_CONDITION; + } + + private @Nullable final LogicalOperator operator; + + private final List conditions; + + private CompoundCondition(@Nullable LogicalOperator operator) { + this.operator = operator; + this.conditions = new ArrayList<>(); + } + + @Override + public Condition and(Condition condition) { + return this.add(AND, condition); + } + + @Override + public Condition or(Condition condition) { + return this.add(OR, condition); + } + + @Override + public Condition xor(Condition condition) { + return this.add(XOR, condition); + } + + private CompoundCondition add( + LogicalOperator chainingOperator, + Condition condition + ) { + if (this == EMPTY_CONDITION) { + return new CompoundCondition(chainingOperator).add(chainingOperator, condition); + } + + if (condition == EMPTY_CONDITION) { + return this; + } + + if (condition instanceof CompoundCondition) { + CompoundCondition compoundCondition = (CompoundCondition) condition; + if (compoundCondition.operator == chainingOperator) { + this.conditions.addAll(compoundCondition.conditions); + } else { + this.conditions.add(condition); + } + + return this; + } + + if (this.operator == chainingOperator) { + conditions.add(condition); + return this; + } + + return CompoundCondition.create(this, chainingOperator, condition); + } + + @Override + public void accept(Visitor visitor) { + + // Fold single or empty condition + boolean hasManyConditions = this.conditions.size() > 1; + + if (hasManyConditions) { + visitor.enter(this); + } + + LogicalOperator currentOperator = null; + for (Condition condition : conditions) { + Visitable.visitIfNotNull(currentOperator, visitor); + condition.accept(visitor); + currentOperator = operator; + } + + if (hasManyConditions) { + visitor.leave(this); + } + } + + /** + * Shared interface for all logical operators. + */ + public enum LogicalOperator implements org.springframework.data.neo4j.core.cypher.support.Visitable { + // Note the full import above: JDK 8 fails to compile this class otherwise. + /* + sdn-rx/spring-data-neo4j-rx/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java:[140,48] cannot find symbol + symbol: class Visitable + location: class org.springframework.data.neo4j.core.cypher.CompoundCondition + */ + + AND("AND"), + OR("OR"), + XOR("XOR"); + + private final String symbol; + + LogicalOperator(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java index 6d98c1fa1..966dc3f29 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.springframework.data.neo4j.core.cypher.CompoundCondition.LogicalOperator; import org.springframework.data.neo4j.core.cypher.support.Visitable; /** @@ -27,4 +28,20 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; * @since 1.0 */ public interface Condition extends Visitable { + + default Condition and(Condition condition) { + return CompoundCondition.create(this, LogicalOperator.AND, condition); + } + + default Condition or(Condition condition) { + return CompoundCondition.create(this, LogicalOperator.OR, condition); + } + + default Condition xor(Condition condition) { + return CompoundCondition.create(this, LogicalOperator.XOR, condition); + } + + default Condition not() { + return new NotCondition(this); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java index 55eea4073..56f7cd782 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java @@ -18,26 +18,63 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.springframework.util.Assert; + /** - * Builder for various conditions. Used internally from properties and other expressions that should take part in conditions. + * Builder for various conditions. Used internally from properties and other expressions that should take part in + * conditions. * * @author Michael J. Simons + * @author Gerrit Meier * @since 1.0 */ -public final class Conditions { +public class Conditions { /** * Creates a condition that matches if the right hand side is a regular expression that matches the the left hand side via * {@code =~}. * * @param lhs The left hand side of the comparision - * @param rhs The right hand side of the comparions + * @param rhs The right hand side of the comparision * @return A "matches" comparision */ static Condition matches(Expression lhs, Expression rhs) { return Comparison.create(lhs, "=~", rhs); } + /** + * Negates the given condition. + * + * @param condition The condition to negate. Must not be null. + * @return The negated condition. + */ + static Condition not(Condition condition) { + + Assert.notNull(condition, "Condition to negate must not be null."); + return condition.not(); + } + + /** + * Creates a placeholder condition which is not rendered in the final statement but is usefull + * while chaining conditions together. + * + * @return A placeholder condition. + */ + public static Condition noCondition() { + + return CompoundCondition.empty(); + } + + static Condition isNull(Expression expression) { + + return IsNull.create(expression, false); + } + + static Condition isNotNull(Expression expression) { + + return IsNull.create(expression, true); + } + /** * Not to be instantiated. */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index d7001f06f..d63fca752 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -19,7 +19,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; import org.springframework.lang.Nullable; /** @@ -48,7 +48,30 @@ public final class Cypher { } /** - * Creates a new {@link StringLiteral} from the {@code content}. + * Starts building a statement based on a match clause. Use {@link Cypher#node(String, String...)} and related to + * retrieve a node or a relationship, which both are pattern elements. + * + * @param pattern The patterns to match + * @return An ongoing match that is used to specify an optional where and a required return clause + */ + public static OngoingMatchWithoutWhere match(PatternElement... pattern) { + + return Statement.builder().match(pattern); + } + + /** + * Creates a new {@link SortItem} to be used as part of an {@link Order}. + * + * @param expression The expression by which things should be sorted + * @return A sort item, providing means to specify ascending or descending order + */ + public static SortItem sort(Expression expression) { + + return SortItem.create(expression, null); + } + + /** + * Creates a new {@link StringLiteral} from a {@code content}. * * @param content the literal content. * @return a new {@link StringLiteral}. @@ -58,15 +81,29 @@ public final class Cypher { } /** - * Starts building a statement based on a match clause. Use {@link Cypher#node(String, String...)} and related to - * retrieve a node or a relationship, which both are pattern elements. + * Creates a new {@link NumberLiteral} from the given {@code number}. * - * @param pattern The patterns to match - * @return An ongoing match that is used to specify an optional where and a required return clause + * @param number the number. + * @return a new {@link NumberLiteral}. */ - public static OngoingMatch match(PatternElement... pattern) { + public static NumberLiteral literalOf(@Nullable Number number) { + return new NumberLiteral(number); + } - return Statement.builder().match(pattern); + /** + * Creates a new {@link ObjectLiteral} from the given {@code object}. + * + * @param object the object to represent. + * @return a new {@link ObjectLiteral}. + */ + public static Literal literalOf(@Nullable Object object) { + if (object instanceof CharSequence) { + return new StringLiteral((CharSequence) object); + } + if (object instanceof Number) { + return new NumberLiteral((Number) object); + } + return new ObjectLiteral(object); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index 9f19c56a8..46047af36 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -18,14 +18,15 @@ */ package org.springframework.data.neo4j.core.cypher; -import static java.util.stream.Collectors.*; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndReturn; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithWhere; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingOrderDefinition; import org.springframework.util.Assert; /** @@ -34,14 +35,23 @@ import org.springframework.util.Assert; * @since 1.0 */ class DefaultStatementBuilder - implements StatementBuilder, OngoingMatch, OngoingMatchAndReturn { + implements StatementBuilder, + OngoingMatch, + OngoingMatchWithWhere, + OngoingMatchWithoutWhere, + OngoingMatchAndReturn, + OngoingOrderDefinition, StatementBuilder.OngoingMatchAndReturnWithOrder { private List matchList = new ArrayList<>(); private List returnList = new ArrayList<>(); - private Condition where; + private List sortItemList = new ArrayList<>(); + private SortItem lastSortItem; + private Skip skip; + private Limit limit; + private Condition condition; @Override - public OngoingMatch match(PatternElement... pattern) { + public OngoingMatchWithoutWhere match(PatternElement... pattern) { Assert.notNull(pattern, "Patterns to match are required."); Assert.notEmpty(pattern, "At least one pattern to match is required."); @@ -56,30 +66,72 @@ class DefaultStatementBuilder Assert.notNull(expressions, "Expressions to return are required."); Assert.notEmpty(expressions, "At least one expressions to return is required."); - this.returnList.addAll(Arrays.stream(expressions) - .map(expression -> expression instanceof Named ? - ((Named) expression).getSymbolicName().map(Expression.class::cast).orElse(expression) : - expression) - .collect(toList())); + this.returnList.addAll(Arrays.asList(expressions)); return this; } @Override - public OngoingMatchAndReturn returning(Node... nodes) { - - Assert.notNull(nodes, "Nodes to return are required."); - Assert.notEmpty(nodes, "At least one node to return is required."); - - this.returnList.addAll(Arrays.stream(nodes) - .map(node -> node.getSymbolicName().map(Expression.class::cast).orElse(node)) - .collect(toList())); + public OngoingMatchAndReturn orderBy(SortItem... sortItem) { + Arrays.stream(sortItem).forEach(this.sortItemList::add); return this; } @Override - public OngoingMatch where(Condition condition) { + public OngoingOrderDefinition orderBy(Expression expression) { + this.lastSortItem = Cypher.sort(expression); + return this; - this.where = condition; + } + + @Override + public OngoingMatchAndReturn descending() { + this.sortItemList.add(this.lastSortItem.descending()); + this.lastSortItem = null; + return this; + } + + @Override + public OngoingMatchAndReturn ascending() { + this.sortItemList.add(this.lastSortItem.ascending()); + this.lastSortItem = null; + return this; + } + + @Override + public OngoingOrderDefinition and(Expression expression) { + return orderBy(expression); + } + + @Override + public OngoingMatchAndReturn skip(Number number) { + skip = Skip.of(number); + return this; + } + + @Override + public OngoingMatchAndReturn limit(Number number) { + limit = Limit.of(number); + return this; + } + + @Override + public OngoingMatchWithWhere where(Condition newCondition) { + + this.condition = newCondition; + return this; + } + + @Override + public OngoingMatchWithWhere and(Condition additionalCondition) { + + this.condition = this.condition.and(additionalCondition); + return this; + } + + @Override + public OngoingMatchWithWhere or(Condition additionalCondition) { + + this.condition = this.condition.or(additionalCondition); return this; } @@ -87,7 +139,18 @@ class DefaultStatementBuilder public Statement build() { Pattern pattern = new Pattern(this.matchList); - Match match = new Match(pattern, this.where == null ? null : new Where(this.where)); - return new SinglePartQuery(match, new Return(returnList.stream().map(ReturnItem::new).collect(toList()))); + Match match = new Match(pattern, hasCondition() ? new Where(this.condition) : null); + ExpressionList returnItems = new ExpressionList(this.returnList); + + if (lastSortItem != null) { + sortItemList.add(lastSortItem); + } + Order order = sortItemList.size() > 0 ? new Order(sortItemList) : null; + + return new SinglePartQuery(match, new Return(returnItems, order, skip, limit)); + } + + private boolean hasCondition() { + return !(this.condition == null || this.condition == CompoundCondition.EMPTY_CONDITION); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java index 48aaa2a34..6c3d096f3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java @@ -40,4 +40,19 @@ public interface Expression extends Visitable { Assert.hasText(alias, "The alias may not be null or empty."); return new AliasedExpression(this, alias); } + + default Condition isEqualTo(Expression rhs) { + return Comparison.create(this, "=", rhs); + } + + // WIP + // TODO Make longs a list of expressions or something + // WIP + default Condition isIn(Iterable ids) { + return Comparison.create( + this, + "in", + new ListLiteral(ids)); + } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java new file mode 100644 index 000000000..372d650d9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.springframework.data.neo4j.core.cypher.Expressions.*; + +import java.util.List; + +import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * Represents a list of expressions. When visited, the expressions are treated as named expression if they have declared + * a symbolic name as variable or as unnamed expression when nameless. + * + * @author Michael J. Simons + * @since 1.0 + */ +class ExpressionList extends TypedSubtree { + + ExpressionList(List returnItems) { + super(returnItems); + } + + ExpressionList(Expression... children) { + super(children); + } + + @Override + protected Visitable prepareVisit(Expression child) { + return nameOrExpression(child); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java new file mode 100644 index 000000000..a1031d1eb --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +/** + * Utility methods for dealing with expressions. + * + * @author Michael J. Simons + * @since 1.0 + */ +public final class Expressions { + + /** + * @param expression Possibly named with a non-empty symbolic name. + * @return The name of the expression if the expression is named or the expression itself. + */ + static Expression nameOrExpression(Expression expression) { + + if (expression instanceof Named) { + return ((Named) expression).getSymbolicName().map(Expression.class::cast).orElse(expression); + } else { + return expression; + } + } + + private Expressions() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java new file mode 100644 index 000000000..9b73244da --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * See + * FunctionInvocation + * + * @author Gerrit Meier + * @author Michael J. Simons + */ +public class FunctionInvocation implements Expression { + + private final String functionName; + + private final ExpressionList arguments; + + FunctionInvocation(String functionName, Expression... arguments) { + + this.functionName = functionName; + this.arguments = new ExpressionList(arguments); + } + + public String getFunctionName() { + return functionName; + } + + @Override + public void accept(Visitor visitor) { + visitor.enter(this); + this.arguments.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java new file mode 100644 index 000000000..4524f04f7 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.util.Assert; + +/** + * Factory methods for creating instances of {@link FunctionInvocation functions}. + * + * @author Michael J. Simons + * @since 1.0 + */ +public final class Functions { + + private static final String F_ID = "id"; + + /** + * Creates a function invocation for {@code id{}}. + * See id. + * + * @param node The node for which the internal id should be retrieved + * @return A function call for {@code id()} on a node. + */ + public static FunctionInvocation id(Node node) { + + Assert.notNull(node, "The node parameter is required."); + + return new FunctionInvocation(F_ID, node); + } + + /** + * Creates a function invocation for {@code id{}}. + * See id. + * + * @param relationship The relationship for which the internal id should be retrieved + * @return A function call for {@code id()} on a relationship. + */ + public static FunctionInvocation id(Relationship relationship) { + + Assert.notNull(relationship, "The relationship parameter is required."); + + return new FunctionInvocation(F_ID, relationship); + } + + /** + * Creates a function invocation for the {@code count()} function. + * See count. + * + * @param expression An expression describing the things to count. Can be a node, relationship, alias or + * a symbolic name + * @return A function call for {@code count()} for one expression + */ + public static FunctionInvocation count(Expression expression) { + + Assert.notNull(expression, "The expression to count is required."); + + return new FunctionInvocation("count", expression); + } + + /** + * Creates a function invocation for the {@code } function. + * See coalesce. + * + * @param expressions One or more expressions to be coalesced + * @return A function call for {@code coalesce}. + */ + public static FunctionInvocation coalesce(Expression... expressions) { + + Assert.notEmpty(expressions, "At least one expression is required."); + Assert.notNull(expressions[0], "At least one expression is required."); + + return new FunctionInvocation("coalesce", expressions); + } + + private Functions() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java new file mode 100644 index 000000000..727875e08 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; + +/** + * A condition to check whether an expression is null. By negating it effectively becomes {@literal IsNotNull}. + * This condition can either be negated itself or through a {@link NotCondition}, which is the same style + * that Cypher supports as well. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class IsNull implements Condition { + + /** + * Creates a new {@link IsNull IsNull-condition}. + * + * @param expression The expression to check for null, must not be {@literal null} + * @param negated A flag whether to turn this into a {@literal IsNotNull} + * @return A new condition + */ + static IsNull create(Expression expression, boolean negated) { + + Assert.notNull(expression, "Expression must not be null."); + + return new IsNull(expression, negated); + } + + /** The expression to check for null, must not be be {@literal null}. */ + private final Expression expression; + + /** + * Flag, if this is actually a IsNotNull. + */ + private final boolean negated; + + private IsNull(Expression expression, boolean negated) { + + this.expression = expression; + this.negated = negated; + } + + public boolean isNegated() { + return negated; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + expression.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnItem.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java similarity index 69% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnItem.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java index 2a9a2d346..b24f03c78 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnItem.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java @@ -22,25 +22,23 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** - * This is a wrapper around an expression. - *

- * See ReturnItem. - * - * @author Michael J. Simons - * @since 1.0 + * @author Gerrit Meier */ -class ReturnItem implements Visitable { +public class Limit implements Visitable { - private final Expression expression; + public static Limit of(Number value) { + return new Limit(new NumberLiteral(value)); + } - ReturnItem(Expression expression) { - this.expression = expression; + private final NumberLiteral limitAmount; + + private Limit(NumberLiteral limitAmount) { + this.limitAmount = limitAmount; } @Override public void accept(Visitor visitor) { visitor.enter(this); - expression.accept(visitor); - visitor.leave(this); + limitAmount.accept(visitor); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java new file mode 100644 index 000000000..a37b567ae --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +public class ListLiteral extends Literal { + + ListLiteral(Iterable content) { + super(content); + } + + @Override + public String asString() { + + return "[" + + StreamSupport.stream(getContent().spliterator(), false).map(Object::toString).collect(Collectors.joining(",")) + + "]"; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java index 3fbc2e989..1f64b051d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java @@ -28,17 +28,29 @@ import org.springframework.lang.Nullable; */ public abstract class Literal implements Expression { + /** + * The content of this literal. + */ private @Nullable T content; Literal(@Nullable T content) { this.content = content; } + /** + * @return The content of this literal, may be {@literal null} + */ public @Nullable T getContent() { return content; } - @Override - public abstract String toString(); + /** + * The string representation should be designed in such a way the a renderer can use it correctly in + * the given context of the literal, i.e. a literal containing a string should quote that string + * and escape all reservered characters. + * + * @return A string representation to be used literally in a cypher statement. + */ + public abstract String asString(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java index 48e97fec2..91d3f5d5a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java @@ -65,6 +65,7 @@ public class Node implements PatternElement, Named, Expression { Node(SymbolicName symbolicName, List labels) { this.symbolicName = symbolicName; + this.labels = new ArrayList<>(labels); } @@ -106,6 +107,10 @@ public class Node implements PatternElement, Named, Expression { return Property.create(this, name); } + public FunctionInvocation internalId() { + return Functions.id(this); + } + public OngoingRelationshipDefinition outgoingRelationShipTo(Node other) { return new DefaultOngoingRelationshipDefinition(this, Direction.LTR, other); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java new file mode 100644 index 000000000..e670693a3 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * A negated version of the condition passed during construnction of this condition. + * + * @author Michael J. Simons + * @since 1.0 + */ +public class NotCondition implements Condition { + + /** + * The condition to which the visit is delegated to after entering this condition. + */ + private final Condition condition; + + NotCondition(Condition condition) { + this.condition = condition; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + condition.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java new file mode 100644 index 000000000..02c82beff --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +public class NumberLiteral extends Literal { + + NumberLiteral(Number content) { + super(content); + } + + @Override + public String asString() { + + return String.valueOf(getContent()); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java similarity index 55% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtils.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java index 61c7c065d..e3052dec0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java @@ -16,30 +16,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.renderer; - -import java.util.Locale; +package org.springframework.data.neo4j.core.cypher; /** - * Helper methods dealing with the formatting of various Cypher nodes and such. + * The most generic implementation of {@link Literal}. Takes an arbitrary object and calls {@code toString} on it to get + * its value representation. * - * @author Michael J. Simons * @author Gerrit Meier + * @author Michael J. Simons * @since 1.0 */ -final class RenderUtils { +public class ObjectLiteral extends Literal { - /** - * Escapes a symbolic name. Such a symbolic name is either used for a nodes label, the type of a relationship or a - * variable. - * - * @param unescapedName - * @return The correctly escaped name, safe to be used in statements. - */ - static CharSequence escapeName(String unescapedName) { - return String.format(Locale.ENGLISH, "`%s`", unescapedName.replaceAll("`", "``")); + ObjectLiteral(Object content) { + + super(content); } - private RenderUtils() { + @Override + public String asString() { + return String.valueOf(getContent()); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java new file mode 100644 index 000000000..1a342840b --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.List; + +import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; + +/** + * Represents the list of sort items that make up the order of records in a result set. + * + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +public class Order extends TypedSubtree { + + Order(List sortItems) { + super(sortItems); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java index fe5eb807f..bbcf49e18 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java @@ -65,4 +65,44 @@ public class Property implements Expression { return Conditions.matches(this, Cypher.literalOf(s)); } + + /** + * The property does not track the condition created here. + * + * @return A condition based on this property that evaluates to true when this property is null + */ + public Condition isNull() { + + return Conditions.isNull(this); + } + + /** + * The property does not track the condition created here. + * + * @return A condition based on this property that evaluates to true when this property is not null + */ + public Condition isNotNull() { + + return Conditions.isNotNull(this); + } + + /** + * The property does not track the sort items created here. + * + * @return A sort item for this property in descending order + */ + public SortItem descending() { + + return SortItem.create(this, SortItem.Direction.DESC); + } + + /** + * The property does not track the sort items created here. + * + * @return A sort item for this property in ascending order + */ + public SortItem ascending() { + + return SortItem.create(this, SortItem.Direction.ASC); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java index 8e77a418e..bcb6c1b82 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java @@ -18,17 +18,45 @@ */ package org.springframework.data.neo4j.core.cypher; -import java.util.List; - -import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; /** + * The container or "body" for return items, order and optional skip and things. + * + * See ReturnBody. + * * @author Michael J. Simons * @since 1.0 */ -public class Return extends TypedSubtree { +public class Return implements Visitable { - Return(List returnItems) { - super(returnItems); + private final ExpressionList returnItems; + + private final Order order; + private final Skip skip; + private final Limit limit; + + Return(ExpressionList returnItems, Order order, Skip skip, Limit limit) { + this.returnItems = returnItems; + this.order = order; + this.skip = skip; + this.limit = limit; + } + + @Override + public void accept(Visitor visitor) { + visitor.enter(this); + returnItems.accept(visitor); + if (order != null) { + order.accept(visitor); + } + if (skip != null) { + skip.accept(visitor); + } + if (limit != null) { + limit.accept(visitor); + } + visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java new file mode 100644 index 000000000..d31181e9d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * @author Gerrit Meier + */ +public class Skip implements Visitable { + + public static Skip of(Number value) { + return new Skip(new NumberLiteral(value)); + } + + private final NumberLiteral skipAmount; + + private Skip(NumberLiteral skipAmount) { + this.skipAmount = skipAmount; + } + + @Override + public void accept(Visitor visitor) { + visitor.enter(this); + skipAmount.accept(visitor); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java new file mode 100644 index 000000000..b48d7e07f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.springframework.data.neo4j.core.cypher.Expressions.*; + +import java.util.Optional; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +public class SortItem implements Visitable { + + private final Expression expression; + private final Direction direction; + + static SortItem create(Expression expression, @Nullable Direction direction) { + + return new SortItem(expression, Optional.ofNullable(direction).orElse(SortItem.Direction.UNDEFINED)); + } + + private SortItem(Expression expression, Direction direction) { + this.expression = expression; + this.direction = direction; + } + + public SortItem ascending() { + return new SortItem(this.expression, Direction.ASC); + } + + public SortItem descending() { + return new SortItem(this.expression, Direction.DESC); + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + nameOrExpression(this.expression).accept(visitor); + + if (this.direction != Direction.UNDEFINED) { + this.direction.accept(visitor); + } + visitor.leave(this); + } + + public enum Direction implements Visitable { + UNDEFINED(""), ASC("ASC"), DESC("DESC"); + + private final String symbol; + + Direction(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return this.symbol; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index 373a945f9..943c6a6a8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -20,9 +20,10 @@ package org.springframework.data.neo4j.core.cypher; /** * @author Michael J. Simons + * @author Gerrit Meier * @since 1.0 */ -interface StatementBuilder { +public interface StatementBuilder { /** * See {@link Cypher#match(PatternElement...)}. @@ -30,16 +31,53 @@ interface StatementBuilder { * @param pattern * @return */ - OngoingMatch match(PatternElement... pattern); - - /** - * A match that knows what to return and which is ready to be build. - */ - interface OngoingMatchAndReturn extends BuildableMatch { - } + OngoingMatchWithoutWhere match(PatternElement... pattern); /** * A match that exposes {@code returning} and {@code where} methods to add required information. + * While the where clause is optional, an returning clause needs to be specified before the + * statement can be build. + */ + interface OngoingMatchWithoutWhere extends OngoingMatch { + + /** + * Adds a where clause to this match. + * + * @param condition The new condition + * @return A match restricted by a where clause with no return items yet. + */ + OngoingMatchWithWhere where(Condition condition); + } + + /** + * A match that has a non-empty {@code where}-part. THe returning clause is still open. + */ + interface OngoingMatchWithWhere extends OngoingMatch { + + /** + * Adds an additional condition to the existing conditions, connected by an {@literal and}. + * Existing conditions will be logically grouped by using {@code ()} in the statement if previous + * conditions used another logical operator. + * + * @param condition An additional condition + * @return The ongoing definition of a match + */ + OngoingMatchWithWhere and(Condition condition); + + /** + * Adds an additional condition to the existing conditions, connected by an {@literal or}. + * Existing conditions will be logically grouped by using {@code ()} in the statement if previous + * conditions used another logical operator. + * + * @param condition An additional condition + * @return The ongoing definition of a match + */ + OngoingMatchWithWhere or(Condition condition); + } + + /** + * A match that exposes {@code returning} and for which it is not decided whether the optional + * where part has been used or note. */ interface OngoingMatch { @@ -50,27 +88,46 @@ interface StatementBuilder { * @return A match that can be build now */ OngoingMatchAndReturn returning(Expression... expressions); - - /** - * Creates a match that returns one ore more nodes. If the nodes have a symbolic name, that symbolic name is - * used in the return clause, otherwise the whole node pattern. - * - * @param nodes The nodes to be returned. Must not be null and be at least one node. - * @return A match that can be build now - */ - OngoingMatchAndReturn returning(Node... nodes); - - /** - * Adds a where clause to this match. - * - * @param condition The new condition - * @return A match restricted by a where clause with no return items yet. - */ - OngoingMatch where(Condition condition); } /** - * A match that has all information required to be build. + * A match that knows what to return and which is ready to be build. + */ + interface OngoingMatchAndReturn extends ExposesOrderBy, ExposesSkip, ExposesLimit { + } + + interface OngoingMatchAndReturnWithOrder extends ExposesSkip, ExposesLimit { + + /** + * Adds another expression to the list of order items. + * + * @return A new order specifying step. + */ + OngoingOrderDefinition and(Expression expression); + } + + /** + * An intermediate step while defining the order of a resultset. + */ + interface OngoingOrderDefinition extends ExposesSkip, ExposesLimit { + + /** + * Specifies descending order and jumps back to defining the match and return statement. + * + * @return The ongoing definition of a match + */ + T descending(); + + /** + * Specifies ascending order and jumps back to defining the match and return statement. + * + * @return The ongoing definition of a match + */ + T ascending(); + } + + /** + * A match that has all information required to be build and exposes a build method. */ interface BuildableMatch { @@ -79,4 +136,56 @@ interface StatementBuilder { */ Statement build(); } + + /** + * A step that exposes several methods to speficy ordering. + */ + interface ExposesOrderBy extends BuildableMatch { + + /** + * Order the result set by one or more {@link SortItem sort items}. Those can be retrieved for + * all expression with {@link Cypher#sort(Expression)} or directly from properties. + * + * @param sortItem One or more sort items + * @param The type of the step being returned + * @return A build step that still offers methods for defining skip and limit + */ + T orderBy(SortItem... sortItem); + + /** + * Order the result set by an expression. + * + * @param expression The expression to order by + * @return A step that allows for adding more expression or finetuning the sort direction of the last expression + */ + OngoingOrderDefinition orderBy(Expression expression); + } + + /** + * A step that exposes the {@link #skip(Number)} method. + */ + interface ExposesSkip extends BuildableMatch { + + /** + * Adds a skip clause, skipping the given number of records. + * + * @param number How many records to skip + * @return A step that only allows the limit of records to be specified + */ + ExposesLimit skip(Number number); + + } + + /** + * A step that exposes the {@link #limit(Number)} method. + */ + interface ExposesLimit extends BuildableMatch { + + /** + * Limits the number of returned records. + * @param number How many records to return + * @return A buildable match statement + */ + BuildableMatch limit(Number number); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java index 8322620d9..2f726eec0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java @@ -18,19 +18,58 @@ */ package org.springframework.data.neo4j.core.cypher; +import static java.util.regex.Pattern.*; + +import java.util.Locale; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.lang.Nullable; + /** + * The string representation of a string literal will be a quoted Cypher string in single tickmarks with + * escaped reserved characters. + * * @author Michael J. Simons * @since 1.0 */ public class StringLiteral extends Literal { + private static final Pattern RESERVED_CHARS = Pattern.compile("([" + quote("\\'\"") + "])"); + private static final String QUOTED_LITERAL_FORMAT = "'%s'"; + StringLiteral(CharSequence content) { super(content); } @Override - public String toString() { - // TODO escape all the things - return "'" + getContent() + "'"; + public String asString() { + + final Optional escapedContent = escapeString(getContent()); + return String.format(Locale.ENGLISH, QUOTED_LITERAL_FORMAT, escapedContent.orElse("")); } + + /** + * Escapes a string so that it can be used as a string literal in both single tickmarks ({@literal '}) + * and quotes ({@literal "}). + * + * @param unescapedString The string to escape + * @return An empty optional when the unescaped string is {@literal null}, an escaped string otherwise + */ + static Optional escapeString(@Nullable CharSequence unescapedString) { + + if (unescapedString == null) { + return Optional.empty(); + } + + final StringBuffer stringBuffer = new StringBuffer(); + Matcher matcher = RESERVED_CHARS.matcher(unescapedString); + while (matcher.find()) { + matcher.appendReplacement(stringBuffer, "\\\\\\" + matcher.group(1)); + } + matcher.appendTail(stringBuffer); + return Optional.of(stringBuffer.toString()); + } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index 6a84ae013..58cfb668a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -21,13 +21,18 @@ package org.springframework.data.neo4j.core.cypher.renderer; import static java.util.stream.Collectors.*; import java.util.HashSet; +import java.util.Locale; +import java.util.Optional; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.data.neo4j.core.cypher.*; -import org.springframework.data.neo4j.core.cypher.Relationship.Direction; +import org.springframework.data.neo4j.core.cypher.CompoundCondition.LogicalOperator; import org.springframework.data.neo4j.core.cypher.support.ReflectiveVisitor; import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.lang.Nullable; /** * This is a simple (some would call it naive) implementation of a visitor to the Cypher AST created by the Cypher builder @@ -47,6 +52,7 @@ class RenderingVisitor extends ReflectiveVisitor { private static final String LABEL_SEPARATOR = ":"; private static final String TYPE_SEPARATOR = ":"; + private static final Pattern LABEL_AND_TYPE_QUOTATION = Pattern.compile("`"); /** * Target of all rendering. @@ -129,6 +135,24 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append(" AS ").append(aliased.getAlias()); } + void enter(Order order) { + builder.append(" ORDER BY "); + } + + void enter(Skip skip) { + builder.append(" SKIP "); + } + + void enter(Limit limit) { + builder.append(" LIMIT "); + } + + void enter(SortItem.Direction direction) { + builder + .append(" ") + .append(direction.getSymbol()); + } + void enter(Property property) { builder .append(property.getParentAlias()) @@ -136,6 +160,17 @@ class RenderingVisitor extends ReflectiveVisitor { .append(property.getName()); } + void enter(FunctionInvocation functionInvocation) { + builder + .append(functionInvocation.getFunctionName()) + .append("("); + } + + void leave(FunctionInvocation functionInvocation) { + builder + .append(")"); + } + void enter(Comparison comparison) { builder .append(" ") @@ -143,15 +178,50 @@ class RenderingVisitor extends ReflectiveVisitor { .append(" "); } - void enter(StringLiteral expression) { - builder.append(expression.toString()); + void leave(IsNull isNull) { + builder + .append(" IS ") + .append(isNull.isNegated() ? "NOT" : "") + .append(" NULL"); + } + + void enter(CompoundCondition compoundCondition) { + builder.append("("); + } + + void leave(CompoundCondition compoundCondition) { + builder.append(")"); + } + + void enter(LogicalOperator logicalOperator) { + builder + .append(" ") + .append(logicalOperator.getSymbol()) + .append(" "); + } + + void enter(NotCondition notCondition) { + builder + .append("NOT ("); + } + + void leave(NotCondition notCondition) { + builder + .append(")"); + } + + void enter(Literal expression) { + builder.append(expression.asString()); } void enter(Node node) { builder.append("(") .append(node.getSymbolicName().map(SymbolicName::getName).orElse("")) .append(node.isLabeled() ? LABEL_SEPARATOR : "") - .append(node.getLabels().stream().map(RenderUtils::escapeName).collect(joining(LABEL_SEPARATOR))) + .append(node.getLabels().stream().map(RenderingVisitor::escapeName) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(joining(LABEL_SEPARATOR))) .append(")"); } @@ -161,13 +231,16 @@ class RenderingVisitor extends ReflectiveVisitor { void enter(RelationshipDetail details) { - Direction direction = details.getDirection(); + Relationship.Direction direction = details.getDirection(); builder.append(direction.getSymbolLeft()); builder .append("[") .append(details.getSymbolicName().map(SymbolicName::getName).orElse("")) .append(details.isTyped() ? TYPE_SEPARATOR : "") - .append(details.getTypes().stream().map(RenderUtils::escapeName).collect(joining(TYPE_SEPARATOR))) + .append(details.getTypes().stream().map(RenderingVisitor::escapeName) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(joining(TYPE_SEPARATOR))) .append("]"); builder.append(direction.getSymbolRight()); } @@ -175,5 +248,23 @@ class RenderingVisitor extends ReflectiveVisitor { public String getRenderedContent() { return this.builder.toString(); } + + /** + * Escapes a symbolic name. Such a symbolic name is either used for a nodes label, the type of a relationship or a + * variable. + * + * @param unescapedName The name to escape. + * @return An empty optional when the unescaped name is {@literal null}, otherwise the correctly escaped name, safe to be used in statements. + */ + static Optional escapeName(@Nullable CharSequence unescapedName) { + + if (unescapedName == null) { + return Optional.empty(); + } + + Matcher matcher = LABEL_AND_TYPE_QUOTATION.matcher(unescapedName); + return Optional.of(String.format(Locale.ENGLISH, "`%s`", matcher.replaceAll("``"))); + } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java index 1232aa082..4e9d825f6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java @@ -19,11 +19,12 @@ package org.springframework.data.neo4j.core.cypher.support; import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -96,29 +97,43 @@ public abstract class ReflectiveVisitor implements Visitor { try { handle.invoke(onVisitable); } catch (Throwable throwable) { + throwable.printStackTrace(); } }); } private Optional findHandleFor(TargetAndPhase targetAndPhase) { - try { - // Using MethodHandles.lookup().findVirtual() doesn't allow to make a protected method accessible. - Method method = this.getClass() - .getDeclaredMethod(targetAndPhase.phase.methodName, targetAndPhase.classOfVisitable); - method.setAccessible(true); - return Optional.of(MethodHandles.lookup().in(this.getClass()).unreflect(method).bindTo(this)); - } catch (IllegalAccessException | NoSuchMethodException e) { - // We don't do anything if the method doesn't exists - return Optional.empty(); + for (Class clazz : targetAndPhase.classHierarchyOfVisitable) { + try { + // Using MethodHandles.lookup().findVirtual() doesn't allow to make a protected method accessible. + Method method = this.getClass() + .getDeclaredMethod(targetAndPhase.phase.methodName, clazz); + method.setAccessible(true); + return Optional.of(MethodHandles.lookup().in(this.getClass()).unreflect(method).bindTo(this)); + } catch (IllegalAccessException | NoSuchMethodException e) { + // We don't do anything if the method doesn't exists + // Try the next parameter type in the hierarchy + } } + return Optional.empty(); } - @RequiredArgsConstructor @EqualsAndHashCode private static class TargetAndPhase { - private final Class classOfVisitable; + private final List> classHierarchyOfVisitable; private final Phase phase; + + TargetAndPhase(Class concreteVisitableClass, Phase phase) { + this.phase = phase; + this.classHierarchyOfVisitable = new ArrayList<>(); + + Class classOfVisitable = concreteVisitableClass; + do { + this.classHierarchyOfVisitable.add(classOfVisitable); + classOfVisitable = classOfVisitable.getSuperclass(); + } while (classOfVisitable != null); + } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java index e06c7e984..bb9b4cea0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java @@ -23,6 +23,9 @@ import java.util.Arrays; import java.util.List; /** + * This class helps to group items of the same type on the same level of the tree into a list structure that can be + * recognized by visitors. + * * @author Michael J. Simons * @since 1.0 */ @@ -33,7 +36,6 @@ public abstract class TypedSubtree implements Visitable { protected TypedSubtree(T... children) { this.children = Arrays.asList(children); - } protected TypedSubtree(List children) { @@ -42,10 +44,19 @@ public abstract class TypedSubtree implements Visitable { } @Override - public void accept(Visitor visitor) { + public final void accept(Visitor visitor) { visitor.enter(this); - this.children.forEach(child -> child.accept(visitor)); + this.children.forEach(child -> prepareVisit(child).accept(visitor)); visitor.leave(this); } + + /** + * A hook for interfere with the visitation of child elements. + * + * @param child The current child element + */ + protected Visitable prepareVisit(T child) { + return child; + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 9cef26e28..b8ca9d8e7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -58,7 +58,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { @Override protected Object getTargetRepository(RepositoryInformation metadata) { - return getTargetRepositoryViaReflection(metadata, nodeManager); + return getTargetRepositoryViaReflection(metadata, nodeManager, metadata.getDomainType()); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index d61f83a71..33b35652a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -18,14 +18,35 @@ */ package org.springframework.data.neo4j.repository.support; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.Optional; +import java.util.function.LongSupplier; import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.cypher.Condition; +import org.springframework.data.neo4j.core.cypher.Conditions; +import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Functions; +import org.springframework.data.neo4j.core.cypher.Node; +import org.springframework.data.neo4j.core.cypher.SortItem; +import org.springframework.data.neo4j.core.cypher.Statement; +import org.springframework.data.neo4j.core.cypher.StatementBuilder; +import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; +import org.springframework.data.neo4j.core.cypher.renderer.Renderer; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; +import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.repository.support.PageableExecutionUtils; +import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -39,20 +60,69 @@ import org.springframework.transaction.annotation.Transactional; @Transactional(readOnly = true) class SimpleNeo4jRepository implements Neo4jRepository { + private static final Renderer renderer = CypherRenderer.create(); + private final NodeManager nodeManager; - SimpleNeo4jRepository(NodeManager nodeManager) { + private final Class nodeClass; + + private final NodeDescription nodeDescription; + private Node node; + + SimpleNeo4jRepository(NodeManager nodeManager, Class nodeClass) { this.nodeManager = nodeManager; + this.nodeClass = nodeClass; + + this.nodeDescription = nodeManager.getSchema().getNodeDescription(nodeClass) + .orElseThrow(() -> new IllegalArgumentException("unsupported class")); + + this.node = Cypher.node(nodeDescription.getPrimaryLabel()).named("n"); } @Override public Iterable findAll(Sort sort) { - throw new UnsupportedOperationException("Not there yet."); + + Statement statement = match(node).returning(node).orderBy(createSort(sort)).build(); + return nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); } @Override public Page findAll(Pageable pageable) { - throw new UnsupportedOperationException("Not there yet."); + + StatementBuilder.OngoingMatchAndReturn returning = match(node).returning(node); + + StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); + + Statement statement = returningWithPaging.build(); + + List allResult = new ArrayList<>(nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass)); + LongSupplier totalCountSupplier = this::count; + return PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier); + } + + @Override + public Page findAll(Example example, Pageable pageable) { + + StatementBuilder.OngoingMatchAndReturn returning = match(node).where(createAndConditions(example)).returning(node); + + StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); + + Statement statement = returningWithPaging.build(); + + List allResult = new ArrayList<>(nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass)); + LongSupplier totalCountSupplier = this::count; + return (Page) PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier); + } + + private StatementBuilder.BuildableMatch addPagingParameter(Pageable pageable, StatementBuilder.OngoingMatchAndReturn returning) { + + Sort sort = pageable.getSort(); + + long skip = pageable.getOffset(); + + int pageSize = pageable.getPageSize(); + + return returning.orderBy(createSort(sort)).skip(skip).limit(pageSize); } @Override @@ -69,27 +139,38 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Optional findById(ID id) { - throw new UnsupportedOperationException("Not there yet."); + // todo only works for internal ids right now + Statement statement = match(node).where(node.internalId().isEqualTo(literalOf((Number) id))).returning(node) + .build(); + return nodeManager.executeTypedQueryForObject(renderer.render(statement), nodeClass); } @Override public boolean existsById(ID id) { - throw new UnsupportedOperationException("Not there yet."); + return findById(id).isPresent(); } @Override public Iterable findAll() { - throw new UnsupportedOperationException("Not there yet."); + + Statement statement = match(node).returning(node).build(); + return nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); } @Override public Iterable findAllById(Iterable ids) { - throw new UnsupportedOperationException("Not there yet."); + // todo only works for internal ids right now + Statement statement = match(node).where(node.internalId().isIn((Iterable) ids)).returning(node) + .build(); + return nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); } @Override public long count() { - throw new UnsupportedOperationException("Not there yet."); + + Statement statement = match(node).returning(Functions.count(node)).build(); + + return nodeManager.executeTypedQueryForObject(renderer.render(statement), Long.class).get(); } @Override @@ -122,31 +203,91 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Optional findOne(Example example) { - throw new UnsupportedOperationException("Not there yet."); + + Statement statement = match(node).where(createAndConditions(example)).returning(node).build(); + + return (Optional) nodeManager.executeTypedQueryForObject(renderer.render(statement), nodeClass); } @Override public Iterable findAll(Example example) { - throw new UnsupportedOperationException("Not there yet."); + Statement statement = match(node).where(createAndConditions(example)).returning(node).build(); + + return (Collection) nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); } @Override public Iterable findAll(Example example, Sort sort) { - throw new UnsupportedOperationException("Not there yet."); - } + Statement statement = match(node).where(createAndConditions(example)).returning(node).orderBy(createSort(sort)).build(); - @Override - public Page findAll(Example example, Pageable pageable) { - throw new UnsupportedOperationException("Not there yet."); + return (Collection) nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); } @Override public long count(Example example) { - throw new UnsupportedOperationException("Not there yet."); + Statement statement = match(node).where(createAndConditions(example)).returning(Functions.count(node)).build(); + + return nodeManager.executeTypedQueryForObject(renderer.render(statement), Long.class).get(); } @Override public boolean exists(Example example) { - throw new UnsupportedOperationException("Not there yet."); + return findAll(example).iterator().hasNext(); + } + + private Condition createAndConditions(Example example) { + + List conditionsFromProperties = createConditionsFromProperties(example); + + Condition result = Conditions.noCondition(); + for (Condition condition : conditionsFromProperties) { + result = result.and(condition); + } + return result; + + } + + + private List createConditionsFromProperties(Example example) { + + Collection graphProperties = nodeDescription.getGraphProperties(); + DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); + ExampleMatcher matcher = example.getMatcher(); + + List conditionList = new ArrayList<>(); + + for (GraphPropertyDescription graphProperty : graphProperties) { + String fieldName = graphProperty.getFieldName(); + if (matcher.isIgnoredPath(fieldName)) { + continue; + } + + Object propertyValue = beanWrapper.getPropertyValue(fieldName); + + if (matcher.getNullHandler() != ExampleMatcher.NullHandler.INCLUDE && propertyValue == null) { + continue; + } + + String propertyName = graphProperty.getPropertyName(); + + Condition condition = node.property(propertyName).isEqualTo(literalOf(propertyValue)); + conditionList.add(condition); + } + + return conditionList; + } + + private SortItem[] createSort(Sort sort) { + + return sort.stream().map(order -> { + String property = order.getProperty(); + SortItem sortItem = Cypher.sort(node.property(property)); + + // Spring's Sort.Order defaults to ascending, so we just need to change this if we have descending order. + if (order.isDescending()) { + sortItem = sortItem.descending(); + } + return sortItem; + }).toArray(SortItem[]::new); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 960206bce..687439004 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -19,6 +19,9 @@ package org.springframework.data.neo4j.core.cypher; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.neo4j.core.cypher.Conditions.*; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.core.cypher.Functions.*; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -27,6 +30,7 @@ import org.springframework.data.neo4j.core.cypher.renderer.Renderer; /** * @author Michael J. Simons + * @author Gerrit Meier */ public class CypherTest { @@ -104,7 +108,498 @@ public class CypherTest { .isEqualTo( "MATCH (u:`User`)-[r1:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(u:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); } + + @Test + void sortOrderDefault() { + Statement statement = Cypher.match(userNode).returning(userNode).orderBy(sort(userNode.property("name"))).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name"); + } + + @Test + void sortOrderAscending() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(Cypher.sort(userNode.property("name")).ascending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); + } + + @Test + void sortOrderDescending() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(Cypher.sort(userNode.property("name")).descending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name DESC"); + } + + @Test + void sortOrderConcatenation() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy( + sort(userNode.property("name")).descending(), + sort(userNode.property("age")).ascending() + ) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name DESC, u.age ASC"); + } + + @Test + void sortOrderDefaultExpression() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(userNode.property("name").ascending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); + } + + @Test + void sortOrderAscendingExpression() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(userNode.property("name").ascending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); + } + + @Test + void sortOrderDescendingExpression() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(userNode.property("name").descending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name DESC"); + } + + @Test + void sortOrderConcatenationExpression() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(userNode.property("name")).descending() + .and(userNode.property("age")).ascending() + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name DESC, u.age ASC"); + } + + @Test + void skip() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(1).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u SKIP 1"); + } + + @Test + void limit() { + Statement statement = Cypher.match(userNode).returning(userNode).limit(1).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u LIMIT 1"); + } + + @Test + void skipAndLimit() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(1).limit(1).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u SKIP 1 LIMIT 1"); + } + } + } + + @Nested + class FunctionRendering { + @Test + void inWhereClause() { + Statement statement = Cypher.match(userNode).where(userNode.internalId().isEqualTo(literalOf(1L))) + .returning(userNode).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE id(u) = 1 RETURN u"); } + @Test + void inReturnClause() { + Statement statement = Cypher.match(userNode).returning(Functions.count(userNode)).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN count(u)"); + } + + @Test + void aliasedInReturnClause() { + Statement statement = Cypher.match(userNode).returning(Functions.count(userNode).as("cnt")).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN count(u) AS cnt"); + } + + @Test + void shouldSupportMoreThanOneArgument() { + Statement statement = Cypher.match(userNode) + .returning(coalesce(userNode.property("a"), userNode.property("b"), Cypher.literalOf("¯\\_(ツ)_/¯"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN coalesce(u.a, u.b, '¯\\\\_(ツ)_/¯')"); + } + } + + @Nested + class ComparisonRendering { + + @Test + void equalsWithStringLiteral() { + Statement statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(Cypher.literalOf("Test"))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.name = 'Test' RETURN u"); + } + + @Test + void equalsWithNumberLiteral() { + Statement statement = Cypher.match(userNode) + .where(userNode.property("age").isEqualTo(Cypher.literalOf(21))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.age = 21 RETURN u"); + } + + @Test + void equalsWithObjectLiteral() { + Statement statement = Cypher.match(userNode) + .where(userNode.property("field").isEqualTo(Cypher.literalOf(new CustomType()))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.field = Value RETURN u"); + } + + + private class CustomType { + @Override + public String toString() { + return "Value"; + } + } + } + + @Nested + class Conditions { + @Test + void conditionsChainingAnd() { + Statement statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(Cypher.literalOf("Test")) + .and(userNode.property("age").isEqualTo(Cypher.literalOf(21)))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' AND u.age = 21) RETURN u"); + } + + @Test + void conditionsChainingOr() { + Statement statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(Cypher.literalOf("Test")) + .or(userNode.property("age").isEqualTo(Cypher.literalOf(21)))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' OR u.age = 21) RETURN u"); + } + + @Test + void conditionsChainingXor() { + Statement statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(Cypher.literalOf("Test")) + .xor(userNode.property("age").isEqualTo(Cypher.literalOf(21)))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' XOR u.age = 21) RETURN u"); + } + + @Test + void chainingOnWhere() { + Statement statement; + + Literal test = literalOf("Test"); + Literal foobar = literalOf("foobar"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE u.name = 'Test' RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'Test' AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'Test' OR u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'Test') OR u.name = 'foobar') AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((u.name = 'Test' OR u.name = 'foobar') AND u.name = 'Test' AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((((u.name = 'Test' OR u.name = 'foobar') AND u.name = 'Test') OR u.name = 'foobar') AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isNotNull()) + .and(userNode.property("name").isEqualTo(test)) + .or(userNode.property("age").isEqualTo(Cypher.literalOf(21))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((u.name IS NOT NULL AND u.name = 'Test') OR u.age = 21) RETURN u"); + } + + @Test + void chainingOnConditions() { + Statement statement; + + Literal test = literalOf("Test"); + Literal foobar = literalOf("foobar"); + Literal bazbar = literalOf("bazbar"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .and(userNode.property("name").isEqualTo(bazbar)) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test)) + .and( + userNode.property("name").isEqualTo(bazbar) + .and(userNode.property("name").isEqualTo(foobar)) + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'bazbar' AND u.name = 'foobar') RETURN u"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .and(userNode.property("name").isEqualTo(bazbar)) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .and(userNode.property("name").isEqualTo(bazbar)) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(bazbar)) + ) + .returning(userNode) + .build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') AND u.name = 'bazbar') RETURN u"); + } + + @Test + void chainingCombined() { + Statement statement; + + Literal test = literalOf("Test"); + Literal foobar = literalOf("foobar"); + Literal bazbar = literalOf("bazbar"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .and(userNode.property("name").isEqualTo(bazbar)) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + ) + .and( + userNode.property("name").isEqualTo(bazbar) + .and(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(test)) + .not() + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') AND NOT (((u.name = 'bazbar' AND u.name = 'foobar') OR u.name = 'Test'))) RETURN u"); + + } + + @Test + void negatedConditions() { + Statement statement = Cypher.match(userNode) + .where(userNode.property("name").isNotNull().not()) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE NOT (u.name IS NOT NULL) RETURN u"); + } + + @Test + void noConditionShouldNotBeRendered() { + Statement statement; + statement = Cypher.match(userNode) + .where(noCondition()) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(Cypher.literalOf("test"))) + .and(noCondition()).or(noCondition()) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.name = 'test' RETURN u"); + } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java index d1281e773..aea3e220d 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java @@ -49,15 +49,5 @@ class DefaultStatementBuilderTest { assertThatIllegalArgumentException().isThrownBy(() -> builder.returning(new Expression[0])) .withMessage("At least one expressions to return is required."); } - - @Test - void forNodes() { - - DefaultStatementBuilder builder = new DefaultStatementBuilder(); - assertThatIllegalArgumentException().isThrownBy(() -> builder.returning((Node[]) null)) - .withMessage("Nodes to return are required."); - assertThatIllegalArgumentException().isThrownBy(() -> builder.returning(new Node[0])) - .withMessage("At least one node to return is required."); - } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java new file mode 100644 index 000000000..2cc80297f --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class FunctionsTest { + + @Nested + class id { + + @Test + void preconditionsShouldBeAsserted() { + + assertThatIllegalArgumentException().isThrownBy(() -> Functions.id((Node) null)) + .withMessage("The node parameter is required."); + + assertThatIllegalArgumentException().isThrownBy(() -> Functions.id((Relationship) null)) + .withMessage("The relationship parameter is required."); + } + } + + @Nested + class count { + + @Test + void preconditionsShouldBeAsserted() { + + assertThatIllegalArgumentException().isThrownBy(() -> Functions.count(null)) + .withMessage("The expression to count is required."); + } + } + + @Nested + class coalesce { + + @Test + void preconditionsShouldBeAsserted() { + + assertThatIllegalArgumentException().isThrownBy(() -> Functions.coalesce(null)) + .withMessage("At least one expression is required."); + + assertThatIllegalArgumentException().isThrownBy(() -> Functions.coalesce(new Expression[0])) + .withMessage("At least one expression is required."); + + assertThatIllegalArgumentException().isThrownBy(() -> Functions.coalesce(new Expression[] { null })) + .withMessage("At least one expression is required."); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/StringLiteralTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/StringLiteralTest.java new file mode 100644 index 000000000..7960e232e --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/StringLiteralTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class StringLiteralTest { + + @Test + void shouldEscapeContent() { + StringLiteral literal = new StringLiteral("A\\B\\\\Ca'bc123\\"); + + assertThat(literal.asString()).isEqualTo("'A\\\\B\\\\\\\\Ca\\'bc123\\\\'"); + } + + @Test + void shouldEscapeNull() { + StringLiteral literal = new StringLiteral(null); + + assertThat(literal.asString()).isEqualTo("''"); + } + + @Test + void shouldCorrectlyEscapeEmptyStrings() { + + for (String[] strings : new String[][] { + { "", "" }, + { " \t ", " \t " }, + { "Nothing to escape", "Nothing to escape" }, + { "' \" '", "\\' \\\" \\'" } + }) { + String string = strings[0]; + String expectedEscapedString = strings[1]; + assertThat(StringLiteral.escapeString(string)).hasValue(expectedEscapedString); + } + } + + @Test + void shouldNotTryToEscapeNullStrings() { + assertThat(StringLiteral.escapeString(null)).isEmpty(); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtilsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitorTest.java similarity index 82% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtilsTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitorTest.java index 7b4f8204e..449e320bc 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderUtilsTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitorTest.java @@ -20,13 +20,14 @@ package org.springframework.data.neo4j.core.cypher.renderer; import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; /** * @author Michael J. Simons */ -class RenderUtilsTest { +class RenderingVisitorTest { @ParameterizedTest @CsvSource({ @@ -38,6 +39,12 @@ class RenderUtilsTest { }) void shouldCorrectlyEscapeNames(String name, String expectedEscapedName) { - assertThat(RenderUtils.escapeName(name)).isEqualTo(expectedEscapedName); + assertThat(RenderingVisitor.escapeName(name)).hasValue(expectedEscapedName); + } + + @Test + void shouldNotTryToEscapeNullNames() { + + assertThat(RenderingVisitor.escapeName(null)).isEmpty(); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java index b53214c37..41abdca66 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.integration; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -31,10 +32,17 @@ import org.springframework.data.neo4j.core.schema.Node; @Node @ToString @AllArgsConstructor +@EqualsAndHashCode public class PersonWithAllConstructor { @Id private final Long id; private final String name; + + private final String sameValue; + + static PersonWithAllConstructor of(String sameValue) { + return new PersonWithAllConstructor(null, null, sameValue); + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index ed43bd011..d34a45648 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.integration; import static org.assertj.core.api.Assertions.*; +import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -33,6 +34,11 @@ import org.neo4j.driver.Transaction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; @@ -48,12 +54,19 @@ import org.testcontainers.junit.jupiter.Testcontainers; @ContextConfiguration(classes = RepositoryIT.Config.class) @Testcontainers class RepositoryIT { + private static final String TEST_PERSON1_NAME = "Test"; + private static final String TEST_PERSON2_NAME = "Test2"; + private static final String TEST_PERSON_SAMEVALUE = "SameValue"; @Container private static Neo4jContainer neo4jContainer = new Neo4jContainer().withoutAuthentication(); private final PersonRepository repository; private final Driver driver; + private Long id1; + private Long id2; + private PersonWithAllConstructor person1; + private PersonWithAllConstructor person2; @Autowired RepositoryIT(PersonRepository repository, Driver driver) { @@ -63,81 +76,197 @@ class RepositoryIT { } @BeforeEach - void setupDb() { + void setupData() { Transaction transaction = driver.session().beginTransaction(); transaction.run("MATCH (n) detach delete n"); - transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = 'Test'"); - transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = 'Test'"); - transaction.run("CREATE (n:PersonWithWither) SET n.name = 'Test'"); + id1 = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = '" + TEST_PERSON1_NAME + "', n.sameValue = '" + TEST_PERSON_SAMEVALUE + "' return id(n)") + .next().get(0).asLong(); + id2 = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = '" + TEST_PERSON2_NAME + "', n.sameValue = '" + TEST_PERSON_SAMEVALUE + "' return id(n)") + .next().get(0).asLong(); + transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = '" + TEST_PERSON1_NAME + "'"); + transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.success(); transaction.close(); + // note that there is no id setting in the mapping right now + person1 = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON_SAMEVALUE); + person2 = new PersonWithAllConstructor(null, TEST_PERSON2_NAME, TEST_PERSON_SAMEVALUE); + } + + @Test + void findAll() { + Iterable people = repository.findAll(); + assertThat(people).hasSize(2); + assertThat(people).extracting("name").containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME); + } + + @Test + void findById() { + Optional person = repository.findById(id1); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); + } + + @Test + void existsById() { + boolean exists = repository.existsById(id1); + assertThat(exists).isTrue(); + } + + @Test + void findAllById() { + Iterable persons = repository.findAllById(Arrays.asList(id1, id2)); + assertThat(persons).hasSize(2); + } + + @Test + void count() { + assertThat(repository.count()).isEqualTo(2); + } + + @Test + void findAllWithSortByOrderDefault() { + Iterable persons = repository.findAll(Sort.by("name")); + + assertThat(persons).containsExactly(person1, person2); + } + + @Test + void findAllWithSortByOrderAsc() { + Iterable persons = repository.findAll(Sort.by(Sort.Order.asc("name"))); + + assertThat(persons).containsExactly(person1, person2); + } + + @Test + void findAllWithSortByOrderDesc() { + Iterable persons = repository.findAll(Sort.by(Sort.Order.desc("name"))); + + assertThat(persons).containsExactly(person2, person1); + } + + @Test + void findAllWithPageable() { + Sort sort = Sort.by("name"); + int page = 0; + int limit = 1; + Page persons = repository.findAll(PageRequest.of(page, limit, sort)); + + assertThat(persons).containsExactly(person1); + + page = 1; + persons = repository.findAll(PageRequest.of(page, limit, sort)); + assertThat(persons).containsExactly(person2); + } + + @Test + void findOneByExample() { + Example example = Example.of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); + Optional person = repository.findOne(example); + + assertThat(person).isPresent(); + assertThat(person.get()).isEqualTo(person1); + } + + @Test + void findAllByExample() { + Example example = Example.of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); + Iterable persons = repository.findAll(example); + + assertThat(persons).containsExactly(person1); + } + + @Test + void findAllByExampleWithSort() { + Example example = Example.of(PersonWithAllConstructor.of(TEST_PERSON_SAMEVALUE)); + Iterable persons = repository.findAll(example, Sort.by(Sort.Direction.DESC, "name")); + + assertThat(persons).containsExactly(person2, person1); + } + + @Test + void findAllByExampleWithPagination() { + Example example = Example.of(PersonWithAllConstructor.of(TEST_PERSON_SAMEVALUE)); + Iterable persons = repository.findAll(example, PageRequest.of(1, 1, Sort.by("name"))); + + assertThat(persons).containsExactly(person2); + } + + @Test + void existsByExample() { + Example example = Example.of(PersonWithAllConstructor.of(TEST_PERSON_SAMEVALUE)); + boolean exists = repository.exists(example); + + assertThat(exists).isTrue(); + } + + @Test + void countByExample() { + Example example = Example.of(person1); + long count = repository.count(example); + + assertThat(count).isEqualTo(1); } @Test void loadAllPersonsWithAllConstructor() { List persons = repository.getAllPersonsViaQuery(); - assertThat(persons).anyMatch(person -> person.getName().equals("Test")); + assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME)); } @Test void loadOnePersonWithAllConstructor() { PersonWithAllConstructor person = repository.getOnePersonViaQuery(); - assertThat(person.getName()).isEqualTo("Test"); + assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME); } @Test void loadOptionalPersonWithAllConstructor() { Optional person = repository.getOptionalPersonsViaQuery(); assertThat(person).isPresent(); - assertThat(person.get().getName()).isEqualTo("Test"); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @Test void loadAllPersonsWithNoConstructor() { List persons = repository.getAllPersonsWithNoConstructorViaQuery(); - assertThat(persons).anyMatch(person -> person.getName().equals("Test")); + assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME)); } @Test void loadOnePersonWithNoConstructor() { PersonWithNoConstructor person = repository.getOnePersonWithNoConstructorViaQuery(); - assertThat(person.getName()).isEqualTo("Test"); + assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME); } @Test void loadOptionalPersonWithNoConstructor() { Optional person = repository.getOptionalPersonsWithNoConstructorViaQuery(); assertThat(person).isPresent(); - assertThat(person.get().getName()).isEqualTo("Test"); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @Test void loadAllPersonsWithWither() { List persons = repository.getAllPersonsWithWitherViaQuery(); - assertThat(persons).anyMatch(person -> person.getName().equals("Test")); + assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME)); } @Test void loadOnePersonWithWither() { PersonWithWither person = repository.getOnePersonWithWitherViaQuery(); - assertThat(person.getName()).isEqualTo("Test"); + assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME); } @Test void loadOptionalPersonWithWither() { Optional person = repository.getOptionalPersonsWithWitherViaQuery(); assertThat(person).isPresent(); - assertThat(person.get().getName()).isEqualTo("Test"); - } - - @Test - void repositoryCallFailsBecauseOfUnsupportedOperationException() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll()); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @Test @@ -148,7 +277,8 @@ class RepositoryIT { @Test void findBySimpleProperty() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findByName("Test")); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> repository.findByName(TEST_PERSON1_NAME)); } @Configuration @@ -160,7 +290,7 @@ class RepositoryIT { public Driver driver() { String boltUrl = neo4jContainer.getBoltUrl(); - return GraphDatabase.driver(boltUrl, AuthTokens.none()); + return GraphDatabase.driver(boltUrl, AuthTokens.basic("neo4j", "secret")); } @Bean diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java index 2793ee0a1..dbf3e2af2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java @@ -22,13 +22,15 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Collections; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.Schema; /** * @author Gerrit Meier @@ -37,12 +39,23 @@ class SimpleNeo4jRepositoryTest { private NodeManager nodeManager; - private SimpleNeo4jRepository repository; + private Schema schema; + + private NodeDescription nodeDescription; + + private SimpleNeo4jRepository repository; @BeforeEach void setupMock() { nodeManager = mock(NodeManager.class); - repository = new SimpleNeo4jRepository(this.nodeManager); + + schema = mock(Schema.class); + when(nodeManager.getSchema()).thenReturn(schema); + + nodeDescription = mock(NodeDescription.class); + when(nodeDescription.getPrimaryLabel()).thenReturn("TestNode"); + when(schema.getNodeDescription(TestNode.class)).thenReturn(Optional.of(nodeDescription)); + repository = new SimpleNeo4jRepository(this.nodeManager, TestNode.class); } @Test @@ -56,21 +69,6 @@ class SimpleNeo4jRepositoryTest { assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.saveAll(null)); } - @Test - void findByIdNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findById(null)); - } - - @Test - void existsByIdNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.existsById(null)); - } - - @Test - void findAllByIdNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAllById(null)); - } - @Test void deleteByIdNotImplemented() { assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteById(null)); @@ -81,71 +79,17 @@ class SimpleNeo4jRepositoryTest { assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.delete(null)); } - @Test - void deleteAllNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteAll()); - } - @Test void deleteAll1NotImplemented() { assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> repository.deleteAll(Collections.emptyList())); } - @Test - void findOneNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findOne(null)); - } + @Node + class TestNode { + @Id + private Long id; - @Test - void findAllNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll()); - } - - @Test - void findAllExampleNotImplemented() { - Example example = Example.of(""); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll(example)); - } - - @Test - void findAllPageableNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> repository.findAll(Pageable.unpaged())); - } - - @Test - void findAllSortByNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> repository.findAll(Sort.by("property"))); - } - - @Test - void findAllExampleAndSortNotImplemented() { - Example example = Example.of(""); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> repository.findAll(example, Sort.by("property"))); - } - - @Test - void findAllExampleAndPageableNotImplemented() { - Example example = Example.of(""); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> repository.findAll(example, Pageable.unpaged())); - } - - @Test - void countNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.count()); - } - - @Test - void countWithExampleNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.count(null)); - } - - @Test - void existsNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.findAll()); + private String value; } } From 739f91cc1f219a6088d599845a79fd26911a375f Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 7 May 2019 13:45:18 +0200 Subject: [PATCH 069/342] Add DSL methods for delete(). --- .../core/cypher/DefaultStatementBuilder.java | 218 ++++++++++++------ .../data/neo4j/core/cypher/Delete.java | 49 ++++ .../data/neo4j/core/cypher/Node.java | 24 +- .../data/neo4j/core/cypher/ReadingClause.java | 2 +- .../neo4j/core/cypher/SinglePartQuery.java | 49 +++- .../neo4j/core/cypher/StatementBuilder.java | 38 ++- .../neo4j/core/cypher/UpdatingClause.java | 28 +++ .../core/cypher/renderer/CypherRenderer.java | 2 +- .../cypher/renderer/RenderingVisitor.java | 17 +- .../data/neo4j/core/cypher/CypherTest.java | 65 +++++- 10 files changed, 403 insertions(+), 89 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UpdatingClause.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index 46047af36..d3a8d3f94 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -21,12 +21,11 @@ package org.springframework.data.neo4j.core.cypher; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndReturn; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithWhere; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingOrderDefinition; import org.springframework.util.Assert; /** @@ -38,16 +37,10 @@ class DefaultStatementBuilder implements StatementBuilder, OngoingMatch, OngoingMatchWithWhere, - OngoingMatchWithoutWhere, - OngoingMatchAndReturn, - OngoingOrderDefinition, StatementBuilder.OngoingMatchAndReturnWithOrder { + OngoingMatchWithoutWhere { private List matchList = new ArrayList<>(); - private List returnList = new ArrayList<>(); - private List sortItemList = new ArrayList<>(); - private SortItem lastSortItem; - private Skip skip; - private Limit limit; + private Condition condition; @Override @@ -63,55 +56,21 @@ class DefaultStatementBuilder @Override public OngoingMatchAndReturn returning(Expression... expressions) { - Assert.notNull(expressions, "Expressions to return are required."); - Assert.notEmpty(expressions, "At least one expressions to return is required."); - - this.returnList.addAll(Arrays.asList(expressions)); - return this; + DefaultStatementWithReturnBuilder ongoingMatchAndReturn = new DefaultStatementWithReturnBuilder(); + ongoingMatchAndReturn.addExpressions(expressions); + return ongoingMatchAndReturn; } @Override - public OngoingMatchAndReturn orderBy(SortItem... sortItem) { - Arrays.stream(sortItem).forEach(this.sortItemList::add); - return this; + public OngoingDetachDelete detach() { + + return new DefaultStatementWithDeleteBuilder(true); } @Override - public OngoingOrderDefinition orderBy(Expression expression) { - this.lastSortItem = Cypher.sort(expression); - return this; + public OngoingMatchAndDelete delete(Expression... expressions) { - } - - @Override - public OngoingMatchAndReturn descending() { - this.sortItemList.add(this.lastSortItem.descending()); - this.lastSortItem = null; - return this; - } - - @Override - public OngoingMatchAndReturn ascending() { - this.sortItemList.add(this.lastSortItem.ascending()); - this.lastSortItem = null; - return this; - } - - @Override - public OngoingOrderDefinition and(Expression expression) { - return orderBy(expression); - } - - @Override - public OngoingMatchAndReturn skip(Number number) { - skip = Skip.of(number); - return this; - } - - @Override - public OngoingMatchAndReturn limit(Number number) { - limit = Limit.of(number); - return this; + return new DefaultStatementWithDeleteBuilder(false).delete(expressions); } @Override @@ -135,22 +94,147 @@ class DefaultStatementBuilder return this; } - @Override - public Statement build() { - - Pattern pattern = new Pattern(this.matchList); - Match match = new Match(pattern, hasCondition() ? new Where(this.condition) : null); - ExpressionList returnItems = new ExpressionList(this.returnList); - - if (lastSortItem != null) { - sortItemList.add(lastSortItem); - } - Order order = sortItemList.size() > 0 ? new Order(sortItemList) : null; - - return new SinglePartQuery(match, new Return(returnItems, order, skip, limit)); - } - private boolean hasCondition() { return !(this.condition == null || this.condition == CompoundCondition.EMPTY_CONDITION); } + + protected Match buildMatch() { + Pattern pattern = new Pattern(DefaultStatementBuilder.this.matchList); + return new Match(pattern, hasCondition() ? new Where(DefaultStatementBuilder.this.condition) : null); + } + + class DefaultStatementWithReturnBuilder + implements OngoingMatchAndReturn, OngoingOrderDefinition, OngoingMatchAndReturnWithOrder { + + private final List returnList = new ArrayList<>(); + private final List sortItemList = new ArrayList<>(); + private SortItem lastSortItem; + private Skip skip; + private Limit limit; + + protected final DefaultStatementWithReturnBuilder addExpressions(Expression... expressions) { + + Assert.notNull(expressions, "Expressions to return are required."); + Assert.notEmpty(expressions, "At least one expressions to return is required."); + + this.returnList.addAll(Arrays.asList(expressions)); + return this; + } + + @Override + public final OngoingMatchAndReturn orderBy(SortItem... sortItem) { + Arrays.stream(sortItem).forEach(this.sortItemList::add); + return this; + } + + @Override + public final OngoingOrderDefinition orderBy(Expression expression) { + this.lastSortItem = Cypher.sort(expression); + return this; + } + + @Override + public final OngoingOrderDefinition and(Expression expression) { + return orderBy(expression); + } + + @Override + public final OngoingMatchAndReturn descending() { + this.sortItemList.add(this.lastSortItem.descending()); + this.lastSortItem = null; + return this; + } + + @Override + public final OngoingMatchAndReturn ascending() { + this.sortItemList.add(this.lastSortItem.ascending()); + this.lastSortItem = null; + return this; + } + + @Override + public final OngoingMatchAndReturn skip(Number number) { + skip = Skip.of(number); + return this; + } + + @Override + public final OngoingMatchAndReturn limit(Number number) { + limit = Limit.of(number); + return this; + } + + protected final Optional buildReturn() { + + if (returnList.isEmpty()) { + return Optional.empty(); + } + + ExpressionList returnItems = new ExpressionList(this.returnList); + + if (lastSortItem != null) { + sortItemList.add(lastSortItem); + } + Order order = sortItemList.size() > 0 ? new Order(sortItemList) : null; + return Optional.of(new Return(returnItems, order, skip, limit)); + } + + @Override + public Statement build() { + + Match match = buildMatch(); + // This must be filled at this stage + Return aReturn = buildReturn().get(); + return SinglePartQuery.createReturningQuery(aReturn, match); + } + } + + class DefaultStatementWithDeleteBuilder extends DefaultStatementWithReturnBuilder + implements OngoingDetachDelete, OngoingMatchAndDelete { + + private final List deleteList = new ArrayList<>(); + private final boolean detach; + + DefaultStatementWithDeleteBuilder(boolean detach) { + this.detach = detach; + } + + @Override + public OngoingMatchAndDelete delete(Expression... expressions) { + + Assert.notNull(expressions, "Expressions to delete are required."); + Assert.notEmpty(expressions, "At least one expressions to delete is required."); + + this.deleteList.addAll(Arrays.asList(expressions)); + + return this; + } + + @Override + public OngoingMatchAndReturn returning(Expression... expressions) { + + + Assert.notNull(expressions, "Expressions to return are required."); + Assert.notEmpty(expressions, "At least one expressions to return is required."); + + super.returnList.addAll(Arrays.asList(expressions)); + return this; + } + + protected final Delete buildDelete() { + + ExpressionList deleteItems = new ExpressionList(this.deleteList); + return new Delete(deleteItems, this.detach); + } + + @Override + public Statement build() { + + Match match = buildMatch(); + Delete delete = buildDelete(); + Optional optionalReturn = buildReturn(); + + return SinglePartQuery.createUpdatingQuery(match, delete, optionalReturn.orElse(null)); + } + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java new file mode 100644 index 000000000..4673fdf03 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * See Delete. + * + * @author Michael J. Simons + */ +public class Delete implements UpdatingClause { + + private final ExpressionList deleteItems; + + private final boolean detach; + + Delete(ExpressionList deleteItems, boolean detach) { + this.deleteItems = deleteItems; + this.detach = detach; + } + + public boolean isDetach() { + return detach; + } + + @Override + public void accept(Visitor visitor) { + visitor.enter(this); + deleteItems.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java index 91d3f5d5a..1c9d7e7c4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java @@ -111,15 +111,33 @@ public class Node implements PatternElement, Named, Expression { return Functions.id(this); } - public OngoingRelationshipDefinition outgoingRelationShipTo(Node other) { + /** + * Starts building an outgoing relationship to the {@code other} {@link Node node}. + * + * @param other The other end of the outgoing relationship + * @return An ongoing relationship definition, that can be used to specify the type + */ + public OngoingRelationshipDefinition relationshipTo(Node other) { return new DefaultOngoingRelationshipDefinition(this, Direction.LTR, other); } - public OngoingRelationshipDefinition incomingRelationShipFrom(Node other) { + /** + * Starts building an incoming relationship starting at the {@code other} {@link Node node}. + * + * @param other The source of the incoming relationship + * @return An ongoing relationship definition, that can be used to specify the type + */ + public OngoingRelationshipDefinition relationshipFrom(Node other) { return new DefaultOngoingRelationshipDefinition(this, Direction.RTR, other); } - public OngoingRelationshipDefinition relationshipWith(Node other) { + /** + * Starts building an undirected relationship between this {@link Node node} and the {@code other}. + * + * @param other The other end of the relationship + * @return An ongoing relationship definition, that can be used to specify the type + */ + public OngoingRelationshipDefinition relationshipBetween(Node other) { return new DefaultOngoingRelationshipDefinition(this, Direction.UNI, other); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java index 0ac176e4a..b6a8cef73 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java @@ -24,5 +24,5 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; * @author Michael J. Simons * @since 1.0 */ -public interface ReadingClause extends Visitable { +interface ReadingClause extends Visitable { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java index 450bdc83c..6917a9b12 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java @@ -19,8 +19,10 @@ package org.springframework.data.neo4j.core.cypher; import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; +import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * See SinglePartQuery. @@ -32,20 +34,53 @@ public class SinglePartQuery implements SingleQuery { private @Nullable final ReadingClause readingClause; - private final Return aReturn; + private @Nullable final UpdatingClause updatingClause; - SinglePartQuery(@Nullable ReadingClause readingClause, Return aReturn) { + private @Nullable final Return aReturn; + + /** + * Creates a single part query consisting of a return clause with an optional reading clause + * + * @param aReturn The expressions to return + * @param readingClause The optional reading clause + * @return + */ + static SinglePartQuery createReturningQuery(Return aReturn, @Nullable ReadingClause readingClause) { + + Assert.notNull(aReturn, "A return clause is required."); + + return new SinglePartQuery(readingClause, null, aReturn); + } + + /** + * Creates a single part query representing an update of some kind. Updates have an optional return clause. + * + * @param readingClause The expressions to match + * @param updatingClause The expressions to update + * @param aReturn The expressions to return, optional + * @return + */ + static SinglePartQuery createUpdatingQuery(ReadingClause readingClause, UpdatingClause updatingClause, + @Nullable Return aReturn) { + + Assert.notNull(readingClause, "The reading clause is required."); + Assert.notNull(updatingClause, "The update clause is required."); + + return new SinglePartQuery(readingClause, updatingClause, aReturn); + } + + private SinglePartQuery(@Nullable ReadingClause readingClause, @Nullable UpdatingClause updatingClause, + @Nullable Return aReturn) { this.readingClause = readingClause; + this.updatingClause = updatingClause; this.aReturn = aReturn; } @Override public void accept(Visitor visitor) { - if (readingClause != null) { - readingClause.accept(visitor); - } - - aReturn.accept(visitor); + Visitable.visitIfNotNull(readingClause, visitor); + Visitable.visitIfNotNull(updatingClause, visitor); + Visitable.visitIfNotNull(aReturn, visitor); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index 943c6a6a8..abe9a5483 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -79,15 +79,14 @@ public interface StatementBuilder { * A match that exposes {@code returning} and for which it is not decided whether the optional * where part has been used or note. */ - interface OngoingMatch { + interface OngoingMatch extends ExposesReturning, OngoingDetachDelete { /** - * Create a match that returns one or more expressions. + * Starts building a delete step that will use {@code DETACH} to remove relationships. * - * @param expressions The expressions to be returned. Must not be null and be at least one expression. - * @return A match that can be build now + * @return An ongoing delete step that is used to specify things to be deleted. */ - OngoingMatchAndReturn returning(Expression... expressions); + OngoingDetachDelete detach(); } /** @@ -137,6 +136,16 @@ public interface StatementBuilder { Statement build(); } + interface ExposesReturning { + /** + * Create a match that returns one or more expressions. + * + * @param expressions The expressions to be returned. Must not be null and be at least one expression. + * @return A match that can be build now + */ + OngoingMatchAndReturn returning(Expression... expressions); + } + /** * A step that exposes several methods to speficy ordering. */ @@ -188,4 +197,23 @@ public interface StatementBuilder { */ BuildableMatch limit(Number number); } + + /** + * A step that exposes only the delete clause. + */ + interface OngoingDetachDelete { + /** + * Creates a delete step with one or more expressions to be deleted. + * + * @param expressions The expressions to be deleted. + * @return A match with a delete clause that can be build now + */ + OngoingMatchAndDelete delete(Expression... expressions); + } + + /** + * A buildable step that will create a MATCH ... DELETE statement. + */ + interface OngoingMatchAndDelete extends BuildableMatch, ExposesReturning { + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UpdatingClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UpdatingClause.java new file mode 100644 index 000000000..f72fe8ea7 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UpdatingClause.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +interface UpdatingClause extends Visitable { +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java index 38be62588..4d6145fcf 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java @@ -40,6 +40,6 @@ public class CypherRenderer implements Renderer { RenderingVisitor renderingVisitor = new RenderingVisitor(); statement.accept(renderingVisitor); - return renderingVisitor.getRenderedContent(); + return renderingVisitor.getRenderedContent().trim(); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index 58cfb668a..7f339dfb0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -131,6 +131,19 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append("RETURN "); } + void enter(Delete delete) { + + if (delete.isDetach()) { + builder.append("DETACH "); + } + + builder.append("DELETE "); + } + + void leave(Delete match) { + builder.append(" "); + } + void leave(AliasedExpression aliased) { builder.append(" AS ").append(aliased.getAlias()); } @@ -181,8 +194,8 @@ class RenderingVisitor extends ReflectiveVisitor { void leave(IsNull isNull) { builder .append(" IS ") - .append(isNull.isNegated() ? "NOT" : "") - .append(" NULL"); + .append(isNull.isNegated() ? "NOT " : "") + .append("NULL"); } void enter(CompoundCondition compoundCondition) { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 687439004..657c721ab 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -69,7 +69,7 @@ public class CypherTest { @Test void simpleRelationship() { Statement statement = Cypher - .match(userNode.outgoingRelationShipTo(bikeNode).withType("OWNS").create()) + .match(userNode.relationshipTo(bikeNode).withType("OWNS").create()) .returning(bikeNode, userNode) .build(); @@ -80,7 +80,7 @@ public class CypherTest { @Test void simpleRelationshipWithReturn() { Relationship owns = userNode - .outgoingRelationShipTo(bikeNode).withType("OWNS").named("o").create(); + .relationshipTo(bikeNode).withType("OWNS").named("o").create(); Statement statement = Cypher .match(owns) @@ -96,7 +96,7 @@ public class CypherTest { Node tripNode = Cypher.node("Trip").named("u"); Statement statement = Cypher .match(userNode - .outgoingRelationShipTo(bikeNode).withType("OWNS").named("r1") + .relationshipTo(bikeNode).withType("OWNS").named("r1") .outgoingRelationShipTo(tripNode).withType("USED_ON").named("r2") .create() ) @@ -602,4 +602,63 @@ public class CypherTest { "MATCH (u:`User`) WHERE u.name = 'test' RETURN u"); } } + + @Nested + class DeleteClause { + + @Test + void shouldRenderDeleteWithoutReturn() { + + Statement statement; + statement = Cypher.match(userNode) + .detach().delete(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) DETACH DELETE u"); + + statement = Cypher.match(userNode) + .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) + .delete(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DELETE u"); + + statement = Cypher.match(userNode, bikeNode) + .delete(userNode, bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`), (b:`Bike`) DELETE u, b"); + } + + @Test + void shouldRenderDeleteWithReturn() { + + Statement statement; + statement = Cypher.match(userNode) + .detach().delete(userNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) DETACH DELETE u RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) + .detach().delete(userNode) + .returning(userNode).orderBy(userNode.property("a").ascending()).skip(2).limit(1) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DETACH DELETE u RETURN u ORDER BY u.a ASC SKIP 2 LIMIT 1"); + + } + } } From 15f7aacf3966300a87bd202119b2b45864156513 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 7 May 2019 17:38:01 +0200 Subject: [PATCH 070/342] Implement retrieval by assigned and and generated ids. --- .../data/neo4j/core/DefaultNeo4jClient.java | 15 ++++-- .../data/neo4j/core/DefaultNodeManager.java | 7 +-- .../data/neo4j/core/Neo4jClient.java | 4 ++ .../data/neo4j/core/NodeManager.java | 5 +- .../mapping/DefaultNeo4jPersistentEntity.java | 13 ++++- .../core/mapping/Neo4jMappingContext.java | 16 ++++++ .../data/neo4j/core/schema/Id.java | 11 +++- .../data/neo4j/core/schema/IdDescription.java | 34 ++++++++---- .../support/SimpleNeo4jRepository.java | 27 +++++++--- .../DefaultPersistenceContextTest.java | 4 ++ .../core/mapping/Neo4jMappingContextTest.java | 27 +++++++++- .../data/neo4j/integration/RepositoryIT.java | 18 +++++-- .../neo4j/integration/ThingRepository.java | 27 ++++++++++ .../integration/ThingWithAssignedId.java | 54 +++++++++++++++++++ .../support/SimpleNeo4jRepositoryTest.java | 14 ++--- 15 files changed, 233 insertions(+), 43 deletions(-) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingWithAssignedId.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index b177013a7..dc022a735 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -221,7 +221,7 @@ class DefaultNeo4jClient implements Neo4jClient { public Optional one() { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { - StatementResult result = statementRunner.run(cypherSupplier.get(), parameters.get()); + StatementResult result = runWith(statementRunner); return result.hasNext() ? Optional.of(mappingFunction.apply(result.single())) : Optional.empty(); } } @@ -230,7 +230,7 @@ class DefaultNeo4jClient implements Neo4jClient { public Optional first() { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { - StatementResult result = statementRunner.run(cypherSupplier.get(), parameters.get()); + StatementResult result = runWith(statementRunner); return result.stream().map(mappingFunction).findFirst(); } } @@ -239,10 +239,19 @@ class DefaultNeo4jClient implements Neo4jClient { public Collection all() { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { - StatementResult result = statementRunner.run(cypherSupplier.get(), parameters.get()); + StatementResult result = runWith(statementRunner); return result.stream().map(mappingFunction).collect(toList()); } } + + private StatementResult runWith(AutoCloseableStatementRunner statementRunner) { + String statementTemplate = cypherSupplier.get(); + if (cypherLog.isDebugEnabled()) { + cypherLog.debug(statementTemplate); + } + StatementResult result = statementRunner.run(statementTemplate, parameters.get()); + return result; + } } @AllArgsConstructor diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index 635ddda7a..d52d9bff4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -129,10 +129,11 @@ class DefaultNodeManager implements NodeManager { throw new UnsupportedOperationException("Not there yet."); } - // todo I don't like this exposure here @Override - public Schema getSchema() { - return schema; + public NodeDescription describe(Class clazz) { + return schema.getNodeDescription(clazz) + .orElseThrow( + () -> new IllegalArgumentException(String.format("%s is not a managed class", clazz.getName()))); } private class ReflectionBasedMappingFunction implements Function { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java index 23adacaa5..7c0525282 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -29,6 +29,8 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.StatementRunner; import org.neo4j.driver.summary.ResultSummary; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Definition of a modern Neo4j client. @@ -43,6 +45,8 @@ import org.neo4j.driver.summary.ResultSummary; @API(status = API.Status.STABLE, since = "1.0") public interface Neo4jClient { + Logger cypherLog = LoggerFactory.getLogger(Neo4jClient.class.getPackage().getName() + ".cypher"); + static Neo4jClient create(Driver driver) { return new DefaultNeo4jClient(driver); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java index 1c7629686..06e76f392 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -23,7 +23,7 @@ import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.driver.Transaction; -import org.springframework.data.neo4j.core.schema.Schema; +import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.lang.Nullable; /** @@ -72,6 +72,5 @@ public interface NodeManager { */ void delete(Object managedEntity); - Schema getSchema(); - + NodeDescription describe(Class clazz); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index 3efb55a79..8f77e8af6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -30,6 +30,7 @@ import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; @@ -94,7 +95,17 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity optionalIdAnnotation = Optional .ofNullable(AnnotatedElementUtils.findMergedAnnotation(idProperty.getField(), Id.class)); return optionalIdAnnotation - .map(idAnnotation -> new IdDescription(idAnnotation.strategy(), idAnnotation.generator())) + .map(idAnnotation -> { + + if (idAnnotation.strategy() == Id.Strategy.INTERNAL + && idProperty.findAnnotation(Property.class) != null) { + throw new IllegalArgumentException( + "Cannot use internal id strategy with custom property " + idProperty.getPropertyName() + + " on entity class " + this.getUnderlyingClass().getName()); + } + + return new IdDescription(idAnnotation.strategy(), idAnnotation.generator(), idProperty.getPropertyName()); + }) .orElseGet(() -> new IdDescription()); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 2e829092a..91910340e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -41,6 +41,7 @@ import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.core.schema.RelationshipDescription; import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; /** * An implementation of both a {@link Schema} as well as a Neo4j version of Spring Datas @@ -168,4 +169,19 @@ public class Neo4jMappingContext return Collections.unmodifiableCollection(relationships); } + + @Override + public void initialize() { + + // Initialize with the starter set + super.initialize(); + // And then validate everything + this.getPersistentEntities().forEach(this::validate); + } + + private void validate(Neo4jPersistentEntity entity) { + + // Make sure there is a valid id + Assert.notNull(entity.getIdDescription(), "An entity is required to describe its id property."); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java index 101987b1c..2f6008df5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java @@ -60,5 +60,14 @@ public @interface Id { Strategy strategy() default Strategy.INTERNAL; - Class generator() default IdDescription.NoopIdGenerator.class; + Class generator() default NoopIdGenerator.class; + + // Needed to make the generator attribute defaultable (null is not a constant) + class NoopIdGenerator implements IdGenerator { + + @Override + public Object generateId(Object entity) { + return null; + } + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java index 4f36f4b8e..5b2d52717 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java @@ -18,7 +18,10 @@ */ package org.springframework.data.neo4j.core.schema; +import java.util.Optional; + import org.apiguardian.api.API; +import org.springframework.lang.Nullable; /** * Description how to generate Ids for entities. @@ -28,32 +31,41 @@ import org.apiguardian.api.API; @API(status = API.Status.INTERNAL, since = "1.0") public final class IdDescription { + /** + * The selected strategy. + */ private final Id.Strategy idStrategy; - private final Class idGeneratorClass; + /** + * The class representing a generator for new ids. + */ + private @Nullable final Class idGeneratorClass; + + /** + * The property that stores the id if applicable. + */ + private @Nullable final String graphPropertyName; public IdDescription() { - this(Id.Strategy.INTERNAL, NoopIdGenerator.class); + this(Id.Strategy.INTERNAL, null, null); } - public IdDescription(Id.Strategy idStrategy, Class idGeneratorClass) { + public IdDescription(Id.Strategy idStrategy, + @Nullable Class idGeneratorClass, @Nullable String graphPropertyName) { this.idStrategy = idStrategy; this.idGeneratorClass = idGeneratorClass; + this.graphPropertyName = graphPropertyName; } public Id.Strategy getIdStrategy() { return idStrategy; } - public Class getIdGeneratorClass() { - return idGeneratorClass; + public Optional> getIdGeneratorClass() { + return Optional.ofNullable(idGeneratorClass); } - static class NoopIdGenerator implements IdGenerator { - - @Override - public Object generateId(Object entity) { - return null; - } + public Optional getGraphPropertyName() { + return Optional.ofNullable(graphPropertyName); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 33b35652a..a33ccea20 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -35,6 +35,7 @@ import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Node; import org.springframework.data.neo4j.core.cypher.SortItem; @@ -43,6 +44,7 @@ import org.springframework.data.neo4j.core.cypher.StatementBuilder; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; +import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.support.PageableExecutionUtils; @@ -68,15 +70,28 @@ class SimpleNeo4jRepository implements Neo4jRepository { private final NodeDescription nodeDescription; private Node node; + private Expression idExpression; SimpleNeo4jRepository(NodeManager nodeManager, Class nodeClass) { this.nodeManager = nodeManager; this.nodeClass = nodeClass; - this.nodeDescription = nodeManager.getSchema().getNodeDescription(nodeClass) - .orElseThrow(() -> new IllegalArgumentException("unsupported class")); - + this.nodeDescription = nodeManager.describe(nodeClass); this.node = Cypher.node(nodeDescription.getPrimaryLabel()).named("n"); + + IdDescription idDescription = this.nodeDescription.getIdDescription(); + switch (idDescription.getIdStrategy()) { + case INTERNAL: + idExpression = node.internalId(); + break; + case ASSIGNED: + case GENERATED: + idExpression = idDescription.getGraphPropertyName() + .map(this.node::property).get(); + break; + default: + throw new IllegalStateException("Unsupported ID strategy: %s" + idDescription.getIdStrategy()); + } } @Override @@ -139,8 +154,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Optional findById(ID id) { - // todo only works for internal ids right now - Statement statement = match(node).where(node.internalId().isEqualTo(literalOf((Number) id))).returning(node) + Statement statement = match(node).where(idExpression.isEqualTo(literalOf(id))).returning(node) .build(); return nodeManager.executeTypedQueryForObject(renderer.render(statement), nodeClass); } @@ -159,8 +173,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Iterable findAllById(Iterable ids) { - // todo only works for internal ids right now - Statement statement = match(node).where(node.internalId().isIn((Iterable) ids)).returning(node) + Statement statement = match(node).where(idExpression.isIn((Iterable) ids)).returning(node) .build(); return nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java index 71f987f7a..a64842353 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.schema.Id; /** * @author Gerrit Meier @@ -113,6 +114,9 @@ class DefaultPersistenceContextTest { class Something { + @Id + private Long id; + String value; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index 4a5d41df1..1f4834827 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -44,7 +44,7 @@ public class Neo4jMappingContextTest { void initializationOfSchemaShouldWork() { Neo4jMappingContext schema = new Neo4jMappingContext(); - schema.setInitialEntitySet(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class))); + schema.setInitialEntitySet(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class, TripNode.class))); schema.initialize(); Optional> optionalUserNodeDescription = schema.getNodeDescription("User"); @@ -84,6 +84,17 @@ public class Neo4jMappingContextTest { }); } + + @Test + void shouldPreventIllegalIdAnnotations() { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + schema.setInitialEntitySet(new HashSet<>(Arrays.asList(InvalidId.class))); + assertThatIllegalArgumentException() + .isThrownBy(() -> schema.initialize()) + .withMessage("Cannot use internal id strategy with custom property poef on entity class org.springframework.data.neo4j.core.mapping.Neo4jMappingContextTest$InvalidId"); + } + @Node("User") static class UserNode { @@ -108,4 +119,18 @@ public class Neo4jMappingContextTest { List renter; } + + static class TripNode { + + @Id + private String id; + + String name; + } + + static class InvalidId { + + @Id @Property("poef") + private String id; + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index d34a45648..f2c35e34f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -62,16 +62,17 @@ class RepositoryIT { private static Neo4jContainer neo4jContainer = new Neo4jContainer().withoutAuthentication(); private final PersonRepository repository; + private final ThingRepository thingRepository; private final Driver driver; private Long id1; private Long id2; private PersonWithAllConstructor person1; private PersonWithAllConstructor person2; - @Autowired - RepositoryIT(PersonRepository repository, Driver driver) { + @Autowired RepositoryIT(PersonRepository repository, ThingRepository thingRepository, Driver driver) { this.repository = repository; + this.thingRepository = thingRepository; this.driver = driver; } @@ -86,6 +87,7 @@ class RepositoryIT { .next().get(0).asLong(); transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'"); + transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})"); transaction.success(); transaction.close(); @@ -120,6 +122,14 @@ class RepositoryIT { assertThat(persons).hasSize(2); } + @Test + void findByAssignedId() { + Optional optionalThing = thingRepository.findById("anId"); + assertThat(optionalThing).isPresent(); + assertThat(optionalThing).map(ThingWithAssignedId::getTheId).contains("anId"); + assertThat(optionalThing).map(ThingWithAssignedId::getName).contains("Homer"); + } + @Test void count() { assertThat(repository.count()).isEqualTo(2); @@ -290,14 +300,14 @@ class RepositoryIT { public Driver driver() { String boltUrl = neo4jContainer.getBoltUrl(); - return GraphDatabase.driver(boltUrl, AuthTokens.basic("neo4j", "secret")); + return GraphDatabase.driver(boltUrl, AuthTokens.none()); } @Bean public NodeManagerFactory nodeManagerFactory(Driver driver) { return new NodeManagerFactory(driver, PersonWithAllConstructor.class, PersonWithNoConstructor.class, - PersonWithWither.class); + PersonWithWither.class, ThingWithAssignedId.class); } @Bean diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java new file mode 100644 index 000000000..76496dec0 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Michael J. Simons + */ +public interface ThingRepository extends CrudRepository { +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingWithAssignedId.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingWithAssignedId.java new file mode 100644 index 000000000..4bfca4085 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingWithAssignedId.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration; + +import static org.springframework.data.neo4j.core.schema.Id.Strategy.*; + +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +/** + * Has an assigned id and a changeable name. + * + * @author Michael J. Simons + */ +@Node("Thing") +public class ThingWithAssignedId { + + @Id(strategy = ASSIGNED) + private final String theId; + + private String name; + + public ThingWithAssignedId(String theId) { + this.theId = theId; + } + + public String getTheId() { + return theId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java index dbf3e2af2..9f530b617 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java @@ -22,25 +22,23 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Collections; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.Schema; /** * @author Gerrit Meier - **/ + * @author Michael J. Simons + */ class SimpleNeo4jRepositoryTest { private NodeManager nodeManager; - private Schema schema; - private NodeDescription nodeDescription; private SimpleNeo4jRepository repository; @@ -49,12 +47,10 @@ class SimpleNeo4jRepositoryTest { void setupMock() { nodeManager = mock(NodeManager.class); - schema = mock(Schema.class); - when(nodeManager.getSchema()).thenReturn(schema); - nodeDescription = mock(NodeDescription.class); when(nodeDescription.getPrimaryLabel()).thenReturn("TestNode"); - when(schema.getNodeDescription(TestNode.class)).thenReturn(Optional.of(nodeDescription)); + when(nodeDescription.getIdDescription()).thenReturn(new IdDescription()); + when(nodeManager.describe(TestNode.class)).thenReturn(nodeDescription); repository = new SimpleNeo4jRepository(this.nodeManager, TestNode.class); } From 7417f960847fda2f586475f8a03dd08198832539 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 8 May 2019 12:07:28 +0200 Subject: [PATCH 071/342] Add a simple Neo4j JUnit 5 extension. This extension allows easy selecting of test containers or local instances. --- .../data/neo4j/integration/RepositoryIT.java | 15 +-- .../data/neo4j/test/Neo4jExtension.java | 124 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 5 + 3 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index f2c35e34f..967eaaed0 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -27,9 +27,7 @@ import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; -import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Transaction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -42,24 +40,22 @@ import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; +import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.testcontainers.containers.Neo4jContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; @ExtendWith(SpringExtension.class) +@ExtendWith(Neo4jExtension.class) @ContextConfiguration(classes = RepositoryIT.Config.class) -@Testcontainers class RepositoryIT { private static final String TEST_PERSON1_NAME = "Test"; private static final String TEST_PERSON2_NAME = "Test2"; private static final String TEST_PERSON_SAMEVALUE = "SameValue"; - @Container - private static Neo4jContainer neo4jContainer = new Neo4jContainer().withoutAuthentication(); + private static Neo4jConnectionSupport neo4jConnectionSupport; private final PersonRepository repository; private final ThingRepository thingRepository; @@ -299,8 +295,7 @@ class RepositoryIT { @Bean public Driver driver() { - String boltUrl = neo4jContainer.getBoltUrl(); - return GraphDatabase.driver(boltUrl, AuthTokens.none()); + return neo4jConnectionSupport.openConnection(); } @Bean diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java new file mode 100644 index 000000000..0789a220f --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.test; + +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.ReflectionUtils; +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Logging; +import org.testcontainers.containers.Neo4jContainer; + +/** + * This extension is for internal use only. It is meant to speed up development and keep test containers for normal build. + * When both {@code SDN_RX_NEO4J_URL} and {@code SDN_RX_NEO4J_PASSWORD} are set as environment variables, the extension + * will inject a field of type {@link Neo4jConnectionSupport} into the extended test with a connection to that instance, + * otherwise it will start a test container and use that connection. + * + * @author Michael J. Simons + */ +@Slf4j +public class Neo4jExtension implements BeforeAllCallback { + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(Neo4jExtension.class); + + private static final String KEY = "neo4j.standalone"; + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + List injectableFields = ReflectionUtils + .findFields(context.getRequiredTestClass(), + field -> ReflectionUtils.isStatic(field) && field.getType() == Neo4jConnectionSupport.class, + ReflectionUtils.HierarchyTraversalMode.BOTTOM_UP); + + if (injectableFields.size() != 1) { + return; + } + + String neo4jUrl = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_URL")).orElse(""); + String neo4jPassword = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_PASSWORD")).orElse(""); + + Neo4jConnectionSupport neo4jConnectionSupport; + if (!(neo4jUrl.isEmpty() || neo4jPassword.isEmpty())) { + log.warn("Using Neo4j instance at {}.", neo4jUrl); + neo4jConnectionSupport = new Neo4jConnectionSupport(neo4jUrl, + AuthTokens.basic("neo4j", neo4jPassword)); + } else { + log.warn("Using Neo4j test container."); + ExtensionContext.Store store = context.getStore(NAMESPACE); + ContainerAdapter adapter = store + .getOrComputeIfAbsent(KEY, key -> new Neo4jExtension.ContainerAdapter(), ContainerAdapter.class); + adapter.start(); + neo4jConnectionSupport = new Neo4jConnectionSupport(adapter.getBoltUrl(), AuthTokens.none()); + } + + Field field = injectableFields.get(0); + field.setAccessible(true); + field.set(null, neo4jConnectionSupport); + } + + public static class Neo4jConnectionSupport { + + public final String url; + + public final AuthToken authToken; + + public final Config config; + + public Neo4jConnectionSupport(String url, AuthToken authToken) { + this.url = url; + this.authToken = authToken; + this.config = Config.builder().withLogging(Logging.slf4j()).build(); + } + + public Driver openConnection() { + return GraphDatabase.driver(url, authToken, config); + } + } + + static class ContainerAdapter implements ExtensionContext.Store.CloseableResource { + + private final Neo4jContainer neo4jContainer = new Neo4jContainer<>() + .withoutAuthentication(); + + public String getBoltUrl() { + return neo4jContainer.getBoltUrl(); + } + + public void start() { + neo4jContainer.start(); + } + + @Override + public void close() { + this.neo4jContainer.close(); + } + } +} diff --git a/spring-data-neo4j/src/test/resources/logback-test.xml b/spring-data-neo4j/src/test/resources/logback-test.xml index 69376ad24..3f187a068 100644 --- a/spring-data-neo4j/src/test/resources/logback-test.xml +++ b/spring-data-neo4j/src/test/resources/logback-test.xml @@ -26,7 +26,12 @@ + + + + + From c522c05e71c4a8c4b86ed6465b941da469ae4a93 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 8 May 2019 16:33:40 +0200 Subject: [PATCH 072/342] Add support for renaming properties. --- .../data/neo4j/core/DefaultNeo4jClient.java | 2 +- .../data/neo4j/core/DefaultNodeManager.java | 122 +++-------------- .../core/DefaultReactiveNeo4jClient.java | 4 +- ...elegatingMappingFunctionWithNullCheck.java | 51 +++++++ .../data/neo4j/core/NamedParameters.java | 1 + .../data/neo4j/core/ReactiveNeo4jClient.java | 2 +- .../mapping/DefaultNeo4jMappingFunction.java | 126 ++++++++++++++++++ .../mapping/DefaultNeo4jPersistentEntity.java | 13 ++ .../core/mapping/Neo4jMappingContext.java | 42 +++--- .../core/schema/GraphPropertyDescription.java | 3 +- .../data/neo4j/core/schema/Id.java | 15 ++- .../data/neo4j/core/schema/IdDescription.java | 1 + .../data/neo4j/core/schema/IdGenerator.java | 3 +- .../neo4j/core/schema/Neo4jSimpleTypes.java | 1 + .../data/neo4j/core/schema/Node.java | 6 +- .../neo4j/core/schema/NodeDescription.java | 10 ++ .../data/neo4j/core/schema/Property.java | 8 ++ .../data/neo4j/core/schema/Relationship.java | 10 ++ .../core/schema/RelationshipDescription.java | 1 + .../data/neo4j/core/schema/Schema.java | 19 +++ ...tingMappingFunctionWithNullCheckTest.java} | 26 ++-- .../data/neo4j/core/Neo4jClientTest.java | 28 ++++ .../neo4j/core/ReactiveNeo4jClientTest.java | 46 ++++++- .../DefaultNeo4jMappingFunctionTest.java | 54 ++++++++ .../core/mapping/Neo4jMappingContextTest.java | 11 +- .../integration/PersonWithAllConstructor.java | 8 +- .../integration/PersonWithNoConstructor.java | 4 + .../data/neo4j/integration/RepositoryIT.java | 48 +++++-- 28 files changed, 501 insertions(+), 164 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/{mapping/DefaultNeo4JPersistentEntityTest.java => DelegatingMappingFunctionWithNullCheckTest.java} (52%) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunctionTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index dc022a735..00fdbd23b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -213,7 +213,7 @@ class DefaultNeo4jClient implements Neo4jClient { public RecordFetchSpec, Collection, T> mappedBy( @SuppressWarnings("HiddenField") Function mappingFunction) { - this.mappingFunction = mappingFunction; + this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction); return this; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index d52d9bff4..da864956e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -20,21 +20,12 @@ package org.springframework.data.neo4j.core; import java.util.Collection; import java.util.Optional; -import java.util.function.Function; import org.apiguardian.api.API; -import org.neo4j.driver.Record; import org.neo4j.driver.Transaction; -import org.neo4j.driver.Value; -import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.SimplePropertyHandler; -import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; import org.springframework.data.neo4j.core.context.DefaultPersistenceContext; import org.springframework.data.neo4j.core.context.PersistenceContext; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.lang.Nullable; @@ -42,6 +33,7 @@ import org.springframework.lang.Nullable; /** * @author Michael J. Simons * @author Gerrit Meier + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") class DefaultNodeManager implements NodeManager { @@ -77,39 +69,22 @@ class DefaultNodeManager implements NodeManager { @Override public Optional executeTypedQueryForObject(String query, Class returnedType) { - Optional> nodeDescriptionOptional = schema.getNodeDescription(returnedType); - // returned type can be sth. like a raw result or scalar - if (!nodeDescriptionOptional.isPresent()) { - return getTypedMappingSpec(query, returnedType).one(); - } - return getTypedFetchSpec(query, returnedType, (NodeDescription) nodeDescriptionOptional.get()).one(); + MappingSpec, Collection, T> mappingSpec = neo4jClient.newQuery(query).fetchAs(returnedType); + return schema.getMappingFunctionFor(returnedType) + .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) + .orElseGet(() -> mappingSpec) + .one(); } @Override public Collection executeTypedQueryForObjects(String query, Class returnedType) { - Optional> nodeDescriptionOptional = schema.getNodeDescription(returnedType); - // returned type can be sth. like a raw result or scalar - if (!nodeDescriptionOptional.isPresent()) { - return getTypedMappingSpec(query, returnedType).all(); - } - - return getTypedFetchSpec(query, returnedType, (NodeDescription) nodeDescriptionOptional.get()).all(); - } - - private Neo4jClient.RecordFetchSpec, Collection, T> getTypedFetchSpec(String query, - Class returnedType, NodeDescription nodeDescription) { - - Function mappingFunction = new ReflectionBasedMappingFunction(nodeDescription); - - return getTypedMappingSpec(query, returnedType).mappedBy(mappingFunction); - } - - private Neo4jClient.MappingSpec, Collection, T> getTypedMappingSpec(String query, - Class returnedType) { - - return neo4jClient.newQuery(query).fetchAs(returnedType); + MappingSpec, Collection, T> mappingSpec = neo4jClient.newQuery(query).fetchAs(returnedType); + return schema.getMappingFunctionFor(returnedType) + .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) + .orElseGet(() -> mappingSpec) + .all(); } @Override @@ -131,75 +106,10 @@ class DefaultNodeManager implements NodeManager { @Override public NodeDescription describe(Class clazz) { + + // // @formatter:off return schema.getNodeDescription(clazz) - .orElseThrow( - () -> new IllegalArgumentException(String.format("%s is not a managed class", clazz.getName()))); + .orElseThrow(() -> new IllegalArgumentException(String.format("%s is not a managed class", clazz.getName()))); + // @formatter:on } - - private class ReflectionBasedMappingFunction implements Function { - - private final NodeDescription nodeDescription; - - private ReflectionBasedMappingFunction(NodeDescription nodeDescription) { - this.nodeDescription = nodeDescription; - } - - @Override - public T apply(Record record) { - - try { - - for (Value value : record.values()) { - // todo - // 1. determine type of returned value: node or relationship - // 2. find most fitting label value.asNode().labels().forEach(); - // strictly assume just to work with nodes for now and every matching type label is the root node - if (value.asNode().hasLabel(nodeDescription.getPrimaryLabel())) { - - if (!(nodeDescription instanceof Neo4jPersistentEntity)) { - return null; - } - - Neo4jPersistentEntity entity = (Neo4jPersistentEntity) nodeDescription; - PreferredConstructor persistenceConstructor = entity - .getPersistenceConstructor(); - - // todo check because there was "something" wrong with the usage of EntityInstantiators in SDN - EntityInstantiators instantiators = new EntityInstantiators(); - - // todo: neo4jPersistentEntity.requiresPropertyPopulation() - - T instance = instantiators.getInstantiatorFor(entity).createInstance(entity, - new ParameterValueProvider() { - @Override - public Object getParameterValue(PreferredConstructor.Parameter parameter) { - return value.get(parameter.getName()).asObject(); - } - }); - - PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(instance); - if (entity.requiresPropertyPopulation()) { - entity.doWithProperties((SimplePropertyHandler) property -> { - if (persistenceConstructor.isConstructorParameter(property)) { - return; - } - - propertyAccessor.setProperty(property, value.get(property.getName()).asObject()); - - }); - instance = propertyAccessor.getBean(); - } - - return instance; - } - } - - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - } - } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 105037d79..61f11e8a9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -48,8 +48,8 @@ import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; * TODO Reactive transaction management is pretty obvious still open * * @author Michael J. Simons - * @since 1.0 * @soundtrack Die Toten Hosen - Im Auftrag des Herrn + * @since 1.0 */ @Slf4j class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { @@ -202,7 +202,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { @Override public RecordFetchSpec, Flux, T> mappedBy(Function mappingFunction) { - this.mappingFunction = mappingFunction; + this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction); return this; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java new file mode 100644 index 000000000..289a6bba9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import java.util.function.Function; + +import org.neo4j.driver.Record; + +/** + * A delegating mapping function that first calls the delegate to get a record map and than checks the returned + * value for {@literal null} and in the case of a null value, an {@link IllegalStateException} will be thrown. + * + * @param The expected type of this function + * @author Michael J. Simons + * @soundtrack Manowar - Fighting The World + * @since 1.0 + */ +class DelegatingMappingFunctionWithNullCheck implements Function { + + Function delegate; + + DelegatingMappingFunctionWithNullCheck(Function delegate) { + this.delegate = delegate; + } + + @Override + public T apply(Record record) { + T t = delegate.apply(record); + if (t == null) { + throw new IllegalStateException( + "Mapping function " + delegate + " returned illegal null value for record " + record); + } + return t; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java index 5cdfe73bd..ea4fd795f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java @@ -27,6 +27,7 @@ import org.apiguardian.api.API; /** * @author Michael J. Simons * @soundtrack Bananafishbones - Viva Conputa + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") final class NamedParameters { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java index b48581a2c..8bdf0a6e9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java @@ -38,8 +38,8 @@ import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; * operations will only be executed once something subscribes to the reactive sequence defined. * * @author Michael J. Simons - * @since 1.0 * @soundtrack Die Toten Hosen - Im Auftrag des Herrn + * @since 1.0 */ @API(status = API.Status.STABLE, since = "1.0") public interface ReactiveNeo4jClient { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java new file mode 100644 index 000000000..2b5d0e714 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.mapping; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Function; + +import org.neo4j.driver.Record; +import org.neo4j.driver.Value; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; +import org.springframework.data.neo4j.core.schema.NodeDescription; + +/** + * The central logic of mapping Neo4j's {@link org.neo4j.driver.Record records} to entities based on the Spring + * implementation of the {@link org.springframework.data.neo4j.core.schema.Schema}, + * represented by the {@link Neo4jMappingContext}. + * + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +@RequiredArgsConstructor +@Slf4j +class DefaultNeo4jMappingFunction implements Function { + + private final EntityInstantiators instantiators; + + private final Neo4jPersistentEntity nodeDescription; + + @Override + public T apply(Record record) { + + try { + + for (Value value : record.values()) { + // todo + // 1. determine type of returned value: node or relationship + // 2. find most fitting label value.asNode().labels().forEach(); + // strictly assume just to work with nodes for now and every matching type label is the root node + if (value.asNode().hasLabel(nodeDescription.getPrimaryLabel())) { + + PreferredConstructor persistenceConstructor = nodeDescription + .getPersistenceConstructor(); + + // todo: neo4jPersistentEntity.requiresPropertyPopulation() + + T instance = instantiators.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, + new ParameterValueProvider() { + @Override + public Object getParameterValue(PreferredConstructor.Parameter parameter) { + + String graphPropertyName = getGraphPropertyNameFor(parameter); + return getValueFor(graphPropertyName, value); + } + }); + + PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(instance); + if (nodeDescription.requiresPropertyPopulation()) { + nodeDescription.doWithProperties((Neo4jPersistentProperty property) -> { + if (persistenceConstructor.isConstructorParameter(property)) { + return; + } + + String graphPropertyName = property.getPropertyName(); + propertyAccessor.setProperty(property, getValueFor(graphPropertyName, value)); + }); + instance = propertyAccessor.getBean(); + } + + return instance; + } + } + } catch (Exception e) { + throw new MappingException("Error mapping " + record.toString(), e); + } + + log.warn("Could not find mappable nodes or relationships inside {} for {}", record, nodeDescription); + return null; + } + + /** + * Extracts the name of the graph property for a field and also for a constructor parameter. The {@link NodeDescription} should + * contain property descriptions for all fields, but may not contain a description for a constructor parameter if the parameter + * has a different name than the field. In this case the name itself is returned. + *

+ * Otherwise the name of the property in the graph will be returned. + * + * @param parameter The constructor parameter for which the property name is needed + * @return The name of the graph property or the name of {@code parameter} if a corresponding property was not found + */ + String getGraphPropertyNameFor(PreferredConstructor.Parameter parameter) { + return nodeDescription + .getGraphProperty(parameter.getName()) + .map(GraphPropertyDescription::getPropertyName) + .orElse(parameter.getName()); + } + + Object getValueFor(String graphProperty, Value from) { + + // TODO conversion, Type system + return from.get(graphProperty).asObject(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index 8f77e8af6..364de703f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -33,6 +33,7 @@ import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Property; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; /** * @author Michael J. Simons @@ -79,6 +80,18 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity getGraphProperty(String fieldName) { + return Optional.ofNullable(this.getPersistentProperty(fieldName)); + } + + @Override + public void verify() { + + super.verify(); + Assert.notNull(this.getIdDescription(), "An entity is required to describe its id property."); + } + String computePrimaryLabel() { Node nodeAnnotation = this.findAnnotation(Node.class); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 91910340e..79de56378 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -29,8 +29,11 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; import java.util.function.Predicate; +import org.neo4j.driver.Record; +import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.context.AbstractMappingContext; @@ -41,7 +44,6 @@ import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.core.schema.RelationshipDescription; import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.data.util.TypeInformation; -import org.springframework.util.Assert; /** * An implementation of both a {@link Schema} as well as a Neo4j version of Spring Datas @@ -53,6 +55,13 @@ import org.springframework.util.Assert; public class Neo4jMappingContext extends AbstractMappingContext, Neo4jPersistentProperty> implements Schema { + /** + * The shared entity instantiators of this context. Those should not be recreated for each entity or even not for + * each query, as otherwise the cache of Spring's org.springframework.data.convert.ClassGeneratingEntityInstantiator + * won't apply + */ + private final EntityInstantiators instantiators = new EntityInstantiators(); + /** * A lookup of entities based on their primary label. We depend on the locking mechanism provided by the * {@link AbstractMappingContext}, so this lookup is not synchronized further. @@ -141,10 +150,22 @@ public class Neo4jMappingContext @Override public Collection getRelationshipsOf(String primaryLabel) { - return this.relationshipsByPrimaryLabel.computeIfAbsent(primaryLabel, this::thing); + return this.relationshipsByPrimaryLabel.computeIfAbsent(primaryLabel, this::computeRelationshipsOf); } - Collection thing(String primaryLabel) { + @Override + public Optional> getMappingFunctionFor(Class targetClass) { + + if (!this.hasPersistentEntityFor(targetClass)) { + return Optional.empty(); + } + + return this.getNodeDescription(targetClass) + .map(Neo4jPersistentEntity.class::cast) + .map(neo4jPersistentEntity -> new DefaultNeo4jMappingFunction<>(instantiators, neo4jPersistentEntity)); + } + + private Collection computeRelationshipsOf(String primaryLabel) { NodeDescription nodeDescription = getRequiredNodeDescription(primaryLabel); @@ -169,19 +190,4 @@ public class Neo4jMappingContext return Collections.unmodifiableCollection(relationships); } - - @Override - public void initialize() { - - // Initialize with the starter set - super.initialize(); - // And then validate everything - this.getPersistentEntities().forEach(this::validate); - } - - private void validate(Neo4jPersistentEntity entity) { - - // Make sure there is a valid id - Assert.notNull(entity.getIdDescription(), "An entity is required to describe its id property."); - } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java index c6544fc06..7ef0354c5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java @@ -23,12 +23,13 @@ import org.apiguardian.api.API; /** * Provides minimal information how to map class attributes to the properties of a node or a relationship. *

- * Spring Datas persistent properties have slightly different semantics. They have an entity centric approach of properties. + * Spring Data's persistent properties have slightly different semantics. They have an entity centric approach of properties. * Spring Data properties contain - if not marked otherwise - also associations. *

* Associations between different node types can be queried on the {@link Schema} itself. * * @author Michael J. Simons + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public interface GraphPropertyDescription { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java index 2f6008df5..0f31626ef 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java @@ -31,6 +31,7 @@ import org.apiguardian.api.API; * Annotation to configure assigment of ids. * * @author Michael J. Simons + * @since 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @@ -40,6 +41,9 @@ import org.apiguardian.api.API; @API(status = API.Status.STABLE, since = "1.0") public @interface Id { + /** + * Enumerating available strategies for providing ids for new entities. + */ enum Strategy { /** @@ -53,13 +57,22 @@ public @interface Id { ASSIGNED, /** - * Use generated values, generator class is required. + * Use generated values, a generator class is required. */ GENERATED } + /** + * Configure the strategy for generating ids. Some strategies require a {@link #generator()}. + * + * @return The strategy applied for generating ids for new entities. Defaults to use Neo4j internal database identifiers. + */ Strategy strategy() default Strategy.INTERNAL; + /** + * + * @return The generator that fits the selected {@link #strategy()}. Defaults to a Noop generator. + */ Class generator() default NoopIdGenerator.class; // Needed to make the generator attribute defaultable (null is not a constant) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java index 5b2d52717..1234aa1ca 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java @@ -27,6 +27,7 @@ import org.springframework.lang.Nullable; * Description how to generate Ids for entities. * * @author Michael J. Simons + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public final class IdDescription { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java index 521567bfc..bb34dfc6f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java @@ -24,13 +24,14 @@ import org.apiguardian.api.API; * Interface for generating ids for entities. * * @author Michael J. Simons + * @since 1.0 */ @FunctionalInterface @API(status = API.Status.STABLE, since = "1.0") public interface IdGenerator { /** - * Generates a new id for given entity + * Generates a new id for given entity. * * @param entity the entity to be saved * @return id to be assigned to the entity diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java index 01298cd7b..f67568794 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java @@ -28,6 +28,7 @@ import org.neo4j.driver.Value; * established for attributes of a node that are simple types. * * @author Michael J. Simons + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public final class Neo4jSimpleTypes { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Node.java index 065e05b3e..03acf3572 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Node.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Node.java @@ -32,6 +32,7 @@ import org.springframework.core.annotation.AliasFor; * The annotation to configure the mapping from a node with a given set of labels to a class and vice versa. * * @author Michael J. Simons + * @since 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @@ -40,11 +41,14 @@ import org.springframework.core.annotation.AliasFor; @API(status = API.Status.STABLE, since = "1.0") public @interface Node { + /** + * @return See {@link #labels()}. + */ @AliasFor("labels") String[] value() default {}; /** - * The labels to identify a node with that is supposed to be mapped to the class annotated with {@link Node @Node}. + * @return The labels to identify a node with that is supposed to be mapped to the class annotated with {@link Node @Node}. */ @AliasFor("value") String[] labels() default {}; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java index 6db8bb213..090676a5f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core.schema; import java.util.Collection; +import java.util.Optional; import org.apiguardian.api.API; @@ -28,6 +29,7 @@ import org.apiguardian.api.API; * * @param The type of the underlying class * @author Michael J. Simons + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public interface NodeDescription { @@ -51,4 +53,12 @@ public interface NodeDescription { * @return A collection of persistent properties that are mapped to graph properties and not to relationships */ Collection getGraphProperties(); + + /** + * Retrieves a {@link GraphPropertyDescription} by its field name. + * + * @param fieldName The field name for which the graph property description should be retrieved + * @return An empty optional if there is no property known for the given field. + */ + Optional getGraphProperty(String fieldName); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java index 143898aab..bdb4d5a7d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java @@ -32,6 +32,7 @@ import org.springframework.core.annotation.AliasFor; * The annotation to configure the mapping from a property to an attribute and vice versa. * * @author Michael J. Simons + * @since 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @@ -40,9 +41,16 @@ import org.springframework.core.annotation.AliasFor; @API(status = API.Status.STABLE, since = "1.0") public @interface Property { + /** + * @return See {@link #name()}. + */ @AliasFor("name") String value() default ""; + /** + * + * @return The name of the property in the graph. + */ @AliasFor("value") String name() default ""; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java index df0bfa531..9a5be837c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java @@ -32,6 +32,7 @@ import org.springframework.core.annotation.AliasFor; * Annotation to configure mappings of relationship. * * @author Michael J. Simons + * @since 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @@ -40,6 +41,9 @@ import org.springframework.core.annotation.AliasFor; @API(status = API.Status.STABLE, since = "1.0") public @interface Relationship { + /** + * Enumeration of the direction a relationship can take. + */ enum Direction { /** @@ -58,6 +62,9 @@ public @interface Relationship { UNDIRECTED } + /** + * @return See {@link #type()}. + */ @AliasFor("type") String value() default ""; @@ -67,6 +74,9 @@ public @interface Relationship { @AliasFor("value") String type() default ""; + /** + * @return The name of the attribute containing the other end of the relationship. + */ String inverse() default ""; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java index c78b16373..87313420c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java @@ -26,6 +26,7 @@ import org.apiguardian.api.API; * {@link #getTarget}. * * @author Michael J. Simons + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public interface RelationshipDescription { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java index 793fff766..6eb2498d2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -21,13 +21,16 @@ package org.springframework.data.neo4j.core.schema; import java.util.Collection; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import org.apiguardian.api.API; +import org.neo4j.driver.Record; /** * Contains the descriptions of all nodes, their properties and relationships known to SDN-RX. * * @author Michael J. Simons + * @since 1.0 */ @API(status = API.Status.STABLE, since = "1.0") public interface Schema { @@ -62,4 +65,20 @@ public interface Schema { * @return The relationships defined by instances of this node. */ Collection getRelationshipsOf(String primaryLabel); + + /** + * Retrieves a schema based mapping function for the {@code targetClass}. The mapping function will expect a + * record containing all the nodes and relationships necessary to fully populate an instance of the given class. + * It will not try to fetch data from any other records or queries. The mapping function is free to throw a {@link RuntimeException}, + * most likely a {@code org.springframework.data.mapping.MappingException} or {@link IllegalStateException} when + * mapping is not possible. + *

+ * In case the mapping function returns a {@literal null}, the Neo4j client will throw an exception and prevent further + * processing. + * + * @param targetClass The target class to which to map to. + * @param Type of the target class + * @return An emtpy optional if the target class is unknown, otherwise an optional containing a stateless, reusable mapping function + */ + Optional> getMappingFunctionFor(Class targetClass); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4JPersistentEntityTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java similarity index 52% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4JPersistentEntityTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java index 00eac1f98..2d56c92d5 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4JPersistentEntityTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java @@ -16,29 +16,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.springframework.data.neo4j.core; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import org.junit.jupiter.api.Test; -import org.springframework.data.neo4j.core.schema.Node; +import org.neo4j.driver.Record; /** * @author Michael J. Simons */ -public class DefaultNeo4JPersistentEntityTest { - - private Neo4jMappingContext mappingContext = new Neo4jMappingContext(); +class DelegatingMappingFunctionWithNullCheckTest { @Test - void shouldDiscoverAnnotatedPrimaryLabel() { - - Neo4jPersistentEntity entity = mappingContext.getPersistentEntity(DummySubEntity.class); - - assertThat(entity).isNotNull(); + void shouldBeHappyWithNonNullValues() { + DelegatingMappingFunctionWithNullCheck function = new DelegatingMappingFunctionWithNullCheck( + record -> "Tada."); + assertThat(function.apply(mock(Record.class))).isEqualTo("Tada."); } - @Node("DummySubNode") - static class DummySubEntity { + @Test + void shouldThrowExceptions() { + DelegatingMappingFunctionWithNullCheck function = new DelegatingMappingFunctionWithNullCheck( + record -> null); + assertThatIllegalStateException().isThrownBy(() -> function.apply(mock(Record.class))) + .withMessageMatching("Mapping function .* returned illegal null value for record .*"); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java index ed74b104c..2daa512a5 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java @@ -246,6 +246,34 @@ class Neo4jClientTest { verify(session).close(); } + @Test + void shouldApplyNullChecksDuringReading() { + + when(session.run(anyString(), anyMap())).thenReturn(statementResult); + when(statementResult.stream()).thenReturn(Stream.of(record1, record2)); + when(record1.get("name")).thenReturn(Values.value("michael")); + + Neo4jClient client = Neo4jClient.create(driver); + + assertThatIllegalStateException().isThrownBy(() -> client + .newQuery("MATCH (n) RETURN n") + .fetchAs(BikeOwner.class).mappedBy(r -> { + if (r == record1) { + return new BikeOwner(r.get("name").asString(), Collections.emptyList()); + } else { + return null; + } + }) + .all()); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + verify(session).run(eq("MATCH (n) RETURN n"), argThat(new MapAssertionMatcher(Collections.emptyMap()))); + verify(statementResult).stream(); + verify(record1).get("name"); + verify(session).close(); + } + @Test void writing() { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java index 5daf07564..54b7fd582 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java @@ -29,6 +29,7 @@ import reactor.test.StepVerifier; import java.time.LocalDate; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; @@ -100,8 +101,6 @@ class ReactiveNeo4jClientTest { when(driver.rxSession(any(Consumer.class))).thenReturn(session); when(session.beginTransaction()).thenReturn(Mono.just(transaction)); - when(transaction.commit()).thenReturn(Mono.empty()); - when(session.close()).thenReturn(Mono.empty()); } @@ -116,6 +115,7 @@ class ReactiveNeo4jClientTest { void queryCreationShouldFeelGood() { when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -161,6 +161,7 @@ class ReactiveNeo4jClientTest { void databaseSelectionShouldBePossibleOnlyOnce() { when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -196,6 +197,8 @@ class ReactiveNeo4jClientTest { @Test void withDefaultDatabase() { + when(transaction.commit()).thenReturn(Mono.empty()); + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); Mono result = client .delegateTo(runner -> Mono.just(21)) @@ -214,6 +217,8 @@ class ReactiveNeo4jClientTest { @Test void withDatabase() { + when(transaction.commit()).thenReturn(Mono.empty()); + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); Mono result = client .delegateTo(runner -> Mono.just(21)) @@ -239,6 +244,7 @@ class ReactiveNeo4jClientTest { void reading() { when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1)); when(record1.get("name")).thenReturn(Values.value("michael")); @@ -270,10 +276,44 @@ class ReactiveNeo4jClientTest { verify(session).close(); } + @Test + void shouldApplyNullChecksDuringReading() { + + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.rollback()).thenReturn(Mono.empty()); + when(statementResult.records()).thenReturn(Flux.just(record1, record2)); + when(record1.get("name")).thenReturn(Values.value("michael")); + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + Flux bikeOwners = client + .newQuery("MATCH (n) RETURN n") + .fetchAs(BikeOwner.class).mappedBy(r -> { + if (r == record1) { + return new BikeOwner(r.get("name").asString(), Collections.emptyList()); + } else { + return null; + } + }) + .all(); + + StepVerifier.create(bikeOwners) + .expectNextCount(1) + .verifyError(); + + verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + + verify(transaction).run(eq("MATCH (n) RETURN n"), argThat(new MapAssertionMatcher(Collections.emptyMap()))); + verify(statementResult).records(); + verify(record1).get("name"); + verify(transaction).rollback(); + verify(session).close(); + } + @Test void writing() { when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.summary()).thenReturn(Mono.just(resultSummary)); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -309,6 +349,7 @@ class ReactiveNeo4jClientTest { void automaticConversion() { when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1)); when(record1.size()).thenReturn(1); when(record1.get(0)).thenReturn(Values.value(23L)); @@ -338,6 +379,7 @@ class ReactiveNeo4jClientTest { void queriesWithoutResultShouldFitInAsWell() { when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); + when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.summary()).thenReturn(Mono.just(resultSummary)); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunctionTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunctionTest.java new file mode 100644 index 000000000..4bc7301a5 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunctionTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.mapping; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.PreferredConstructor; + +/** + * @author Michael J. Simons + */ +class DefaultNeo4jMappingFunctionTest { + + @Nested + class PropertyNames { + + @Test + void shouldDefaultToParameterNames() { + Neo4jPersistentEntity nodeDescription = mock(Neo4jPersistentEntity.class); + when(nodeDescription.getGraphProperty(Mockito.eq("aParameter"))).thenReturn(Optional.empty()); + + PreferredConstructor.Parameter parameter = mock(PreferredConstructor.Parameter.class); + when(parameter.getName()).thenReturn("aParameter"); + + DefaultNeo4jMappingFunction mappingFunction = new DefaultNeo4jMappingFunction( + mock(EntityInstantiators.class), nodeDescription); + + assertThat(mappingFunction.getGraphPropertyNameFor(parameter)).isEqualTo("aParameter"); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index 1f4834827..442c01bb7 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -92,7 +92,14 @@ public class Neo4jMappingContextTest { schema.setInitialEntitySet(new HashSet<>(Arrays.asList(InvalidId.class))); assertThatIllegalArgumentException() .isThrownBy(() -> schema.initialize()) - .withMessage("Cannot use internal id strategy with custom property poef on entity class org.springframework.data.neo4j.core.mapping.Neo4jMappingContextTest$InvalidId"); + .withMessageMatching("Cannot use internal id strategy with custom property getMappingFunctionFor on entity .*"); + } + + @Test + void shouldNotProvideMappingForUnknownClasses() { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + assertThat(schema.getMappingFunctionFor(UserNode.class)).isEmpty(); } @Node("User") @@ -130,7 +137,7 @@ public class Neo4jMappingContextTest { static class InvalidId { - @Id @Property("poef") + @Id @Property("getMappingFunctionFor") private String id; } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java index 41abdca66..948554ee8 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java @@ -26,6 +26,7 @@ import lombok.ToString; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; @Getter @Setter @@ -40,9 +41,8 @@ public class PersonWithAllConstructor { private final String name; - private final String sameValue; + @Property("first_name") + private final String firstName; - static PersonWithAllConstructor of(String sameValue) { - return new PersonWithAllConstructor(null, null, sameValue); - } + private final String sameValue; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java index c3bc4b38c..db84bd235 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java @@ -24,6 +24,7 @@ import lombok.ToString; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; @Getter @Setter @@ -34,4 +35,7 @@ public class PersonWithNoConstructor { @Id private Long id; private String name; + + @Property("first_name") + private String firstName; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 967eaaed0..28aeb1b44 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -24,11 +24,13 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,14 +49,24 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +/** + * @author Michael J. Simons + * @author Gerrit Meier + */ @ExtendWith(SpringExtension.class) @ExtendWith(Neo4jExtension.class) @ContextConfiguration(classes = RepositoryIT.Config.class) class RepositoryIT { private static final String TEST_PERSON1_NAME = "Test"; private static final String TEST_PERSON2_NAME = "Test2"; + private static final String TEST_PERSON1_FIRST_NAME = "A"; + private static final String TEST_PERSON2_FIRST_NAME = "B"; private static final String TEST_PERSON_SAMEVALUE = "SameValue"; + static PersonWithAllConstructor personExample(String sameValue) { + return new PersonWithAllConstructor(null, null, null, sameValue); + } + private static Neo4jConnectionSupport neo4jConnectionSupport; private final PersonRepository repository; @@ -77,19 +89,25 @@ class RepositoryIT { Transaction transaction = driver.session().beginTransaction(); transaction.run("MATCH (n) detach delete n"); - id1 = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = '" + TEST_PERSON1_NAME + "', n.sameValue = '" + TEST_PERSON_SAMEVALUE + "' return id(n)") - .next().get(0).asLong(); - id2 = transaction.run("CREATE (n:PersonWithAllConstructor) SET n.name = '" + TEST_PERSON2_NAME + "', n.sameValue = '" + TEST_PERSON_SAMEVALUE + "' return id(n)") - .next().get(0).asLong(); - transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = '" + TEST_PERSON1_NAME + "'"); + + id1 = transaction.run( + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName return id(n)", + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME) + ).next().get(0).asLong(); + id2 = transaction.run( + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName return id(n)", + Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME) + ).next().get(0).asLong(); + transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = $name, n.first_name = $firstName", + Values.parameters("name", TEST_PERSON1_NAME, "firstName", TEST_PERSON1_FIRST_NAME)); transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})"); transaction.success(); transaction.close(); // note that there is no id setting in the mapping right now - person1 = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON_SAMEVALUE); - person2 = new PersonWithAllConstructor(null, TEST_PERSON2_NAME, TEST_PERSON_SAMEVALUE); + person1 = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE); + person2 = new PersonWithAllConstructor(null, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE); } @Test @@ -185,7 +203,7 @@ class RepositoryIT { @Test void findAllByExampleWithSort() { - Example example = Example.of(PersonWithAllConstructor.of(TEST_PERSON_SAMEVALUE)); + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); Iterable persons = repository.findAll(example, Sort.by(Sort.Direction.DESC, "name")); assertThat(persons).containsExactly(person2, person1); @@ -193,7 +211,7 @@ class RepositoryIT { @Test void findAllByExampleWithPagination() { - Example example = Example.of(PersonWithAllConstructor.of(TEST_PERSON_SAMEVALUE)); + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); Iterable persons = repository.findAll(example, PageRequest.of(1, 1, Sort.by("name"))); assertThat(persons).containsExactly(person2); @@ -201,7 +219,7 @@ class RepositoryIT { @Test void existsByExample() { - Example example = Example.of(PersonWithAllConstructor.of(TEST_PERSON_SAMEVALUE)); + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); boolean exists = repository.exists(example); assertThat(exists).isTrue(); @@ -239,20 +257,26 @@ class RepositoryIT { void loadAllPersonsWithNoConstructor() { List persons = repository.getAllPersonsWithNoConstructorViaQuery(); - assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME)); + assertThat(persons) + .extracting(PersonWithNoConstructor::getName, PersonWithNoConstructor::getFirstName) + .containsExactlyInAnyOrder( + Tuple.tuple(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME) + ); } @Test void loadOnePersonWithNoConstructor() { PersonWithNoConstructor person = repository.getOnePersonWithNoConstructorViaQuery(); assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME); + assertThat(person.getFirstName()).isEqualTo(TEST_PERSON1_FIRST_NAME); } @Test void loadOptionalPersonWithNoConstructor() { Optional person = repository.getOptionalPersonsWithNoConstructorViaQuery(); assertThat(person).isPresent(); - assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); + assertThat(person).map(PersonWithNoConstructor::getName).contains(TEST_PERSON1_NAME); + assertThat(person).map(PersonWithNoConstructor::getFirstName).contains(TEST_PERSON1_FIRST_NAME); } @Test From 8c9243e72f7d92bbaf1d36202d4ac67c31eb898b Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 8 May 2019 21:35:07 +0200 Subject: [PATCH 073/342] Add support for optional matches and anyNodes. --- .../data/neo4j/core/DefaultNodeManager.java | 6 +- .../data/neo4j/core/cypher/Cypher.java | 18 ++++ .../core/cypher/DefaultStatementBuilder.java | 96 +++++++++++++---- .../data/neo4j/core/cypher/Match.java | 11 +- .../data/neo4j/core/cypher/Node.java | 11 +- .../data/neo4j/core/cypher/ReadingClause.java | 16 +++ .../neo4j/core/cypher/StatementBuilder.java | 34 +++++- .../cypher/renderer/RenderingVisitor.java | 3 + .../data/neo4j/core/cypher/CypherTest.java | 100 ++++++++++++++++++ 9 files changed, 269 insertions(+), 26 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index da864956e..b8c762ce0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -73,7 +73,7 @@ class DefaultNodeManager implements NodeManager { MappingSpec, Collection, T> mappingSpec = neo4jClient.newQuery(query).fetchAs(returnedType); return schema.getMappingFunctionFor(returnedType) .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) - .orElseGet(() -> mappingSpec) + .orElse(mappingSpec) .one(); } @@ -83,7 +83,7 @@ class DefaultNodeManager implements NodeManager { MappingSpec, Collection, T> mappingSpec = neo4jClient.newQuery(query).fetchAs(returnedType); return schema.getMappingFunctionFor(returnedType) .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) - .orElseGet(() -> mappingSpec) + .orElse(mappingSpec) .all(); } @@ -107,7 +107,7 @@ class DefaultNodeManager implements NodeManager { @Override public NodeDescription describe(Class clazz) { - // // @formatter:off + // @formatter:off return schema.getNodeDescription(clazz) .orElseThrow(() -> new IllegalArgumentException(String.format("%s is not a managed class", clazz.getName()))); // @formatter:on diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index d63fca752..b4e479adf 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; import org.springframework.lang.Nullable; @@ -47,6 +48,23 @@ public final class Cypher { return Node.create(primaryLabel, additionalLabels); } + /** + * @return A node matching any node. + */ + public static Node anyNode() { + return Node.create(); + } + + /** + * Prepares an optional match statement. + * + * @return An optional match without any patterns yet. + */ + public static ExposesMatch optional() { + + return Statement.builder().optional(); + } + /** * Starts building a statement based on a match clause. Use {@link Cypher#node(String, String...)} and related to * retrieve a node or a relationship, which both are pattern elements. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index d3a8d3f94..f9e6fcc14 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -22,7 +22,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.springframework.data.neo4j.core.cypher.ReadingClause.MatchList; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithWhere; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; @@ -39,9 +43,25 @@ class DefaultStatementBuilder OngoingMatchWithWhere, OngoingMatchWithoutWhere { - private List matchList = new ArrayList<>(); + /** + * Current list of matches to be generated. + */ + private final List matchesList = new ArrayList<>(); + /** + * Will the next match generated be optional? + */ + private final AtomicBoolean nextMatchShouldBeOptional = new AtomicBoolean(false); + /** + * The latest ongoing match, + */ + private DefaultOngoingMatch currentOngoingMatch; - private Condition condition; + @Override + public ExposesMatch optional() { + + nextMatchShouldBeOptional.set(true); + return this; + } @Override public OngoingMatchWithoutWhere match(PatternElement... pattern) { @@ -49,7 +69,11 @@ class DefaultStatementBuilder Assert.notNull(pattern, "Patterns to match are required."); Assert.notEmpty(pattern, "At least one pattern to match is required."); - this.matchList.addAll(Arrays.asList(pattern)); + if (this.currentOngoingMatch != null) { + this.matchesList.add(this.currentOngoingMatch); + } + this.currentOngoingMatch = new DefaultOngoingMatch(this.nextMatchShouldBeOptional.getAndSet(false)); + this.currentOngoingMatch.matchList.addAll(Arrays.asList(pattern)); return this; } @@ -76,31 +100,30 @@ class DefaultStatementBuilder @Override public OngoingMatchWithWhere where(Condition newCondition) { - this.condition = newCondition; + this.currentOngoingMatch.where(newCondition); return this; } @Override public OngoingMatchWithWhere and(Condition additionalCondition) { - this.condition = this.condition.and(additionalCondition); + this.currentOngoingMatch.and(additionalCondition); return this; } @Override public OngoingMatchWithWhere or(Condition additionalCondition) { - this.condition = this.condition.or(additionalCondition); + this.currentOngoingMatch.or(additionalCondition); return this; } - private boolean hasCondition() { - return !(this.condition == null || this.condition == CompoundCondition.EMPTY_CONDITION); - } - - protected Match buildMatch() { - Pattern pattern = new Pattern(DefaultStatementBuilder.this.matchList); - return new Match(pattern, hasCondition() ? new Where(DefaultStatementBuilder.this.condition) : null); + protected MatchList buildMatchList() { + List matchList = Stream.concat(this.matchesList.stream(), + this.currentOngoingMatch == null ? Stream.empty() : Stream.of(this.currentOngoingMatch)) + .map(DefaultOngoingMatch::buildMatch) + .collect(Collectors.toList()); + return new MatchList(matchList); } class DefaultStatementWithReturnBuilder @@ -182,10 +205,10 @@ class DefaultStatementBuilder @Override public Statement build() { - Match match = buildMatch(); + MatchList matchList = buildMatchList(); // This must be filled at this stage Return aReturn = buildReturn().get(); - return SinglePartQuery.createReturningQuery(aReturn, match); + return SinglePartQuery.createReturningQuery(aReturn, matchList); } } @@ -213,7 +236,6 @@ class DefaultStatementBuilder @Override public OngoingMatchAndReturn returning(Expression... expressions) { - Assert.notNull(expressions, "Expressions to return are required."); Assert.notEmpty(expressions, "At least one expressions to return is required."); @@ -230,11 +252,49 @@ class DefaultStatementBuilder @Override public Statement build() { - Match match = buildMatch(); + MatchList matchList = buildMatchList(); Delete delete = buildDelete(); Optional optionalReturn = buildReturn(); - return SinglePartQuery.createUpdatingQuery(match, delete, optionalReturn.orElse(null)); + return SinglePartQuery.createUpdatingQuery(matchList, delete, optionalReturn.orElse(null)); } } + + class DefaultOngoingMatch { + + private final List matchList = new ArrayList<>(); + + private final boolean optional; + + private Condition condition; + + DefaultOngoingMatch(boolean optional) { + this.optional = optional; + } + + void where(Condition newCondition) { + + this.condition = newCondition; + } + + void and(Condition additionalCondition) { + + this.condition = this.condition.and(additionalCondition); + } + + void or(Condition additionalCondition) { + + this.condition = this.condition.or(additionalCondition); + } + + boolean hasCondition() { + return !(this.condition == null || this.condition == CompoundCondition.EMPTY_CONDITION); + } + + Match buildMatch() { + Pattern pattern = new Pattern(this.matchList); + return new Match(optional, pattern, hasCondition() ? new Where(this.condition) : null); + } + } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java index bcfc4800b..dc5f4d718 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java @@ -28,17 +28,24 @@ import org.springframework.lang.Nullable; * @author Michael J. Simons * @since 1.0 */ -public class Match implements ReadingClause { +public class Match implements Visitable { + + private final boolean optional; private final Pattern pattern; private @Nullable final Where optionalWhere; - Match(Pattern pattern, @Nullable Where optionalWhere) { + Match(boolean optional, Pattern pattern, @Nullable Where optionalWhere) { + this.optional = optional; this.pattern = pattern; this.optionalWhere = optionalWhere; } + public boolean isOptional() { + return optional; + } + @Override public void accept(Visitor visitor) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java index 1c9d7e7c4..5f5c0e807 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java @@ -49,6 +49,13 @@ public class Node implements PatternElement, Named, Expression { return new Node(primaryLabel, additionalLabels); } + /** + * @return A node without labels + */ + static Node create() { + return new Node(null); + } + private @Nullable final SymbolicName symbolicName; private final List labels; @@ -58,7 +65,9 @@ public class Node implements PatternElement, Named, Expression { this.symbolicName = null; this.labels = new ArrayList<>(); - this.labels.add(primaryLabel); + if (!(primaryLabel == null || primaryLabel.isEmpty())) { + this.labels.add(primaryLabel); + } this.labels.addAll(Arrays.asList(additionalLabels)); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java index b6a8cef73..8a8c83c7e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java @@ -18,11 +18,27 @@ */ package org.springframework.data.neo4j.core.cypher; +import java.util.List; + import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; /** * @author Michael J. Simons * @since 1.0 */ interface ReadingClause extends Visitable { + + class MatchList implements ReadingClause { + private final List matchList; + + MatchList(List matchList) { + this.matchList = matchList; + } + + @Override + public void accept(Visitor visitor) { + matchList.forEach(match -> match.accept(visitor)); + } + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index abe9a5483..b24fdea71 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -25,6 +25,13 @@ package org.springframework.data.neo4j.core.cypher; */ public interface StatementBuilder { + /** + * See {@link Cypher#optional()}. + * + * @return + */ + ExposesMatch optional(); + /** * See {@link Cypher#match(PatternElement...)}. * @@ -38,7 +45,14 @@ public interface StatementBuilder { * While the where clause is optional, an returning clause needs to be specified before the * statement can be build. */ - interface OngoingMatchWithoutWhere extends OngoingMatch { + interface OngoingMatchWithoutWhere extends OngoingMatch, ExposesMatch { + + /** + * Marks the next match as optional match. + * + * @return A step exposing a {@link ExposesMatch#match(PatternElement...)} method for adding patterns to match. + */ + ExposesMatch optional(); /** * Adds a where clause to this match. @@ -52,7 +66,9 @@ public interface StatementBuilder { /** * A match that has a non-empty {@code where}-part. THe returning clause is still open. */ - interface OngoingMatchWithWhere extends OngoingMatch { + interface OngoingMatchWithWhere extends OngoingMatch, ExposesMatch { + + ExposesMatch optional(); /** * Adds an additional condition to the existing conditions, connected by an {@literal and}. @@ -211,6 +227,20 @@ public interface StatementBuilder { OngoingMatchAndDelete delete(Expression... expressions); } + /** + * A step exposing a {@link #match(PatternElement...)} method. + */ + interface ExposesMatch { + + /** + * Adds another match clause. + * + * @param pattern The patterns to match + * @return An ongoing match that is used to specify an optional where and a required return clause + */ + OngoingMatchWithoutWhere match(PatternElement... pattern); + } + /** * A buildable step that will create a MATCH ... DELETE statement. */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index 7f339dfb0..2dc811890 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -116,6 +116,9 @@ class RenderingVisitor extends ReflectiveVisitor { } void enter(Match match) { + if (match.isOptional()) { + builder.append("OPTIONAL "); + } builder.append("MATCH "); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 657c721ab..1bc915021 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -223,6 +223,91 @@ public class CypherTest { } } + @Nested + class MultipleMatches { + @Test + void simple() { + Statement statement = Cypher + .match(bikeNode) + .match(userNode, Cypher.node("U").named("o")) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) RETURN b"); + } + + @Test + void simpleWhere() { + Statement statement = Cypher + .match(bikeNode) + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull()) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); + } + + @Test + void multiWhere() { + Statement statement = Cypher + .match(bikeNode) + .where(bikeNode.property("a").isNotNull()) + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull()) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) WHERE b.a IS NOT NULL MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); + } + + @Test + void multiWhereMultiConditions() { + Statement statement = Cypher + .match(bikeNode) + .where(bikeNode.property("a").isNotNull()) + .and(bikeNode.property("b").isNull()) + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull().or(userNode.internalId().isEqualTo(literalOf(4711)))) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) WHERE (b.a IS NOT NULL AND b.b IS NULL) MATCH (u:`User`), (o:`U`) WHERE (u.a IS NULL OR id(u) = 4711) RETURN b"); + } + + @Test + void optional() { + Statement statement = Cypher + .optional() + .match(bikeNode) + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull()) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("OPTIONAL MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); + } + + @Test + void optionalNext() { + Statement statement = Cypher + .match(bikeNode) + .optional() + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull()) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) OPTIONAL MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); + } + } + @Nested class FunctionRendering { @Test @@ -660,5 +745,20 @@ public class CypherTest { "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DETACH DELETE u RETURN u ORDER BY u.a ASC SKIP 2 LIMIT 1"); } + + @Test + void shouldRenderNodeDelete() { + Node n = anyNode().named("n"); + Relationship r = n.relationshipBetween(anyNode()).named("r0").create(); + Statement statement = Cypher + .match(n).where(n.internalId().isEqualTo(literalOf(4711))) + .optional().match(r) + .delete(r, n) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) WHERE id(n) = 4711 OPTIONAL MATCH (n)-[r0]-() DELETE r0, n"); + } } } From 09a05d7177baa67c04d5559a67821d0e7e70b57d Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 9 May 2019 13:15:14 +0200 Subject: [PATCH 074/342] Improve DSL with fluent relationships and parameter placeholders. --- etc/jqassistant/api.adoc | 3 +- .../data/neo4j/core/cypher/Aliased.java | 3 + .../neo4j/core/cypher/AliasedExpression.java | 4 +- .../data/neo4j/core/cypher/Comparison.java | 4 +- .../neo4j/core/cypher/CompoundCondition.java | 5 +- .../data/neo4j/core/cypher/Condition.java | 2 + .../data/neo4j/core/cypher/Conditions.java | 4 +- .../data/neo4j/core/cypher/Cypher.java | 16 ++ .../data/neo4j/core/cypher/Delete.java | 5 +- .../core/cypher/ExposesRelationships.java | 54 ++++++ .../data/neo4j/core/cypher/Expression.java | 2 + .../data/neo4j/core/cypher/Expressions.java | 3 + .../neo4j/core/cypher/FunctionInvocation.java | 5 +- .../data/neo4j/core/cypher/Functions.java | 2 + .../data/neo4j/core/cypher/IsNull.java | 4 +- .../data/neo4j/core/cypher/Limit.java | 6 +- .../data/neo4j/core/cypher/ListLiteral.java | 5 +- .../data/neo4j/core/cypher/Literal.java | 2 + .../data/neo4j/core/cypher/Match.java | 4 +- .../data/neo4j/core/cypher/Named.java | 3 + .../data/neo4j/core/cypher/Node.java | 183 ++---------------- .../data/neo4j/core/cypher/NotCondition.java | 4 +- .../data/neo4j/core/cypher/NumberLiteral.java | 5 +- .../data/neo4j/core/cypher/ObjectLiteral.java | 5 +- .../data/neo4j/core/cypher/Order.java | 4 +- .../data/neo4j/core/cypher/Parameter.java | 41 ++++ .../data/neo4j/core/cypher/Pattern.java | 4 +- .../neo4j/core/cypher/PatternElement.java | 2 + .../data/neo4j/core/cypher/Property.java | 4 +- .../data/neo4j/core/cypher/RegularQuery.java | 3 + .../data/neo4j/core/cypher/Relationship.java | 43 +++- .../neo4j/core/cypher/RelationshipChain.java | 99 ++++++++++ .../neo4j/core/cypher/RelationshipDetail.java | 40 +++- .../data/neo4j/core/cypher/Relationships.java | 61 ------ .../data/neo4j/core/cypher/Return.java | 4 +- .../neo4j/core/cypher/SinglePartQuery.java | 4 +- .../data/neo4j/core/cypher/Skip.java | 6 +- .../data/neo4j/core/cypher/SortItem.java | 4 +- .../data/neo4j/core/cypher/Statement.java | 3 + .../neo4j/core/cypher/StatementBuilder.java | 3 + .../data/neo4j/core/cypher/StringLiteral.java | 4 +- .../data/neo4j/core/cypher/SymbolicName.java | 5 +- .../data/neo4j/core/cypher/Where.java | 4 +- .../cypher/renderer/RenderingVisitor.java | 45 ++++- .../data/neo4j/core/cypher/CypherTest.java | 63 +++++- 45 files changed, 506 insertions(+), 273 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExposesRelationships.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipChain.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationships.java diff --git a/etc/jqassistant/api.adoc b/etc/jqassistant/api.adoc index 07ca9e282..ec809b2d5 100644 --- a/etc/jqassistant/api.adoc +++ b/etc/jqassistant/api.adoc @@ -42,10 +42,11 @@ See ADR-003. [[api:internal]] [source,cypher,role="constraint",requiresConcepts="api:api-guardian-api-concept"] -.Public classes that are only part of internal API must be final +.Non abstract, public classes that are only part of internal API must be final ---- MATCH (c:Class) - [:IS_PART_OF] -> (:Api {type: 'Internal'}) WHERE c.visibility = 'public' + AND coalesce(c.abstract, false) = false AND NOT exists(c.final) RETURN c.name ---- diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java index c4d9ec494..6020b563c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; + /** * An element with an alias. An alias has a subtle difference to a symbolic name in cypher. Nodes and relationships can * have symbolic names which in turn can be aliased as well. @@ -27,6 +29,7 @@ package org.springframework.data.neo4j.core.cypher; * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Aliased { /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java index 0965f7500..cdce556d1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.core.cypher; import static org.springframework.data.neo4j.core.cypher.Expressions.*; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.util.Assert; @@ -29,7 +30,8 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -public class AliasedExpression implements Aliased, Expression { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class AliasedExpression implements Aliased, Expression { private final Expression delegate; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java index 962be6b84..b9415c8af 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.util.Assert; @@ -28,7 +29,8 @@ import org.springframework.util.Assert; * @author Gerrit Meier * @since 1.0 */ -public class Comparison implements Condition { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Comparison implements Condition { static Comparison create(Expression lhs, String comparator, Expression rhs) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java index 5f50cddab..e3a388775 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java @@ -23,6 +23,7 @@ import static org.springframework.data.neo4j.core.cypher.CompoundCondition.Logic import java.util.ArrayList; import java.util.List; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; @@ -33,8 +34,10 @@ import org.springframework.util.Assert; * Logical connective (operator). * * @author Michael J. Simons + * @since 1.0 */ -public class CompoundCondition implements Condition { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class CompoundCondition implements Condition { /** * The empty, compound condition. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java index 966dc3f29..322188f60 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.CompoundCondition.LogicalOperator; import org.springframework.data.neo4j.core.cypher.support.Visitable; @@ -27,6 +28,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Condition extends Visitable { default Condition and(Condition condition) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java index 56f7cd782..70a20a35c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.util.Assert; /** @@ -28,7 +29,8 @@ import org.springframework.util.Assert; * @author Gerrit Meier * @since 1.0 */ -public class Conditions { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Conditions { /** * Creates a condition that matches if the right hand side is a regular expression that matches the the left hand side via diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index b4e479adf..bc8522120 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -55,6 +55,22 @@ public final class Cypher { return Node.create(); } + /** + * @return A node matching any node with the symbolic the given {@code symbolicName}. + */ + public static Node anyNode(String symbolicName) { + return Node.create().named(symbolicName); + } + + /** + * Creates a new parameter placeholder. + * @param name + * @return + */ + public static Parameter parameter(String name) { + return new Parameter(name); + } + /** * Prepares an optional match statement. * diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java index 4673fdf03..d65efa5e6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java @@ -18,14 +18,17 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** * See Delete. * * @author Michael J. Simons + * @since 1.0 */ -public class Delete implements UpdatingClause { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Delete implements UpdatingClause { private final ExpressionList deleteItems; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExposesRelationships.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExposesRelationships.java new file mode 100644 index 000000000..86ae63b6d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExposesRelationships.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; + +/** + * A marker interface for things that expose methods to create new relationships to other elements. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public interface ExposesRelationships { + /** + * Starts building an outgoing relationship to the {@code other} {@link Node node}. + * + * @param other The other end of the outgoing relationship + * @return An ongoing relationship definition, that can be used to specify the type + */ + T relationshipTo(Node other, String... types); + + /** + * Starts building an incoming relationship starting at the {@code other} {@link Node node}. + * + * @param other The source of the incoming relationship + * @return An ongoing relationship definition, that can be used to specify the type + */ + T relationshipFrom(Node other, String... types); + + /** + * Starts building an undirected relationship between this {@link Node node} and the {@code other}. + * + * @param other The other end of the relationship + * @return An ongoing relationship definition, that can be used to specify the type + */ + T relationshipBetween(Node other, String... types); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java index 6c3d096f3..3c58c6f7b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.util.Assert; @@ -27,6 +28,7 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Expression extends Visitable { /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java index a1031d1eb..da35e3f3c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java @@ -18,12 +18,15 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; + /** * Utility methods for dealing with expressions. * * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public final class Expressions { /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java index 9b73244da..2ef66fc4c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** @@ -26,8 +27,10 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ -public class FunctionInvocation implements Expression { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class FunctionInvocation implements Expression { private final String functionName; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java index 4524f04f7..71d43ab4e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.util.Assert; /** @@ -26,6 +27,7 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public final class Functions { private static final String F_ID = "id"; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java index 727875e08..15671b7d3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.util.Assert; @@ -29,7 +30,8 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -public class IsNull implements Condition { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class IsNull implements Condition { /** * Creates a new {@link IsNull IsNull-condition}. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java index b24f03c78..5fc7aa7f8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java @@ -18,13 +18,17 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 */ -public class Limit implements Visitable { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Limit implements Visitable { public static Limit of(Number value) { return new Limit(new NumberLiteral(value)); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java index a37b567ae..cdca7f2ae 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java @@ -21,12 +21,15 @@ package org.springframework.data.neo4j.core.cypher; import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import org.apiguardian.api.API; + /** * @author Gerrit Meier * @author Michael J. Simons * @since 1.0 */ -public class ListLiteral extends Literal { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class ListLiteral extends Literal { ListLiteral(Iterable content) { super(content); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java index 1f64b051d..05e9d56a3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.lang.Nullable; /** @@ -26,6 +27,7 @@ import org.springframework.lang.Nullable; * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public abstract class Literal implements Expression { /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java index dc5f4d718..b5c7397a2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; @@ -28,7 +29,8 @@ import org.springframework.lang.Nullable; * @author Michael J. Simons * @since 1.0 */ -public class Match implements Visitable { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Match implements Visitable { private final boolean optional; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java index 1f464deb2..7e97ede50 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java @@ -20,12 +20,15 @@ package org.springframework.data.neo4j.core.cypher; import java.util.Optional; +import org.apiguardian.api.API; + /** * A named thing exposes an optional {@link #getSymbolicName() symbolic name}. * * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Named { Optional getSymbolicName(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java index 5f5c0e807..48086bd9c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java @@ -18,15 +18,16 @@ */ package org.springframework.data.neo4j.core.cypher; -import lombok.RequiredArgsConstructor; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.Relationship.Direction; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -36,7 +37,8 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -public class Node implements PatternElement, Named, Expression { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Node implements PatternElement, Named, Expression, ExposesRelationships { static Node create(String primaryLabel, String... additionalLabels) { @@ -79,7 +81,7 @@ public class Node implements PatternElement, Named, Expression { } /** - * Creates a copy of that node with a new symbolic name. + * Creates a copy of this node with a new symbolic name. * * @param newSymbolicName the new symbolic name. * @return The new node. @@ -120,171 +122,26 @@ public class Node implements PatternElement, Named, Expression { return Functions.id(this); } - /** - * Starts building an outgoing relationship to the {@code other} {@link Node node}. - * - * @param other The other end of the outgoing relationship - * @return An ongoing relationship definition, that can be used to specify the type - */ - public OngoingRelationshipDefinition relationshipTo(Node other) { - return new DefaultOngoingRelationshipDefinition(this, Direction.LTR, other); + @Override + public Relationship relationshipTo(Node other, String... types) { + return Relationship.create(this, Direction.LTR, other, types); } - /** - * Starts building an incoming relationship starting at the {@code other} {@link Node node}. - * - * @param other The source of the incoming relationship - * @return An ongoing relationship definition, that can be used to specify the type - */ - public OngoingRelationshipDefinition relationshipFrom(Node other) { - return new DefaultOngoingRelationshipDefinition(this, Direction.RTR, other); + @Override + public Relationship relationshipFrom(Node other, String... types) { + return Relationship.create(this, Direction.RTR, other, types); } - /** - * Starts building an undirected relationship between this {@link Node node} and the {@code other}. - * - * @param other The other end of the relationship - * @return An ongoing relationship definition, that can be used to specify the type - */ - public OngoingRelationshipDefinition relationshipBetween(Node other) { - return new DefaultOngoingRelationshipDefinition(this, Direction.UNI, other); + @Override + public Relationship relationshipBetween(Node other, String... types) { + return Relationship.create(this, Direction.UNI, other, types); } - /** - * Exposes {@code withType) and terminal operations for creating a relationship. - * - * @param

The final thing to be defined - */ - public interface OngoingRelationshipDefinition

- extends OngoingRelationshipDefinitionWithType

, OngoingRelationshipDefinitionWithSymbolicName

{ + @Override + public void accept(Visitor visitor) { - /** - * Specifies the type of the relationship. - * - * @param types Zero or more types for this relationship - * @return The ongoing relationship definition - */ - OngoingRelationshipDefinitionWithType

withType(String... types); - } - - /** - * Exposes {@code withType} to specify types for the relationship or the the last element of a chain of relationships, - * {@code as} to specify a symbolic name for the last element in the chain as well as terminal operations - * to create the chain or continue it with the next hop. - * - * @param

The final thing to be defined - */ - public interface OngoingRelationshipDefinitionWithType

extends OngoingRelationshipDefinitionWithSymbolicName

{ - - /** - * Gives the relationship a new, symbolic name. - * - * @param symbolicName The symbolic name to use. - * @return The ongoing relationship definition - */ - OngoingRelationshipDefinitionWithSymbolicName

named(String symbolicName); - } - - /** - * @param

The final thing to be defined - */ - public interface OngoingRelationshipDefinitionWithSymbolicName

{ - - /** - * Creates the chain of relationships. - * - * @return A chain of relationship with at least one element. - */ - P create(); - - /** - * Adds a new link to this chain of relationships. - * - * @param tripNode The end node of the next link - * @return The ongoing relationship definition - */ - OngoingRelationshipDefinition outgoingRelationShipTo(Node tripNode); - } - - @RequiredArgsConstructor - private static class DefaultOngoingRelationshipDefinition - implements OngoingRelationshipDefinition { - - private final Node left; - - private final Direction direction; - - private final Node right; - - private String[] types = new String[0]; - - private String symbolicName; - - @Override - public OngoingRelationshipDefinitionWithType withType(@SuppressWarnings("HiddenField") String... types) { - this.types = types; - return this; - } - - @Override - public Relationship create() { - return Relationship.create(left, direction, right, symbolicName, types); - } - - @Override - public OngoingRelationshipDefinitionWithSymbolicName named(@SuppressWarnings("HiddenField") String symbolicName) { - this.symbolicName = symbolicName; - return this; - } - - @Override - public OngoingRelationshipDefinition outgoingRelationShipTo(Node next) { - return new DefaultOngoingRelationshipsDefinition(this.create(), - new DefaultOngoingRelationshipDefinition(right, Direction.LTR, next)); - } - } - - private static class DefaultOngoingRelationshipsDefinition - implements OngoingRelationshipDefinition { - - private final List chain; - - private OngoingRelationshipDefinition nextElement; - - DefaultOngoingRelationshipsDefinition( - Relationship firstElement, - OngoingRelationshipDefinition nextElement) { - this.chain = new ArrayList<>(); - this.chain.add(firstElement); - this.nextElement = nextElement; - } - - @Override - public OngoingRelationshipDefinitionWithType withType(String... types) { - this.nextElement.withType(types); - return this; - } - - @Override - public OngoingRelationshipDefinitionWithSymbolicName named(String symbolicName) { - this.nextElement.named(symbolicName); - return this; - } - - @Override - public Relationships create() { - this.chain.add(this.nextElement.create()); - return new Relationships(this.chain); - } - - @Override - public OngoingRelationshipDefinition outgoingRelationShipTo(Node next) { - Relationship lastRelationship = this.nextElement.create(); - this.chain.add(lastRelationship); - this.nextElement = - new DefaultOngoingRelationshipDefinition(lastRelationship.getRight(), Direction.LTR, next); - - return this; - } + visitor.enter(this); + Visitable.visitIfNotNull(this.symbolicName, visitor); + visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java index e670693a3..de429eeee 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** @@ -26,7 +27,8 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; * @author Michael J. Simons * @since 1.0 */ -public class NotCondition implements Condition { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class NotCondition implements Condition { /** * The condition to which the visit is delegated to after entering this condition. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java index 02c82beff..82d848446 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java @@ -18,12 +18,15 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; + /** * @author Gerrit Meier * @author Michael J. Simons * @since 1.0 */ -public class NumberLiteral extends Literal { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class NumberLiteral extends Literal { NumberLiteral(Number content) { super(content); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java index e3052dec0..7610687cf 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; + /** * The most generic implementation of {@link Literal}. Takes an arbitrary object and calls {@code toString} on it to get * its value representation. @@ -26,7 +28,8 @@ package org.springframework.data.neo4j.core.cypher; * @author Michael J. Simons * @since 1.0 */ -public class ObjectLiteral extends Literal { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class ObjectLiteral extends Literal { ObjectLiteral(Object content) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java index 1a342840b..0d9e92435 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.core.cypher; import java.util.List; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; /** @@ -29,7 +30,8 @@ import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; * @author Michael J. Simons * @since 1.0 */ -public class Order extends TypedSubtree { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Order extends TypedSubtree { Order(List sortItems) { super(sortItems); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java new file mode 100644 index 000000000..4999a6b2b --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; + +/** + * Represents a named parameter inside a Cypher statement. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Parameter implements Expression { + + private final String name; + + Parameter(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java index cb3b7d6a8..45b32915d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.core.cypher; import java.util.List; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; /** @@ -31,7 +32,8 @@ import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; * @author Michael J. Simons * @since 1.0 */ -public class Pattern extends TypedSubtree { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Pattern extends TypedSubtree { Pattern(List patternElements) { super(patternElements); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java index d56b57ed5..ae1bb0fc1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java @@ -18,11 +18,13 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; /** * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface PatternElement extends Visitable { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java index bbcf49e18..9dd250d4f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.util.Assert; /** @@ -26,7 +27,8 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -public class Property implements Expression { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Property implements Expression { static Property create(Node parentContainer, String name) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java index 3a3270fac..0e464ea5f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java @@ -18,11 +18,14 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; + /** * See RegularQuery. * * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface RegularQuery { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java index be08e761d..02837e773 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core.cypher; import java.util.Arrays; import java.util.Optional; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -31,7 +32,10 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -public class Relationship implements PatternElement, Named, Expression { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Relationship implements + PatternElement, Named, Expression, + ExposesRelationships { /** * While the direction in the schema package is centered around the node, the direction here is the direction between two nodes. @@ -69,14 +73,14 @@ public class Relationship implements PatternElement, Named, Expression { } static Relationship create(Node left, - @Nullable Direction direction, Node right, @Nullable String symbolicName, String... types) { + @Nullable Direction direction, Node right, String... types) { Assert.notNull(left, "Left node is required."); Assert.notNull(right, "Right node is required."); RelationshipDetail details = new RelationshipDetail( Optional.ofNullable(direction).orElse(Direction.UNI), - Optional.ofNullable(symbolicName).map(SymbolicName::new).orElse(null), Arrays.asList(types)); + null, Arrays.asList(types)); return new Relationship(left, details, right); } @@ -104,10 +108,43 @@ public class Relationship implements PatternElement, Named, Expression { return details; } + /** + * Creates a copy of this relationship with a new symbolic name. + * + * @param newSymbolicName the new symbolic name. + * @return The new relationship. + */ + public Relationship named(String newSymbolicName) { + + // Sanity check of newSymbolicName delegated to the details. + return new Relationship(this.left, this.details.named(newSymbolicName), this.right); + } + public Optional getSymbolicName() { return details.getSymbolicName(); } + @Override + public RelationshipChain relationshipTo(Node other, String... types) { + return RelationshipChain + .create(this) + .add(this.right.relationshipTo(other, types)); + } + + @Override + public RelationshipChain relationshipFrom(Node other, String... types) { + return RelationshipChain + .create(this) + .add(this.right.relationshipFrom(other, types)); + } + + @Override + public RelationshipChain relationshipBetween(Node other, String... types) { + return RelationshipChain + .create(this) + .add(this.right.relationshipBetween(other, types)); + } + @Override public void accept(Visitor visitor) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipChain.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipChain.java new file mode 100644 index 000000000..c1bd645d6 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipChain.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.springframework.data.neo4j.core.cypher.support.Visitable.*; + +import java.util.LinkedList; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; + +/** + * Represents a chain of relationships. The chain is meant to be in order and the right node of an element is related to + * the left node of the next element. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class RelationshipChain implements PatternElement, ExposesRelationships { + + private final LinkedList relationships = new LinkedList<>(); + + static RelationshipChain create(Relationship firstElement) { + + return new RelationshipChain() + .add(firstElement); + } + + RelationshipChain add(Relationship element) { + + Assert.notNull(element, "Elements of a relationship chain must not be null."); + this.relationships.add(element); + return this; + } + + @Override + public RelationshipChain relationshipTo(Node other, String... types) { + return this.add(this.relationships.getLast().getRight().relationshipTo(other, types)); + } + + @Override + public RelationshipChain relationshipFrom(Node other, String... types) { + return this.add(this.relationships.getLast().getRight().relationshipFrom(other, types)); + } + + @Override + public RelationshipChain relationshipBetween(Node other, String... types) { + return this.add(this.relationships.getLast().getRight().relationshipBetween(other, types)); + } + + /** + * Replaces the last element of this chains with a copy of the relationship with the new symbolic name.. + * + * @param newSymbolicName The new symbolic name to use + * @return This chain + */ + public RelationshipChain named(String newSymbolicName) { + + Relationship lastElement = this.relationships.removeLast(); + return this.add(lastElement.named(newSymbolicName)); + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + + Node lastNode = null; + for (Relationship relationship : relationships) { + + relationship.getLeft().accept(visitor); + relationship.getDetails().accept(visitor); + + lastNode = relationship.getRight(); + } + + visitIfNotNull(lastNode, visitor); + + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java index 51e7efaef..ff2aef843 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java @@ -19,13 +19,17 @@ package org.springframework.data.neo4j.core.cypher; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.Relationship.Direction; import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * See RelationshipDetail @@ -33,7 +37,8 @@ import org.springframework.lang.Nullable; * @author Michael J. Simons * @since 1.0 */ -public class RelationshipDetail implements Visitable { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class RelationshipDetail implements Visitable { /** * The direction between the nodes of the relationship. @@ -46,16 +51,37 @@ public class RelationshipDetail implements Visitable { RelationshipDetail(Direction direction, @Nullable SymbolicName symbolicName, List types) { + + this(direction, symbolicName); + this.types.addAll(types); + } + + private RelationshipDetail(Direction direction, + @Nullable SymbolicName symbolicName) { + this.direction = direction; this.symbolicName = symbolicName; - this.types = new ArrayList<>(types); + this.types = new ArrayList<>(); + } + + RelationshipDetail named(String newSymbolicName) { + + Assert.hasText(newSymbolicName, "Symbolic name is required."); + return new RelationshipDetail(this.direction, new SymbolicName(newSymbolicName), this.types); + } + + RelationshipDetail withType(String... newTypes) { + + RelationshipDetail newDetails = new RelationshipDetail(this.direction, this.symbolicName); + newDetails.types.addAll(Arrays.asList(newTypes)); + return newDetails; } public Direction getDirection() { return direction; } - public Optional getSymbolicName() { + Optional getSymbolicName() { return Optional.ofNullable(symbolicName); } @@ -66,4 +92,12 @@ public class RelationshipDetail implements Visitable { public boolean isTyped() { return !this.types.isEmpty(); } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + Visitable.visitIfNotNull(this.symbolicName, visitor); + visitor.leave(this); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationships.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationships.java deleted file mode 100644 index 1be13d98b..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationships.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.cypher; - -import static org.springframework.data.neo4j.core.cypher.support.Visitable.*; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.data.neo4j.core.cypher.support.Visitor; - -/** - * Represents a chain of relationships. The chain is meant to be in order and the right node of an element is related to - * the left node of the next element. - * - * @author Michael J. Simons - * @since 1.0 - */ -public class Relationships implements PatternElement { - - private List relationships; - - Relationships(List relationships) { - this.relationships = new ArrayList(relationships); - } - - @Override - public void accept(Visitor visitor) { - - visitor.enter(this); - - Node lastNode = null; - for (Relationship relationship : relationships) { - - relationship.getLeft().accept(visitor); - relationship.getDetails().accept(visitor); - - lastNode = relationship.getRight(); - } - - visitIfNotNull(lastNode, visitor); - - visitor.leave(this); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java index bcb6c1b82..ac33c6660 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; @@ -29,7 +30,8 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; * @author Michael J. Simons * @since 1.0 */ -public class Return implements Visitable { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Return implements Visitable { private final ExpressionList returnItems; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java index 6917a9b12..76ee4a295 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; @@ -30,7 +31,8 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -public class SinglePartQuery implements SingleQuery { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class SinglePartQuery implements SingleQuery { private @Nullable final ReadingClause readingClause; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java index d31181e9d..9e3d6946f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java @@ -18,13 +18,17 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 */ -public class Skip implements Visitable { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Skip implements Visitable { public static Skip of(Number value) { return new Skip(new NumberLiteral(value)); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java index b48d7e07f..f47b4f78d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java @@ -22,6 +22,7 @@ import static org.springframework.data.neo4j.core.cypher.Expressions.*; import java.util.Optional; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; @@ -31,7 +32,8 @@ import org.springframework.lang.Nullable; * @author Michael J. Simons * @since 1.0 */ -public class SortItem implements Visitable { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class SortItem implements Visitable { private final Expression expression; private final Direction direction; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java index 4b73b8e84..d540db0d7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; /** @@ -30,7 +31,9 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; * @author Michael J. Simons * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Statement extends Visitable { + static StatementBuilder builder() { return new DefaultStatementBuilder(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index b24fdea71..004ba50e1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -18,11 +18,14 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; + /** * @author Michael J. Simons * @author Gerrit Meier * @since 1.0 */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface StatementBuilder { /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java index 2f726eec0..6929b73fd 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java @@ -25,6 +25,7 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apiguardian.api.API; import org.springframework.lang.Nullable; /** @@ -34,7 +35,8 @@ import org.springframework.lang.Nullable; * @author Michael J. Simons * @since 1.0 */ -public class StringLiteral extends Literal { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class StringLiteral extends Literal { private static final Pattern RESERVED_CHARS = Pattern.compile("([" + quote("\\'\"") + "])"); private static final String QUOTED_LITERAL_FORMAT = "'%s'"; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java index 6872dc224..160406b8b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; + /** * A symbolic name to identify nodes and relationships. *

@@ -27,7 +29,8 @@ package org.springframework.data.neo4j.core.cypher; * @author Michael J. Simons * @since 1.0 */ -public class SymbolicName implements Expression { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class SymbolicName implements Expression { private final String name; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java index cfee360d7..67305cbcc 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.cypher; +import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; @@ -27,7 +28,8 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; * @author Michael J. Simons * @since 1.0 */ -public class Where implements Visitable { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Where implements Visitable { private final Condition condition; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index 2dc811890..77c5f98e2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -69,6 +69,11 @@ class RenderingVisitor extends ReflectiveVisitor { */ private final Set separatorOnLevel = new HashSet<>(); + /** + * Keeps track of named objects that have been already visited. + */ + private final Set visitedNamed = new HashSet<>(); + /** * The current level in the tree of cypher elements. */ @@ -231,14 +236,24 @@ class RenderingVisitor extends ReflectiveVisitor { } void enter(Node node) { - builder.append("(") - .append(node.getSymbolicName().map(SymbolicName::getName).orElse("")) - .append(node.isLabeled() ? LABEL_SEPARATOR : "") - .append(node.getLabels().stream().map(RenderingVisitor::escapeName) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(joining(LABEL_SEPARATOR))) - .append(")"); + builder.append("("); + + } + + void leave(Node node) { + + // This is only relevant for nodes in relationships. + if (!(node.getSymbolicName().isPresent() && visitedNamed.contains(node))) { + builder + .append(node.isLabeled() ? LABEL_SEPARATOR : "") + .append(node.getLabels().stream().map(RenderingVisitor::escapeName) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(joining(LABEL_SEPARATOR))); + visitedNamed.add(node); + } + + builder.append(")"); } void enter(SymbolicName symbolicName) { @@ -250,8 +265,13 @@ class RenderingVisitor extends ReflectiveVisitor { Relationship.Direction direction = details.getDirection(); builder.append(direction.getSymbolLeft()); builder - .append("[") - .append(details.getSymbolicName().map(SymbolicName::getName).orElse("")) + .append("["); + } + + void leave(RelationshipDetail details) { + + Relationship.Direction direction = details.getDirection(); + builder .append(details.isTyped() ? TYPE_SEPARATOR : "") .append(details.getTypes().stream().map(RenderingVisitor::escapeName) .filter(Optional::isPresent) @@ -261,6 +281,11 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append(direction.getSymbolRight()); } + void enter(Parameter parameter) { + + builder.append("$").append(parameter.getName()); + } + public String getRenderedContent() { return this.builder.toString(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 1bc915021..3de3d538d 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -69,7 +69,7 @@ public class CypherTest { @Test void simpleRelationship() { Statement statement = Cypher - .match(userNode.relationshipTo(bikeNode).withType("OWNS").create()) + .match(userNode.relationshipTo(bikeNode, "OWNS")) .returning(bikeNode, userNode) .build(); @@ -80,7 +80,7 @@ public class CypherTest { @Test void simpleRelationshipWithReturn() { Relationship owns = userNode - .relationshipTo(bikeNode).withType("OWNS").named("o").create(); + .relationshipTo(bikeNode, "OWNS").named("o"); Statement statement = Cypher .match(owns) @@ -93,12 +93,11 @@ public class CypherTest { @Test void chainedRelations() { - Node tripNode = Cypher.node("Trip").named("u"); + Node tripNode = Cypher.node("Trip").named("t"); Statement statement = Cypher .match(userNode - .relationshipTo(bikeNode).withType("OWNS").named("r1") - .outgoingRelationShipTo(tripNode).withType("USED_ON").named("r2") - .create() + .relationshipTo(bikeNode, "OWNS").named("r1") + .relationshipTo(tripNode, "USED_ON").named("r2") ) .where(userNode.property("name").matches(".*aName")) .returning(bikeNode, userNode) @@ -106,7 +105,36 @@ public class CypherTest { assertThat(cypherRenderer.render(statement)) .isEqualTo( - "MATCH (u:`User`)-[r1:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(u:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); + "MATCH (u:`User`)-[r1:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); + + statement = Cypher + .match(userNode + .relationshipTo(bikeNode, "OWNS") + .relationshipTo(tripNode, "USED_ON").named("r2") + ) + .where(userNode.property("name").matches(".*aName")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); + + + statement = Cypher + .match(userNode + .relationshipTo(bikeNode, "OWNS") + .relationshipTo(tripNode, "USED_ON").named("r2") + .relationshipFrom(userNode, "WAS_ON").named("x") + .relationshipBetween(Cypher.node("SOMETHING")).named("y") + ) + .where(userNode.property("name").matches(".*aName")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`)<-[x:`WAS_ON`]-(u)-[y]-(:`SOMETHING`) WHERE u.name =~ '.*aName' RETURN b, u"); } @Test @@ -748,8 +776,8 @@ public class CypherTest { @Test void shouldRenderNodeDelete() { - Node n = anyNode().named("n"); - Relationship r = n.relationshipBetween(anyNode()).named("r0").create(); + Node n = anyNode("n"); + Relationship r = n.relationshipBetween(anyNode()).named("r0"); Statement statement = Cypher .match(n).where(n.internalId().isEqualTo(literalOf(4711))) .optional().match(r) @@ -761,4 +789,21 @@ public class CypherTest { "MATCH (n) WHERE id(n) = 4711 OPTIONAL MATCH (n)-[r0]-() DELETE r0, n"); } } + + @Nested + class Expressions { + @Test + void shouldRenderParameters() { + Statement statement; + statement = Cypher.match(userNode) + .where(userNode.property("a").isEqualTo(parameter("aParameter"))) + .detach().delete(userNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.a = $aParameter DETACH DELETE u RETURN u"); + } + } } From 1028f83412bf4cfa619493bdec1a140449cec716 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 14 May 2019 16:03:19 +0200 Subject: [PATCH 075/342] Decouple mapping and NodeManager, add support for PartTree queries. The gist of this change is as follows: - The schema knows everything about how to query nodes and relationships - Repositories etc. query the schema for mapping functions and Cypher statements - Those are than combined into a prepared query - The prepared query is turned into an executable query by the nodemanager, that adds nodes to the context as needed --- etc/jqassistant/structure.adoc | 5 +- .../data/neo4j/core/DefaultNeo4jClient.java | 8 +- .../data/neo4j/core/DefaultNodeManager.java | 77 ++-- .../data/neo4j/core/DefaultPreparedQuery.java | 73 ++++ ...elegatingMappingFunctionWithNullCheck.java | 3 + .../data/neo4j/core/ExecutableQuery.java | 54 +++ .../data/neo4j/core/NamedParameters.java | 23 +- .../data/neo4j/core/Neo4jClient.java | 3 +- .../data/neo4j/core/NoResultException.java | 37 ++ .../data/neo4j/core/NodeManager.java | 12 +- .../neo4j/core/NonUniqueResultException.java | 37 ++ .../data/neo4j/core/PersistenceException.java | 68 ++++ .../data/neo4j/core/PreparedQuery.java | 102 +++++ .../context/DefaultPersistenceContext.java | 20 +- .../core/context/PersistenceContext.java | 6 +- .../tracking/EntityComparisonStrategy.java | 2 +- .../tracking/EntityTrackingStrategy.java | 4 +- .../cypher/Asterisk.java} | 22 +- .../neo4j/core/cypher/BooleanLiteral.java | 44 ++ .../data/neo4j/core/cypher/Conditions.java | 67 ++++ .../data/neo4j/core/cypher/Cypher.java | 49 ++- .../core/cypher/DefaultStatementBuilder.java | 378 ++++++++++++++---- .../data/neo4j/core/cypher/Expression.java | 88 +++- .../data/neo4j/core/cypher/Functions.java | 7 + .../data/neo4j/core/cypher/Match.java | 2 +- .../neo4j/core/cypher/MultiPartElement.java | 58 +++ .../neo4j/core/cypher/MultiPartQuery.java | 54 +++ .../data/neo4j/core/cypher/Operation.java | 63 +++ .../data/neo4j/core/cypher/Operations.java | 49 +++ .../data/neo4j/core/cypher/Operator.java | 47 +++ .../data/neo4j/core/cypher/Parameter.java | 14 +- .../data/neo4j/core/cypher/Property.java | 32 +- .../data/neo4j/core/cypher/ReadingClause.java | 16 - .../data/neo4j/core/cypher/Return.java | 33 +- .../data/neo4j/core/cypher/ReturnBody.java | 57 +++ .../neo4j/core/cypher/SinglePartQuery.java | 49 +-- .../neo4j/core/cypher/StatementBuilder.java | 106 +++-- .../data/neo4j/core/cypher/With.java | 61 +++ .../neo4j/core/cypher/renderer/Renderer.java | 6 + .../cypher/renderer/RenderingVisitor.java | 32 +- .../core/mapping/Neo4jMappingContext.java | 11 + .../data/neo4j/core/schema/Schema.java | 42 +- .../core/schema/UnknownEntityException.java | 43 ++ ...Neo4jRepositoryConfigurationExtension.java | 10 +- .../repository/query/AbstractNeo4jQuery.java | 62 ++- .../neo4j/repository/query/CountQuery.java | 4 + .../repository/query/CypherQueryCreator.java | 160 ++++++++ .../neo4j/repository/query/ExistsQuery.java | 4 + .../repository/query/Neo4jQueryExecution.java | 56 +++ .../query/Neo4jQueryLookupStrategy.java | 82 ++++ .../repository/query/Neo4jQueryMethod.java | 86 ++-- .../repository/query/PartTreeNeo4jQuery.java | 66 ++- .../data/neo4j/repository/query/Query.java | 9 + .../query/StringBasedNeo4jQuery.java | 68 +++- .../MappingNeo4jEntityInformation.java | 42 -- .../support/Neo4jRepositoryFactory.java | 47 +-- .../support/Neo4jRepositoryFactoryBean.java | 9 +- .../support/SimpleNeo4jRepository.java | 138 +++++-- .../DefaultPersistenceContextTest.java | 41 +- .../EntityComparisonStrategyTest.java | 12 +- .../data/neo4j/core/cypher/CypherTest.java | 140 ++++++- .../cypher/DefaultStatementBuilderTest.java | 2 +- .../data/neo4j/core/cypher/FunctionsTest.java | 2 +- .../neo4j/integration/PersonRepository.java | 28 +- .../integration/PersonWithAllConstructor.java | 6 + .../data/neo4j/integration/RepositoryIT.java | 118 +++++- ...java => Neo4jQueryLookupStrategyTest.java} | 41 +- .../support/SimpleNeo4jRepositoryTest.java | 9 +- 68 files changed, 2700 insertions(+), 506 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ExecutableQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NoResultException.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NonUniqueResultException.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PersistenceException.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/{repository/query/Neo4jEntityInformation.java => core/cypher/Asterisk.java} (62%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanLiteral.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartElement.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/UnknownEntityException.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/MappingNeo4jEntityInformation.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/{Neo4jQueryMethodTest.java => Neo4jQueryLookupStrategyTest.java} (52%) diff --git a/etc/jqassistant/structure.adoc b/etc/jqassistant/structure.adoc index afb12264c..cb113db04 100644 --- a/etc/jqassistant/structure.adoc +++ b/etc/jqassistant/structure.adoc @@ -5,11 +5,12 @@ Most of the time, the package structure under `org.springframework.data.neo4j` s [[structure:mapping]] [source,cypher,role=constraint,requiresConcepts="dependency:Package"] -.The mapping package must not depend on any other SDN-RX packages than `schema` +.The mapping package must not depend on any other SDN-RX packages than `schema` and its transitive dependencies ---- MATCH (a:Main:Artifact) +MATCH (a) -[:CONTAINS]-> (s:Package) WHERE s.fqn = 'org.springframework.data.neo4j.core.schema' MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a) WHERE p1.fqn = 'org.springframework.data.neo4j.core.mapping' - AND NOT ((p1) -[:CONTAINS]-> (p2) OR p2.fqn = 'org.springframework.data.neo4j.core.schema') + AND NOT (p2 = s OR (s) -[:DEPENDS_ON]-> (p2) OR (p1) -[:CONTAINS]-> (p2)) return p1,p2 ---- diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index 00fdbd23b..267b942f0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -246,9 +246,15 @@ class DefaultNeo4jClient implements Neo4jClient { private StatementResult runWith(AutoCloseableStatementRunner statementRunner) { String statementTemplate = cypherSupplier.get(); + if (cypherLog.isDebugEnabled()) { - cypherLog.debug(statementTemplate); + cypherLog.debug("Executing:{}{}", System.lineSeparator(), statementTemplate); + + if (cypherLog.isTraceEnabled() && !parameters.isEmpty()) { + cypherLog.trace("with parameters:{}{}", System.lineSeparator(), parameters); + } } + StatementResult result = statementRunner.run(statementTemplate, parameters.get()); return result; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java index b8c762ce0..0792accaf 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java @@ -18,12 +18,19 @@ */ package org.springframework.data.neo4j.core; +import static java.util.stream.Collectors.*; + +import lombok.RequiredArgsConstructor; + import java.util.Collection; +import java.util.List; import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.driver.Transaction; +import org.neo4j.driver.exceptions.NoSuchRecordException; import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; +import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; import org.springframework.data.neo4j.core.context.DefaultPersistenceContext; import org.springframework.data.neo4j.core.context.PersistenceContext; import org.springframework.data.neo4j.core.schema.NodeDescription; @@ -52,7 +59,7 @@ class DefaultNodeManager implements NodeManager { this.neo4jClient = neo4jClient; this.transaction = transaction; - this.persistenceContext = new DefaultPersistenceContext(schema); + this.persistenceContext = new DefaultPersistenceContext(); } @Override @@ -62,36 +69,25 @@ class DefaultNodeManager implements NodeManager { } @Override - public Object executeQuery(String query) { + public ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery) { - return neo4jClient.newQuery(query).fetch().all(); - } - - @Override - public Optional executeTypedQueryForObject(String query, Class returnedType) { - - MappingSpec, Collection, T> mappingSpec = neo4jClient.newQuery(query).fetchAs(returnedType); - return schema.getMappingFunctionFor(returnedType) + Class resultType = preparedQuery.getResultType(); + MappingSpec, Collection, T> mappingSpec = neo4jClient.newQuery(preparedQuery.getCypherQuery()) + .bindAll(preparedQuery.getParameters()) + .fetchAs(resultType); + RecordFetchSpec, Collection, T> fetchSpec = preparedQuery.getOptionalMappingFunction() .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) - .orElse(mappingSpec) - .one(); - } + .orElse(mappingSpec); - @Override - public Collection executeTypedQueryForObjects(String query, Class returnedType) { - - MappingSpec, Collection, T> mappingSpec = neo4jClient.newQuery(query).fetchAs(returnedType); - return schema.getMappingFunctionFor(returnedType) - .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) - .orElse(mappingSpec) - .all(); + return new DefaultExecutableQuery(preparedQuery, schema.getNodeDescription(resultType), fetchSpec); } @Override public T save(T entityWithUnknownState) { // TODO if already registered, here or in the context? - this.persistenceContext.register(entityWithUnknownState); + this.persistenceContext + .register(entityWithUnknownState, schema.getRequiredNodeDescription(entityWithUnknownState.getClass())); throw new UnsupportedOperationException("Not there yet."); } @@ -104,12 +100,37 @@ class DefaultNodeManager implements NodeManager { throw new UnsupportedOperationException("Not there yet."); } - @Override - public NodeDescription describe(Class clazz) { + @RequiredArgsConstructor + class DefaultExecutableQuery implements ExecutableQuery { - // @formatter:off - return schema.getNodeDescription(clazz) - .orElseThrow(() -> new IllegalArgumentException(String.format("%s is not a managed class", clazz.getName()))); - // @formatter:on + private final PreparedQuery preparedQuery; + private final Optional> optionalNodeDescription; + private final RecordFetchSpec, Collection, T> fetchSpec; + + @Override + public List getResults() { + return fetchSpec.all().stream().map(this::register).collect(toList()); + } + + @Override + public Optional getSingleResult() { + try { + return fetchSpec.one().map(this::register); + } catch (NoSuchRecordException e) { + return Optional.empty(); + } + } + + @Override + public T getRequiredSingleResult() { + return fetchSpec.one().map(this::register) + .orElseThrow(() -> new NoResultException(1L, preparedQuery.getCypherQuery())); + } + + private T register(T entity) { + this.optionalNodeDescription.ifPresent( + nodeDescription -> DefaultNodeManager.this.persistenceContext.register(entity, nodeDescription)); + return entity; + } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java new file mode 100644 index 000000000..eb66a2079 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import org.neo4j.driver.Record; +import org.springframework.lang.Nullable; + +/** + * Typed preparation of a query that is used to create an {@link org.springframework.data.neo4j.core.ExecutableQuery} of the same type. + *

+ * When no mapping function is provided, the node manager will assume a simple type to be returned. Otherwise make sure + * that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected + * by the mapping function to work correctly. + * + * @param The type of the objects returned by this query. + * @author Michael J. Simons + * @soundtrack Deichkind - Noch fünf Minuten Mutti + * @since 1.0 + */ +final class DefaultPreparedQuery implements PreparedQuery { + + private final Class resultType; + private final String cypherQuery; + private final Map parameters; + private final @Nullable Function mappingFunction; + + DefaultPreparedQuery(OptionalBuildSteps optionalBuildSteps) { + this.resultType = optionalBuildSteps.resultType; + this.mappingFunction = (Function) optionalBuildSteps.mappingFunction; + this.cypherQuery = optionalBuildSteps.cypherQuery; + this.parameters = optionalBuildSteps.parameters; + } + + @Override + public Class getResultType() { + return this.resultType; + } + + @Override + public Optional> getOptionalMappingFunction() { + return Optional.ofNullable(mappingFunction); + } + + @Override + public String getCypherQuery() { + return this.cypherQuery; + } + + @Override + public Map getParameters() { + return this.parameters; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java index 289a6bba9..6f64e2006 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java @@ -25,6 +25,9 @@ import org.neo4j.driver.Record; /** * A delegating mapping function that first calls the delegate to get a record map and than checks the returned * value for {@literal null} and in the case of a null value, an {@link IllegalStateException} will be thrown. + *

+ * This class has been introduced instead of {@code Function#andThen} notion to be able throw a decent exception + * containing some information about the delegate used and which record was problematic. * * @param The expected type of this function * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ExecutableQuery.java new file mode 100644 index 000000000..74a23c2ac --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ExecutableQuery.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import java.util.List; +import java.util.Optional; + +import org.apiguardian.api.API; + +/** + * Interface for controlling query execution. + * + * @param The type of the objects returned by this query. + * @author Michael J. Simons + * @soundtrack Deichkind - Niveau weshalb warum + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public interface ExecutableQuery { + + /** + * @return All results returned by this query. + */ + List getResults(); + + /** + * @return A single result + * @throws NonUniqueResultException + */ + Optional getSingleResult(); + + /** + * @return A single result + * @throws NonUniqueResultException + * @throws NoResultException + */ + T getRequiredSingleResult(); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java index ea4fd795f..116d6759f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; import org.apiguardian.api.API; @@ -38,13 +39,10 @@ final class NamedParameters { * Adds all of the values contained in {@code newParameters} to this list of named parameters. * * @param newParameters Additional parameters to add - * @return This object * @throws IllegalStateException when any value in {@code newParameters} exists under the same name in the current parameters. */ - NamedParameters addAll(Map newParameters) { + void addAll(Map newParameters) { newParameters.forEach(this::add); - - return this; } /** @@ -52,10 +50,9 @@ final class NamedParameters { * * @param name The name of the new parameter * @param value The value of the new parameter - * @return This object * @throws IllegalStateException when a parameter with the given name already exists */ - NamedParameters add(String name, Object value) { + void add(String name, Object value) { if (this.parameters.containsKey(name)) { Object previousValue = this.parameters.get(name); @@ -67,7 +64,6 @@ final class NamedParameters { )); } this.parameters.put(name, value); - return this; } /** @@ -76,4 +72,17 @@ final class NamedParameters { Map get() { return Collections.unmodifiableMap(parameters); } + + public boolean isEmpty() { + return parameters.isEmpty(); + } + + @Override + public String toString() { + return parameters + .entrySet() + .stream() + .map(e -> String.format("%s: %s", e.getKey(), e.getValue())) + .collect(Collectors.joining(",", "params {", "}")); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java index 7c0525282..817f4dd2d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -181,10 +181,9 @@ public interface Neo4jClient { interface RecordFetchSpec { /** - * Fetches exactly one record and throws an exception if there are zero or more than one records. + * Fetches exactly one record and throws an exception if there are more entries. * * @return The one and only record. - * @throws org.neo4j.driver.exceptions.NoSuchRecordException */ S one(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NoResultException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NoResultException.java new file mode 100644 index 000000000..f26205a98 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NoResultException.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSizeException; + +/** + * Throw when a query doesn't return a required result. + * + * @author Michael J. Simons + * @soundtrack Deichkind - Niveau weshalb warum + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public class NoResultException extends IllegalResultSizeException { + + NoResultException(long expectedNumberOfResults, String query) { + super(expectedNumberOfResults, 0L, query); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java index 06e76f392..38c30e96b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java @@ -18,12 +18,8 @@ */ package org.springframework.data.neo4j.core; -import java.util.Collection; -import java.util.Optional; - import org.apiguardian.api.API; import org.neo4j.driver.Transaction; -import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.lang.Nullable; /** @@ -48,11 +44,7 @@ public interface NodeManager { @Nullable Transaction getTransaction(); - Object executeQuery(String query); - - Collection executeTypedQueryForObjects(String query, Class returnedType); - - Optional executeTypedQueryForObject(String query, Class returnedType); + ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery); /** * Saves an entity. When the entity is not yet managed in this instance of the NodeManager, and will be registered as @@ -71,6 +63,4 @@ public interface NodeManager { * @param managedEntity Object to be removed */ void delete(Object managedEntity); - - NodeDescription describe(Class clazz); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NonUniqueResultException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NonUniqueResultException.java new file mode 100644 index 000000000..3bd6174d8 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NonUniqueResultException.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSizeException; + +/** + * Throw when a query doesn't return a unique result. + * + * @author Michael J. Simons + * @soundtrack Deichkind - Niveau weshalb warum + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public class NonUniqueResultException extends IllegalResultSizeException { + + public NonUniqueResultException(String query, long actualNumberOfResults) { + super(1L, actualNumberOfResults, query); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PersistenceException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PersistenceException.java new file mode 100644 index 000000000..2a192b794 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PersistenceException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import org.apiguardian.api.API; + +/** + * Shared base class for exceptions during persistence operations of a {@link NodeManager}. + * + * @author Michael J. Simons + * @soundtrack Deichkind - Niveau weshalb warum + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public abstract class PersistenceException extends RuntimeException { + + protected PersistenceException(String message) { + super(message); + } + + protected PersistenceException(String message, Throwable cause) { + super(message, cause); + } + + abstract static class IllegalResultSizeException extends PersistenceException { + + private final long expectedNumberOfResults; + private final long actualNumberOfResults; + private final String query; + + IllegalResultSizeException(long expectedNumberOfResults, long actualNumberOfResults, String query) { + + super(String.format("Expected %d results, got %d", expectedNumberOfResults, actualNumberOfResults)); + + this.query = query; + this.expectedNumberOfResults = expectedNumberOfResults; + this.actualNumberOfResults = actualNumberOfResults; + } + + public long getExpectedNumberOfResults() { + return expectedNumberOfResults; + } + + public long getActualNumberOfResults() { + return actualNumberOfResults; + } + + public String getQuery() { + return query; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java new file mode 100644 index 000000000..7bb72f69a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.neo4j.driver.Record; +import org.springframework.lang.Nullable; + +/** + * @param The type of the objects returned by this query. + * @author Michael J. Simons + * @soundtrack Deichkind - Arbeit nervt + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public interface PreparedQuery { + + static RequiredBuildStep queryFor(Class resultType) { + return new RequiredBuildStep(resultType); + } + + Class getResultType(); + + Optional> getOptionalMappingFunction(); + + String getCypherQuery(); + + Map getParameters(); + + /** + * @param The concrete type of this build step. + */ + class RequiredBuildStep { + private final Class resultType; + + private RequiredBuildStep(Class resultType) { + this.resultType = resultType; + } + + public OptionalBuildSteps withCypherQuery(String cypherQuery) { + return new OptionalBuildSteps<>(resultType, cypherQuery); + } + } + + /** + * @param The concrete type of this build step. + */ + class OptionalBuildSteps { + + final Class resultType; + final String cypherQuery; + Map parameters = Collections.emptyMap(); + @Nullable Function mappingFunction; + + OptionalBuildSteps(Class resultType, String cypherQuery) { + this.resultType = resultType; + this.cypherQuery = cypherQuery; + } + + /** + * This replaces the current parameters. + * + * @param newParameters The new parameters for the prepared query. + * @return This builder. + */ + public OptionalBuildSteps withParameters(Map newParameters) { + this.parameters = new HashMap<>(newParameters); + return this; + } + + public OptionalBuildSteps usingMappingFunction(@Nullable Function newMappingFunction) { + this.mappingFunction = newMappingFunction; + return this; + } + + public PreparedQuery build() { + return new DefaultPreparedQuery<>(this); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java index 60e6cb9d0..0034ff0a9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java @@ -32,7 +32,6 @@ import org.springframework.data.neo4j.core.context.tracking.EntityChangeEvent; import org.springframework.data.neo4j.core.context.tracking.EntityComparisonStrategy; import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy; import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.Schema; /** * TODO explain what the persistence context should do other than being the plural of EntityTrackingStrategy :D @@ -43,37 +42,28 @@ import org.springframework.data.neo4j.core.schema.Schema; @Slf4j public class DefaultPersistenceContext implements PersistenceContext { - /** - * The schema is required to find out which properties to track with the {@link #entityTrackingStrategy}. - */ - private final Schema schema; - private final EntityTrackingStrategy entityTrackingStrategy; private final Set registeredObjectIds = new HashSet<>(); - public DefaultPersistenceContext(final Schema schema) { - this.schema = schema; + public DefaultPersistenceContext() { this.entityTrackingStrategy = getEntityTrackingStrategy(); } @Override - public void register(Object entity) { + public void register(Object entity, NodeDescription description) { Objects.requireNonNull(entity, "Cannot track null-entity!"); int identityOfEntity = getIdentityOf(entity); if (registeredObjectIds.contains(identityOfEntity)) { - log.info("Object " + entity + " was already registered"); + log.debug("Object " + entity + " was already registered"); return; } - NodeDescription nodeDescription = schema.getNodeDescription(entity.getClass()) - .orElseThrow(() -> new IllegalStateException("Cannot track entity that has no schema entry!")); - registeredObjectIds.add(identityOfEntity); - entityTrackingStrategy.track(nodeDescription, entity); + entityTrackingStrategy.track(entity, description); } @Override @@ -81,7 +71,7 @@ public class DefaultPersistenceContext implements PersistenceContext { int identityOfEntity = getIdentityOf(managedEntity); if (!registeredObjectIds.contains(identityOfEntity)) { - log.warn("Cannot deregister " + managedEntity + " because it were never registered"); + log.info("Cannot deregister " + managedEntity + " because it were never registered"); return; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java index 17320a316..e5c42fe5e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core.context; import java.util.Collection; import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.schema.NodeDescription; /** * Represents the state of varies instances being tracked. Those include: @@ -37,11 +38,12 @@ import org.apiguardian.api.API; public interface PersistenceContext { /** - * Registers an entity within the context and starts tracking the entitys state. + * Registers an entity within the context and starts tracking the entities state. * * @param entity The entity to register + * @param description The schema description of the entities class */ - void register(Object entity); + void register(Object entity, NodeDescription description); /** * Removes an entity from this context and stops tracking the entitys state. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java index bd65b1c12..160af229d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java @@ -47,7 +47,7 @@ public class EntityComparisonStrategy implements EntityTrackingStrategy { private final Map statesOfEntities = new HashMap<>(); @Override - public void track(NodeDescription nodeDescription, Object entity) { + public void track(Object entity, NodeDescription nodeDescription) { statesOfEntities.put(getObjectIdentifier(entity), new EntityState(nodeDescription, entity, getDefaultObjectIdentifier())); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java index 44742e6ce..a569cf65d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java @@ -41,10 +41,10 @@ public interface EntityTrackingStrategy { * Register an entity in the entity tracking strategy. {@link NodeDescription} is needed to determine the fields that * are considered mapping relevant. * - * @param nodeDescription the "rich" entity's class description. * @param entity the object that should get tracked. + * @param nodeDescription the "rich" entity's class description. */ - void track(NodeDescription nodeDescription, Object entity); + void track(Object entity, NodeDescription nodeDescription); /** * Remove an entity from tracking. This method should get called after an entity was deleted to have a clean change diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Asterisk.java similarity index 62% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jEntityInformation.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Asterisk.java index 51e2c1b4c..f0b3f08c6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jEntityInformation.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Asterisk.java @@ -16,17 +16,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.springframework.data.neo4j.core.cypher; -import org.springframework.data.repository.core.EntityInformation; +import org.apiguardian.api.API; /** - * Neo4j specific {@link EntityInformation} + * The {@code *}. * - * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 */ -public interface Neo4jEntityInformation extends EntityInformation { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Asterisk extends Literal { - String getIdPropertyName(); + public static final Asterisk INSTANCE = new Asterisk(); + private Asterisk() { + super("*"); + } + + @Override + public String asString() { + return super.getContent(); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanLiteral.java new file mode 100644 index 000000000..1e0469550 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanLiteral.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; + +/** + * The boolean literal. + * + * @author Michael J. Simons + * @soundtrack Bad Religion - Age Of Unreason + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class BooleanLiteral extends Literal { + + static BooleanLiteral TRUE = new BooleanLiteral(true); + static BooleanLiteral FALSE = new BooleanLiteral(false); + + private BooleanLiteral(boolean content) { + super(content); + } + + @Override + public String asString() { + return getContent().toString(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java index 70a20a35c..8901e298a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java @@ -44,6 +44,28 @@ public final class Conditions { return Comparison.create(lhs, "=~", rhs); } + /** + * Creates a condition that matches if both expressions are equals according to {@code =}. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return An "equals" comparision + */ + static Condition isEqualTo(Expression lhs, Expression rhs) { + return Comparison.create(lhs, "=", rhs); + } + + /** + * Creates a condition that matches if both expressions are equals according to {@code <>}. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return An "not equals" comparision + */ + static Condition isNotEqualTo(Expression lhs, Expression rhs) { + return Comparison.create(lhs, "<>", rhs); + } + /** * Negates the given condition. * @@ -56,6 +78,39 @@ public final class Conditions { return condition.not(); } + /** + * Creates a condition that checks whether the {@code lhs} starts with the {@code rhs}. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return A new condition. + */ + static Condition startsWith(Expression lhs, Expression rhs) { + return Comparison.create(lhs, "STARTS WITH", rhs); + } + + /** + * Creates a condition that checks whether the {@code lhs} contains with the {@code rhs}. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return A new condition. + */ + static Condition contains(Expression lhs, Expression rhs) { + return Comparison.create(lhs, "CONTAINS", rhs); + } + + /** + * Creates a condition that checks whether the {@code lhs} ends with the {@code rhs}. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return A new condition. + */ + static Condition endsWith(Expression lhs, Expression rhs) { + return Comparison.create(lhs, "ENDS WITH", rhs); + } + /** * Creates a placeholder condition which is not rendered in the final statement but is usefull * while chaining conditions together. @@ -67,11 +122,23 @@ public final class Conditions { return CompoundCondition.empty(); } + /** + * Creates a condition that checks whether the {@code expression} is {@literal null}. + * + * @param expression The expression to check for {@literal null} + * @return A new condition. + */ static Condition isNull(Expression expression) { return IsNull.create(expression, false); } + /** + * Creates a condition that checks whether the {@code expression} is not {@literal null}. + * + * @param expression The expression to check for {@literal null} + * @return A new condition. + */ static Condition isNotNull(Expression expression) { return IsNull.create(expression, true); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index bc8522120..d8279eea4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -55,6 +55,13 @@ public final class Cypher { return Node.create(); } + /** + * @return The {@code *} wildcard literal. + */ + public static Asterisk asterisk() { + return Asterisk.INSTANCE; + } + /** * @return A node matching any node with the symbolic the given {@code symbolicName}. */ @@ -62,23 +69,37 @@ public final class Cypher { return Node.create().named(symbolicName); } + public static Property property(String containerName, String name) { + return property(symbolicName(containerName), name); + } + + public static Property property(SymbolicName containerName, String name) { + return Property.create(containerName, name); + } + + public static SymbolicName symbolicName(String value) { + return new SymbolicName(value); + } + /** - * Creates a new parameter placeholder. - * @param name - * @return + * Creates a new parameter placeholder. Existing $-signs will be removed. + * + * @param name The name of the parameter, must not be null + * @return The new parameter */ public static Parameter parameter(String name) { - return new Parameter(name); + return Parameter.create(name); } /** * Prepares an optional match statement. * - * @return An optional match without any patterns yet. + * @param pattern The patterns to match + * @return An ongoing match that is used to specify an optional where and a required return clause */ - public static ExposesMatch optional() { + public static ExposesMatch optionalMatch(PatternElement... pattern) { - return Statement.builder().optional(); + return Statement.builder().optionalMatch(pattern); } /** @@ -140,6 +161,20 @@ public final class Cypher { return new ObjectLiteral(object); } + /** + * @return The {@literal true} literal. + */ + public static Literal literalTrue() { + return BooleanLiteral.TRUE; + } + + /** + * @return The {@literal false} literal. + */ + public static Literal literalFalse() { + return BooleanLiteral.FALSE; + } + /** * Not to be instantiated. */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index f9e6fcc14..5362fb301 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -22,14 +22,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.springframework.data.neo4j.core.cypher.ReadingClause.MatchList; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithWhere; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; +import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.util.Assert; /** @@ -44,35 +41,40 @@ class DefaultStatementBuilder OngoingMatchWithoutWhere { /** - * Current list of matches to be generated. + * Current list of reading or update clauses to be generated. */ - private final List matchesList = new ArrayList<>(); + private final List currentSinglePartElements = new ArrayList<>(); + /** - * Will the next match generated be optional? + * The latest ongoing match. */ - private final AtomicBoolean nextMatchShouldBeOptional = new AtomicBoolean(false); + private DefaultMatchBuilder currentOngoingMatch; + /** - * The latest ongoing match, + * A list of already build withs. */ - private DefaultOngoingMatch currentOngoingMatch; + private final List multiPartElements = new ArrayList<>(); @Override - public ExposesMatch optional() { + public OngoingMatchWithoutWhere optionalMatch(PatternElement... pattern) { - nextMatchShouldBeOptional.set(true); - return this; + return this.match(true, pattern); } @Override public OngoingMatchWithoutWhere match(PatternElement... pattern) { + return this.match(false, pattern); + } + + private OngoingMatchWithoutWhere match(boolean optional, PatternElement... pattern) { + Assert.notNull(pattern, "Patterns to match are required."); Assert.notEmpty(pattern, "At least one pattern to match is required."); - if (this.currentOngoingMatch != null) { - this.matchesList.add(this.currentOngoingMatch); + this.currentSinglePartElements.add(this.currentOngoingMatch.buildMatch()); } - this.currentOngoingMatch = new DefaultOngoingMatch(this.nextMatchShouldBeOptional.getAndSet(false)); + this.currentOngoingMatch = new DefaultMatchBuilder(optional); this.currentOngoingMatch.matchList.addAll(Arrays.asList(pattern)); return this; } @@ -80,68 +82,115 @@ class DefaultStatementBuilder @Override public OngoingMatchAndReturn returning(Expression... expressions) { - DefaultStatementWithReturnBuilder ongoingMatchAndReturn = new DefaultStatementWithReturnBuilder(); + return returning(false, expressions); + } + + @Override + public OngoingMatchAndReturn returningDistinct(Expression... expressions) { + return returning(true, expressions); + } + + private OngoingMatchAndReturn returning(boolean distinct, Expression... expressions) { + + DefaultStatementWithReturnBuilder ongoingMatchAndReturn = new DefaultStatementWithReturnBuilder(distinct); ongoingMatchAndReturn.addExpressions(expressions); return ongoingMatchAndReturn; } @Override - public OngoingDetachDelete detach() { + public OngoingMatchAndWithWithoutWhere with(Expression... expressions) { - return new DefaultStatementWithDeleteBuilder(true); + return with(false, expressions); + } + + @Override + public OngoingMatchAndWithWithoutWhere withDistinct(Expression... expressions) { + + return with(true, expressions); + } + + private OngoingMatchAndWithWithoutWhere with(boolean distinct, Expression... expressions) { + + DefaultStatementWithWithBuilder ongoingMatchAndWith = new DefaultStatementWithWithBuilder(distinct); + ongoingMatchAndWith.addExpressions(expressions); + return ongoingMatchAndWith; } @Override public OngoingMatchAndDelete delete(Expression... expressions) { - return new DefaultStatementWithDeleteBuilder(false).delete(expressions); + return new DefaultStatementWithDeleteBuilder(false, expressions); + } + + @Override + public OngoingMatchAndDelete detachDelete(Expression... expressions) { + + return new DefaultStatementWithDeleteBuilder(true, expressions); } @Override public OngoingMatchWithWhere where(Condition newCondition) { - this.currentOngoingMatch.where(newCondition); + this.currentOngoingMatch.conditionBuilder.where(newCondition); return this; } @Override public OngoingMatchWithWhere and(Condition additionalCondition) { - this.currentOngoingMatch.and(additionalCondition); + this.currentOngoingMatch.conditionBuilder.and(additionalCondition); return this; } @Override public OngoingMatchWithWhere or(Condition additionalCondition) { - this.currentOngoingMatch.or(additionalCondition); + this.currentOngoingMatch.conditionBuilder.or(additionalCondition); return this; } - protected MatchList buildMatchList() { - List matchList = Stream.concat(this.matchesList.stream(), - this.currentOngoingMatch == null ? Stream.empty() : Stream.of(this.currentOngoingMatch)) - .map(DefaultOngoingMatch::buildMatch) - .collect(Collectors.toList()); - return new MatchList(matchList); + protected final List buildMatchList() { + List completeMatchesList = new ArrayList(this.currentSinglePartElements); + if (this.currentOngoingMatch != null) { + completeMatchesList.add(this.currentOngoingMatch.buildMatch()); + } + this.currentOngoingMatch = null; + this.currentSinglePartElements.clear(); + return completeMatchesList; } - class DefaultStatementWithReturnBuilder + protected final DefaultStatementBuilder addWith(Optional optionalWith) { + + optionalWith.ifPresent(with -> { + multiPartElements.add(new MultiPartElement(buildMatchList(), with)); + }); + return this; + } + + protected final DefaultStatementBuilder addUpdatingClause(UpdatingClause updatingClause) { + + // Close current match + if (this.currentOngoingMatch != null) { + this.currentSinglePartElements.add(this.currentOngoingMatch.buildMatch()); + this.currentOngoingMatch = null; + } + + this.currentSinglePartElements.add(updatingClause); + return this; + } + + protected class DefaultStatementWithReturnBuilder implements OngoingMatchAndReturn, OngoingOrderDefinition, OngoingMatchAndReturnWithOrder { - private final List returnList = new ArrayList<>(); - private final List sortItemList = new ArrayList<>(); - private SortItem lastSortItem; - private Skip skip; - private Limit limit; + protected final List returnList = new ArrayList<>(); + protected final List sortItemList = new ArrayList<>(); + protected boolean distinct; + protected SortItem lastSortItem; + protected Skip skip; + protected Limit limit; - protected final DefaultStatementWithReturnBuilder addExpressions(Expression... expressions) { - - Assert.notNull(expressions, "Expressions to return are required."); - Assert.notEmpty(expressions, "At least one expressions to return is required."); - - this.returnList.addAll(Arrays.asList(expressions)); - return this; + protected DefaultStatementWithReturnBuilder(boolean distinct) { + this.distinct = distinct; } @Override @@ -187,6 +236,25 @@ class DefaultStatementBuilder return this; } + @Override + public Statement build() { + SinglePartQuery singlePartQuery = SinglePartQuery.create(buildMatchList(), buildReturn().orElse(null)); + + if (multiPartElements.isEmpty()) { + return singlePartQuery; + } else { + return new MultiPartQuery(multiPartElements, singlePartQuery); + } + } + + protected final void addExpressions(Expression... expressions) { + + Assert.notNull(expressions, "Expressions to return are required."); + Assert.notEmpty(expressions, "At least one expressions to return is required."); + + this.returnList.addAll(Arrays.asList(expressions)); + } + protected final Optional buildReturn() { if (returnList.isEmpty()) { @@ -199,38 +267,148 @@ class DefaultStatementBuilder sortItemList.add(lastSortItem); } Order order = sortItemList.size() > 0 ? new Order(sortItemList) : null; - return Optional.of(new Return(returnItems, order, skip, limit)); - } - - @Override - public Statement build() { - - MatchList matchList = buildMatchList(); - // This must be filled at this stage - Return aReturn = buildReturn().get(); - return SinglePartQuery.createReturningQuery(aReturn, matchList); + return Optional.of(new Return(distinct, returnItems, order, skip, limit)); } } - class DefaultStatementWithDeleteBuilder extends DefaultStatementWithReturnBuilder - implements OngoingDetachDelete, OngoingMatchAndDelete { + /** + * Adds support for With to a return builder. + */ + protected abstract class WithBuilderSupport extends DefaultStatementWithReturnBuilder { + protected final DefaultConditionBuilder conditionBuilder = new DefaultConditionBuilder(); - private final List deleteList = new ArrayList<>(); - private final boolean detach; + protected WithBuilderSupport(boolean distinct) { + super(distinct); + } - DefaultStatementWithDeleteBuilder(boolean detach) { - this.detach = detach; + protected final Optional buildWith() { + + if (returnList.isEmpty()) { + return Optional.empty(); + } + + ExpressionList returnItems = new ExpressionList(returnList); + + if (lastSortItem != null) { + sortItemList.add(lastSortItem); + } + Order order = sortItemList.size() > 0 ? new Order(sortItemList) : null; + Where where = conditionBuilder.buildCondition().map(Where::new).orElse(null); + + return Optional.of(new With(distinct, returnItems, order, skip, limit, where)); + } + } + + /** + * Ongoing with extends from {@link WithBuilderSupport} and therefore from {@Defaultd} + */ + protected final class DefaultStatementWithWithBuilder extends WithBuilderSupport + implements OngoingMatchAndWithWithoutWhere, OngoingMatchAndWithWithWhere { + + protected DefaultStatementWithWithBuilder(boolean distinct) { + super(distinct); + } + + @Override + public OngoingMatchAndReturn returning(Expression... expressions) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .returning(expressions); + } + + @Override + public OngoingMatchAndReturn returningDistinct(Expression... expressions) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .returning(expressions); } @Override public OngoingMatchAndDelete delete(Expression... expressions) { + return DefaultStatementBuilder.this + .addWith(buildWith()) + .delete(expressions); + } + + @Override + public OngoingMatchAndDelete detachDelete(Expression... expressions) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .detachDelete(expressions); + } + + @Override + public OngoingMatchAndWithWithoutWhere with(Expression... expressions) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .with(expressions); + } + + @Override + public OngoingMatchAndWithWithoutWhere withDistinct(Expression... expressions) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .withDistinct(expressions); + } + + @Override + public OngoingMatchAndWithWithWhere where(Condition newCondition) { + + super.conditionBuilder.where(newCondition); + return this; + } + + @Override + public OngoingMatchAndWithWithWhere and(Condition additionalCondition) { + + super.conditionBuilder.and(additionalCondition); + return this; + } + + @Override + public OngoingMatchAndWithWithWhere or(Condition additionalCondition) { + + super.conditionBuilder.or(additionalCondition); + return this; + } + + @Override + public OngoingMatchWithoutWhere match(PatternElement... pattern) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .match(pattern); + } + + @Override + public OngoingMatchWithoutWhere optionalMatch(PatternElement... pattern) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .optionalMatch(pattern); + } + } + + protected final class DefaultStatementWithDeleteBuilder extends WithBuilderSupport + implements OngoingMatchAndDelete { + + private final List deleteList; + private final boolean detach; + + protected DefaultStatementWithDeleteBuilder(boolean detach, Expression... expressions) { + super(false); + this.detach = detach; + Assert.notNull(expressions, "Expressions to delete are required."); Assert.notEmpty(expressions, "At least one expressions to delete is required."); - this.deleteList.addAll(Arrays.asList(expressions)); - - return this; + this.deleteList = Arrays.asList(expressions); } @Override @@ -243,35 +421,83 @@ class DefaultStatementBuilder return this; } - protected final Delete buildDelete() { + @Override + public OngoingMatchAndReturn returningDistinct(Expression... expressions) { - ExpressionList deleteItems = new ExpressionList(this.deleteList); - return new Delete(deleteItems, this.detach); + returning(expressions); + super.distinct = true; + return this; } @Override public Statement build() { - MatchList matchList = buildMatchList(); - Delete delete = buildDelete(); - Optional optionalReturn = buildReturn(); + DefaultStatementBuilder.this.addUpdatingClause(buildDelete()); + return super.build(); + } - return SinglePartQuery.createUpdatingQuery(matchList, delete, optionalReturn.orElse(null)); + @Override + public OngoingMatchAndDelete delete(Expression... expressions) { + return delete(false, expressions); + } + + @Override + public OngoingMatchAndDelete detachDelete(Expression... expressions) { + return delete(true, expressions); + } + + private OngoingMatchAndDelete delete(boolean nextDetach, Expression... expressions) { + DefaultStatementBuilder.this.addUpdatingClause(buildDelete()); + return DefaultStatementBuilder.this.new DefaultStatementWithDeleteBuilder(nextDetach, expressions); + } + + @Override + public OngoingMatchAndWithWithoutWhere with(Expression... expressions) { + return this.with(false, expressions); + } + + @Override + public OngoingMatchAndWithWithoutWhere withDistinct(Expression... expressions) { + return this.with(true, expressions); + } + + private OngoingMatchAndWithWithoutWhere with(boolean distinct, Expression... expressions) { + DefaultStatementBuilder.this.addUpdatingClause(buildDelete()); + return DefaultStatementBuilder.this + .addWith(buildWith()) + .with(distinct, expressions); + } + + private Delete buildDelete() { + + ExpressionList deleteItems = new ExpressionList(this.deleteList); + return new Delete(deleteItems, this.detach); } } - class DefaultOngoingMatch { + // Static builder and support classes + + static final class DefaultMatchBuilder { private final List matchList = new ArrayList<>(); + private final DefaultConditionBuilder conditionBuilder = new DefaultConditionBuilder(); + private final boolean optional; - private Condition condition; - - DefaultOngoingMatch(boolean optional) { + DefaultMatchBuilder(boolean optional) { this.optional = optional; } + Match buildMatch() { + Pattern pattern = new Pattern(this.matchList); + return new Match(optional, pattern, conditionBuilder.buildCondition().map(Where::new).orElse(null)); + } + } + + static final class DefaultConditionBuilder { + protected Condition condition; + void where(Condition newCondition) { this.condition = newCondition; @@ -287,14 +513,12 @@ class DefaultStatementBuilder this.condition = this.condition.or(additionalCondition); } - boolean hasCondition() { + private boolean hasCondition() { return !(this.condition == null || this.condition == CompoundCondition.EMPTY_CONDITION); } - Match buildMatch() { - Pattern pattern = new Pattern(this.matchList); - return new Match(optional, pattern, hasCondition() ? new Where(this.condition) : null); + Optional buildCondition() { + return hasCondition() ? Optional.of(this.condition) : Optional.empty(); } } - } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java index 3c58c6f7b..3dff7d00c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.cypher; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; + import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.util.Assert; @@ -44,17 +46,99 @@ public interface Expression extends Visitable { } default Condition isEqualTo(Expression rhs) { - return Comparison.create(this, "=", rhs); + return Conditions.isEqualTo(this, rhs); + } + + default Condition isNotEqualTo(Expression rhs) { + return Conditions.isNotEqualTo(this, rhs); + } + + /** + * Creates a condition that checks whether this {@code expression} is {@literal true}. + * + * @return A new condition + */ + default Condition isTrue() { + return Conditions.isEqualTo(this, literalTrue()); + } + + /** + * Creates a condition that checks whether this {@code expression} is {@literal false}. + * + * @return A new condition + */ + default Condition isFalse() { + return Conditions.isEqualTo(this, literalFalse()); + } + + /** + * Creates a condition that checks whether this {@code expression} matches that {@code expression}. + * + * @param expression The expression to match against. Must evaluate into a string during runtime. + * @return A new condition. + */ + default Condition matches(Expression expression) { + return Conditions.matches(this, expression); + } + + /** + * Creates a condition that checks whether this {@code expression} matches the given {@code pattern}. + * + * @param pattern The pattern to match + * @return A new condition. + */ + default Condition matches(String pattern) { + return Conditions.matches(this, Cypher.literalOf(pattern)); + } + + /** + * Creates a condition that checks whether this {@code expression} starts with that {@code expression}. + * + * @param expression The expression to match against. Must evaluate into a string during runtime. + * @return A new condition. + */ + default Condition startsWith(Expression expression) { + return Conditions.startsWith(this, expression); + } + + /** + * Creates a condition that checks whether this {@code expression} contains that {@code expression}. + * + * @param expression The expression to match against. Must evaluate into a string during runtime. + * @return A new condition. + */ + default Condition contains(Expression expression) { + return Conditions.contains(this, expression); + } + + /** + * Creates a condition that checks whether this {@code expression} ends with that {@code expression}. + * + * @param expression The expression to match against. Must evaluate into a string during runtime. + * @return A new condition. + */ + default Condition endsWith(Expression expression) { + return Conditions.endsWith(this, expression); + } + + /** + * Creates a {@code +} operation of this and that {@code expression}. + * + * @param expression The expression to add + * @return A new operation. + */ + default Operation plus(Expression expression) { + return Operations.plus(this, expression); } // WIP // TODO Make longs a list of expressions or something // WIP + default Condition isIn(Iterable ids) { return Comparison.create( this, "in", new ListLiteral(ids)); } - } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java index 71d43ab4e..9bd2bfec0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java @@ -60,6 +60,13 @@ public final class Functions { return new FunctionInvocation(F_ID, relationship); } + public static FunctionInvocation id(SymbolicName symbolicName) { + + Assert.notNull(symbolicName, "The symbolic name is required."); + + return new FunctionInvocation(F_ID, symbolicName); + } + /** * Creates a function invocation for the {@code count()} function. * See count. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java index b5c7397a2..5b1ae2adb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java @@ -30,7 +30,7 @@ import org.springframework.lang.Nullable; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class Match implements Visitable { +public final class Match implements ReadingClause { private final boolean optional; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartElement.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartElement.java new file mode 100644 index 000000000..ea954612d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartElement.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; + +/** + * @author Michael J. Simons + * @soundtrack Deichkind - Bitte ziehen Sie durch + * @since 1.0 + */ +class MultiPartElement implements Visitable { + private final List precedingClauses; + + private final With with; + + MultiPartElement(@Nullable List precedingClauses, With with) { + + if (precedingClauses == null || precedingClauses.isEmpty()) { + this.precedingClauses = Collections.emptyList(); + } else { + this.precedingClauses = new ArrayList<>(precedingClauses); + } + + this.with = with; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + precedingClauses.forEach(c -> c.accept(visitor)); + with.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java new file mode 100644 index 000000000..6ff99da6c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.ArrayList; +import java.util.List; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * See MultiPartQuery. + * + * @author Michael J. Simons + * @soundtrack Ferris MC - Ferris MC's Audiobiographie + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class MultiPartQuery implements SingleQuery { + + private final List parts; + + private final SinglePartQuery remainder; + + public MultiPartQuery(List parts, SinglePartQuery remainder) { + + this.parts = new ArrayList<>(parts); + this.remainder = remainder; + } + + @Override + public void accept(Visitor visitor) { + + parts.forEach(p -> p.accept(visitor)); + remainder.accept(visitor); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java new file mode 100644 index 000000000..e3d3a17e9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; + +/** + * A binary operation. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Operation implements Expression { + + static Operation create(Expression op1, Operator operator, Expression op2) { + + Assert.notNull(op1, "The first operand must not be null."); + Assert.notNull(operator, "Operator must not be empty."); + Assert.notNull(op2, "The second operand must not be null."); + + return new Operation(op1, operator, op2); + } + + private final Expression left; + private final Operator operator; + private final Expression right; + + private Operation(Expression left, Operator operator, Expression right) { + + this.left = left; + this.operator = operator; + this.right = right; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + left.accept(visitor); + operator.accept(visitor); + right.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java new file mode 100644 index 000000000..d899e605a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; + +/** + * A set of operations. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Operations { + + /** + * Creates a {@code +} operation. Both operands must evaluate to a compatible type. + * + * @param op1 first operand + * @param op2 second operation + * @return A new operation. + */ + static Operation plus(Expression op1, Expression op2) { + + return Operation.create(op1, Operator.PLUS, op2); + } + + /** + * Not to be instantiated. + */ + private Operations() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java new file mode 100644 index 000000000..79340c7bc --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * An operator + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public enum Operator implements Visitable { + + /** + * The {@code +} operator. + */ + PLUS("+"); + + private final String representation; + + Operator(String representation) { + this.representation = representation; + } + + public String getRepresentation() { + return representation; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java index 4999a6b2b..d7b97ce7b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; +import org.springframework.util.Assert; /** * Represents a named parameter inside a Cypher statement. @@ -31,7 +32,18 @@ public final class Parameter implements Expression { private final String name; - Parameter(String name) { + static Parameter create(String name) { + + Assert.hasText(name, "The name of the parameter is required!"); + + if (name.startsWith("$")) { + return create(name.substring(1)); + } + + return new Parameter(name); + } + + private Parameter(String name) { this.name = name; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java index 9dd250d4f..0383c916b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.util.Assert; /** @@ -36,36 +37,43 @@ public final class Property implements Expression { "A property derived from a node needs a parent with a symbolic name."); Assert.hasText(name, "The properties name is required."); - return new Property(parentContainer, name); + return new Property(parentContainer.getSymbolicName().get(), name); + } + + static Property create(SymbolicName containerName, String name) { + + Assert.notNull(containerName, "The property containers name is required."); + Assert.hasText(name, "The properties name is required."); + + return new Property(containerName, name); + } /** - * The property container this property belongs to. + * The symbolic name of the parent container. */ - private final Node parentContainer; + private final SymbolicName containerName; /** * The name of this property. */ private final String name; - Property(Node parentContainer, String name) { + Property(SymbolicName containerName, String name) { - this.parentContainer = parentContainer; + this.containerName = containerName; this.name = name; } - public String getParentAlias() { - return parentContainer.getSymbolicName().get().getName(); - } - public String getName() { return name; } - public Condition matches(String s) { - - return Conditions.matches(this, Cypher.literalOf(s)); + @Override + public void accept(Visitor visitor) { + visitor.enter(this); + this.containerName.accept(visitor); + visitor.leave(this); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java index 8a8c83c7e..b6a8cef73 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java @@ -18,27 +18,11 @@ */ package org.springframework.data.neo4j.core.cypher; -import java.util.List; - import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; /** * @author Michael J. Simons * @since 1.0 */ interface ReadingClause extends Visitable { - - class MatchList implements ReadingClause { - private final List matchList; - - MatchList(List matchList) { - this.matchList = matchList; - } - - @Override - public void accept(Visitor visitor) { - matchList.forEach(match -> match.accept(visitor)); - } - } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java index ac33c6660..b7ca97d8b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java @@ -23,9 +23,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** - * The container or "body" for return items, order and optional skip and things. - * - * See ReturnBody. + * See Return. * * @author Michael J. Simons * @since 1.0 @@ -33,32 +31,23 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; @API(status = API.Status.INTERNAL, since = "1.0") public final class Return implements Visitable { - private final ExpressionList returnItems; + private final boolean distinct; - private final Order order; - private final Skip skip; - private final Limit limit; + private final ReturnBody body; - Return(ExpressionList returnItems, Order order, Skip skip, Limit limit) { - this.returnItems = returnItems; - this.order = order; - this.skip = skip; - this.limit = limit; + Return(boolean distinct, ExpressionList returnItems, Order order, Skip skip, Limit limit) { + this.distinct = distinct; + this.body = new ReturnBody(returnItems, order, skip, limit); + } + + public boolean isDistinct() { + return distinct; } @Override public void accept(Visitor visitor) { visitor.enter(this); - returnItems.accept(visitor); - if (order != null) { - order.accept(visitor); - } - if (skip != null) { - skip.accept(visitor); - } - if (limit != null) { - limit.accept(visitor); - } + this.body.accept(visitor); visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java new file mode 100644 index 000000000..ed75ee687 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; + +/** + * The container or "body" for return items, order and optional skip and things. + * See ReturnBody + * + * @author Michael J. Simons + * @soundtrack Ferris MC - Ferris MC's Audiobiographie + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class ReturnBody implements Visitable { + + private final ExpressionList returnItems; + + @Nullable private final Order order; + @Nullable private final Skip skip; + @Nullable private final Limit limit; + + ReturnBody(ExpressionList returnItems, @Nullable Order order, @Nullable Skip skip, @Nullable Limit limit) { + this.returnItems = returnItems; + this.order = order; + this.skip = skip; + this.limit = limit; + } + + @Override + public void accept(Visitor visitor) { + returnItems.accept(visitor); + Visitable.visitIfNotNull(order, visitor); + Visitable.visitIfNotNull(skip, visitor); + Visitable.visitIfNotNull(limit, visitor); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java index 76ee4a295..20a1c02bd 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java @@ -18,6 +18,9 @@ */ package org.springframework.data.neo4j.core.cypher; +import java.util.ArrayList; +import java.util.List; + import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; import org.springframework.data.neo4j.core.cypher.support.Visitable; @@ -34,55 +37,29 @@ import org.springframework.util.Assert; @API(status = API.Status.INTERNAL, since = "1.0") public final class SinglePartQuery implements SingleQuery { - private @Nullable final ReadingClause readingClause; + private final List precedingClauses; - private @Nullable final UpdatingClause updatingClause; + @Nullable private final Return aReturn; - private @Nullable final Return aReturn; + static SinglePartQuery create(List precedingClauses, @Nullable Return aReturn) { - /** - * Creates a single part query consisting of a return clause with an optional reading clause - * - * @param aReturn The expressions to return - * @param readingClause The optional reading clause - * @return - */ - static SinglePartQuery createReturningQuery(Return aReturn, @Nullable ReadingClause readingClause) { + if (precedingClauses.isEmpty() || precedingClauses.get(precedingClauses.size() - 1) instanceof Match) { + Assert.notNull(aReturn, "A return clause is required."); + } - Assert.notNull(aReturn, "A return clause is required."); - - return new SinglePartQuery(readingClause, null, aReturn); + return new SinglePartQuery(precedingClauses, aReturn); } - /** - * Creates a single part query representing an update of some kind. Updates have an optional return clause. - * - * @param readingClause The expressions to match - * @param updatingClause The expressions to update - * @param aReturn The expressions to return, optional - * @return - */ - static SinglePartQuery createUpdatingQuery(ReadingClause readingClause, UpdatingClause updatingClause, - @Nullable Return aReturn) { + private SinglePartQuery(List precedingClauses, @Nullable Return aReturn) { - Assert.notNull(readingClause, "The reading clause is required."); - Assert.notNull(updatingClause, "The update clause is required."); - - return new SinglePartQuery(readingClause, updatingClause, aReturn); - } - - private SinglePartQuery(@Nullable ReadingClause readingClause, @Nullable UpdatingClause updatingClause, - @Nullable Return aReturn) { - this.readingClause = readingClause; - this.updatingClause = updatingClause; + this.precedingClauses = new ArrayList(precedingClauses); this.aReturn = aReturn; } @Override public void accept(Visitor visitor) { - Visitable.visitIfNotNull(readingClause, visitor); - Visitable.visitIfNotNull(updatingClause, visitor); + precedingClauses.forEach(c -> c.accept(visitor)); Visitable.visitIfNotNull(aReturn, visitor); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index 004ba50e1..4b0b8bf64 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -29,11 +29,11 @@ import org.apiguardian.api.API; public interface StatementBuilder { /** - * See {@link Cypher#optional()}. + * See {@link Cypher#optionalMatch(PatternElement...)}. * * @return */ - ExposesMatch optional(); + OngoingMatchWithoutWhere optionalMatch(PatternElement... pattern); /** * See {@link Cypher#match(PatternElement...)}. @@ -50,13 +50,6 @@ public interface StatementBuilder { */ interface OngoingMatchWithoutWhere extends OngoingMatch, ExposesMatch { - /** - * Marks the next match as optional match. - * - * @return A step exposing a {@link ExposesMatch#match(PatternElement...)} method for adding patterns to match. - */ - ExposesMatch optional(); - /** * Adds a where clause to this match. * @@ -69,10 +62,10 @@ public interface StatementBuilder { /** * A match that has a non-empty {@code where}-part. THe returning clause is still open. */ - interface OngoingMatchWithWhere extends OngoingMatch, ExposesMatch { - - ExposesMatch optional(); + interface OngoingMatchWithWhere extends OngoingMatch, ExposesMatch, ExposesConditions { + } + interface ExposesConditions { /** * Adds an additional condition to the existing conditions, connected by an {@literal and}. * Existing conditions will be logically grouped by using {@code ()} in the statement if previous @@ -81,7 +74,7 @@ public interface StatementBuilder { * @param condition An additional condition * @return The ongoing definition of a match */ - OngoingMatchWithWhere and(Condition condition); + T and(Condition condition); /** * Adds an additional condition to the existing conditions, connected by an {@literal or}. @@ -91,21 +84,14 @@ public interface StatementBuilder { * @param condition An additional condition * @return The ongoing definition of a match */ - OngoingMatchWithWhere or(Condition condition); + T or(Condition condition); } /** * A match that exposes {@code returning} and for which it is not decided whether the optional * where part has been used or note. */ - interface OngoingMatch extends ExposesReturning, OngoingDetachDelete { - - /** - * Starts building a delete step that will use {@code DETACH} to remove relationships. - * - * @return An ongoing delete step that is used to specify things to be deleted. - */ - OngoingDetachDelete detach(); + interface OngoingMatch extends ExposesReturning, ExposesWith, ExposesDelete { } /** @@ -114,6 +100,28 @@ public interface StatementBuilder { interface OngoingMatchAndReturn extends ExposesOrderBy, ExposesSkip, ExposesLimit { } + /** + * A match that knows what to pipe to the next part of a multi part query. + */ + interface OngoingMatchAndWithWithoutWhere extends OngoingMatchAndWith { + + /** + * Adds a where clause to this match. + * + * @param condition The new condition + * @return A match restricted by a where clause with no return items yet. + */ + OngoingMatchAndWithWithWhere where(Condition condition); + } + + interface OngoingMatchAndWithWithWhere + extends OngoingMatchAndWith, ExposesConditions { + } + + interface OngoingMatchAndWith + extends OngoingMatch, ExposesMatch, ExposesOrderBy, ExposesSkip, ExposesLimit, ExposesReturning { + } + interface OngoingMatchAndReturnWithOrder extends ExposesSkip, ExposesLimit { /** @@ -156,6 +164,7 @@ public interface StatementBuilder { } interface ExposesReturning { + /** * Create a match that returns one or more expressions. * @@ -163,10 +172,40 @@ public interface StatementBuilder { * @return A match that can be build now */ OngoingMatchAndReturn returning(Expression... expressions); + + /** + * Create a match that returns the distinct set of one or more expressions. + * + * @param expressions The expressions to be returned. Must not be null and be at least one expression. + * @return A match that can be build now + */ + OngoingMatchAndReturn returningDistinct(Expression... expressions); } /** - * A step that exposes several methods to speficy ordering. + * A step that exposes the {@code WITH} clause. + */ + interface ExposesWith { + + /** + * Create a match that returns one or more expressions. + * + * @param expressions The expressions to be returned. Must not be null and be at least one expression. + * @return A match that can be build now + */ + OngoingMatchAndWithWithoutWhere with(Expression... expressions); + + /** + * Create a match that returns the distinct set of one or more expressions. + * + * @param expressions The expressions to be returned. Must not be null and be at least one expression. + * @return A match that can be build now + */ + OngoingMatchAndWithWithoutWhere withDistinct(Expression... expressions); + } + + /** + * A step that exposes several methods to specify ordering. */ interface ExposesOrderBy extends BuildableMatch { @@ -220,7 +259,8 @@ public interface StatementBuilder { /** * A step that exposes only the delete clause. */ - interface OngoingDetachDelete { + interface ExposesDelete { + /** * Creates a delete step with one or more expressions to be deleted. * @@ -228,6 +268,14 @@ public interface StatementBuilder { * @return A match with a delete clause that can be build now */ OngoingMatchAndDelete delete(Expression... expressions); + + /** + * Starts building a delete step that will use {@code DETACH} to remove relationships. + * + * @param expressions The expressions to be deleted. + * @return A match with a delete clause that can be build now + */ + OngoingMatchAndDelete detachDelete(Expression... expressions); } /** @@ -242,11 +290,19 @@ public interface StatementBuilder { * @return An ongoing match that is used to specify an optional where and a required return clause */ OngoingMatchWithoutWhere match(PatternElement... pattern); + + /** + * Adds another optional match clause. + * + * @param pattern The patterns to match + * @return An ongoing match that is used to specify an optional where and a required return clause + */ + OngoingMatchWithoutWhere optionalMatch(PatternElement... pattern); } /** * A buildable step that will create a MATCH ... DELETE statement. */ - interface OngoingMatchAndDelete extends BuildableMatch, ExposesReturning { + interface OngoingMatchAndDelete extends BuildableMatch, ExposesReturning, ExposesWith, ExposesDelete { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java new file mode 100644 index 000000000..996d8af4f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; + +/** + * See With. + * + * @author Michael J. Simons + * @soundtrack Ferris MC - Ferris MC's Audiobiographie + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class With implements Visitable { + + private final boolean distinct; + + private final ReturnBody body; + + @Nullable + private final Where where; + + With(boolean distinct, ExpressionList returnItems, Order order, Skip skip, Limit limit, @Nullable Where where) { + this.distinct = distinct; + this.body = new ReturnBody(returnItems, order, skip, limit); + this.where = where; + } + + public boolean isDistinct() { + return distinct; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + this.body.accept(visitor); + Visitable.visitIfNotNull(where, visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java index 520801bf3..e6ba3a572 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java @@ -21,10 +21,16 @@ package org.springframework.data.neo4j.core.cypher.renderer; import org.springframework.data.neo4j.core.cypher.Statement; /** + * Instances of this class are supposed to be threadsafe. * @author Michael J. Simons * @since 1.0 */ public interface Renderer { + /** + * Renders a statement. + * @param statement + * @return + */ String render(Statement statement); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index 77c5f98e2..63dee3a0a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -137,6 +137,20 @@ class RenderingVisitor extends ReflectiveVisitor { void enter(Return returning) { builder.append("RETURN "); + if (returning.isDistinct()) { + builder.append("DISTINCT "); + } + } + + void enter(With with) { + builder.append("WITH "); + if (with.isDistinct()) { + builder.append("DISTINCT "); + } + } + + void leave(With with) { + builder.append(" "); } void enter(Delete delete) { @@ -174,9 +188,8 @@ class RenderingVisitor extends ReflectiveVisitor { .append(direction.getSymbol()); } - void enter(Property property) { + void leave(Property property) { builder - .append(property.getParentAlias()) .append(".") .append(property.getName()); } @@ -199,6 +212,21 @@ class RenderingVisitor extends ReflectiveVisitor { .append(" "); } + void enter(Operation operation) { + builder.append("("); + } + + void enter(Operator operator) { + builder + .append(" ") + .append(operator.getRepresentation()) + .append(" "); + } + + void leave(Operation operation) { + builder.append(")"); + } + void leave(IsNull isNull) { builder .append(" IS ") diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 79de56378..377e0c7cc 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -39,6 +39,11 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.neo4j.core.cypher.Condition; +import org.springframework.data.neo4j.core.cypher.Conditions; +import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Node; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.core.schema.RelationshipDescription; @@ -165,6 +170,12 @@ public class Neo4jMappingContext .map(neo4jPersistentEntity -> new DefaultNeo4jMappingFunction<>(instantiators, neo4jPersistentEntity)); } + @Override + public OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition) { + Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named("n"); + return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())).with(rootNode); + } + private Collection computeRelationshipsOf(String primaryLabel) { NodeDescription nodeDescription = getRequiredNodeDescription(primaryLabel); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java index 6eb2498d2..9280ffad1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -25,6 +25,9 @@ import java.util.function.Function; import org.apiguardian.api.API; import org.neo4j.driver.Record; +import org.springframework.data.neo4j.core.cypher.Condition; +import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; /** * Contains the descriptions of all nodes, their properties and relationships known to SDN-RX. @@ -58,6 +61,16 @@ public interface Schema { */ Optional> getNodeDescription(Class underlyingClass); + /** + * @param underlyingClass + * @return The node description for the given. class + * @throws UnknownEntityException When {@code targetClass} is not a known entity class. + */ + default NodeDescription getRequiredNodeDescription(Class underlyingClass) { + + return getNodeDescription(underlyingClass).orElseThrow(() -> new UnknownEntityException(underlyingClass)); + } + /** * This returns the outgoing relationships this node has to other nodes. * @@ -78,7 +91,34 @@ public interface Schema { * * @param targetClass The target class to which to map to. * @param Type of the target class - * @return An emtpy optional if the target class is unknown, otherwise an optional containing a stateless, reusable mapping function + * @return An empty optional if the target class is unknown, otherwise an optional containing a stateless, reusable mapping function */ Optional> getMappingFunctionFor(Class targetClass); + + /** + * @param targetClass The target class to which to map to. + * @param Type of the target class + * @return The default mapping function for the given target class + * @throws IllegalStateException When {@code targetClass} is not a managed class + * @see #getMappingFunctionFor(Class) + */ + default Function getRequiredMappingFunctionFor(Class targetClass) { + + return getMappingFunctionFor(targetClass).orElseThrow(() -> new UnknownEntityException(targetClass)); + } + + /** + * This will create a match statement that fits the given node description and may contains additional conditions. + * The {@code WITH} clause of this statement contains all nodes and relationships necessary to map a record to + * the given {@code nodeDescription}. + *

+ * It is recommended to use {@link Cypher#asterisk()} to return everything from the query in the end. + *

+ * The root node is guaranted to have the symbolic name {@code n}. + * + * @param nodeDescription The node description for which a match clause should be generated + * @param condition Optional conditions to add + * @return An ongoing match + */ + OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/UnknownEntityException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/UnknownEntityException.java new file mode 100644 index 000000000..4e6a9aba2 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/UnknownEntityException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.schema; + +import org.apiguardian.api.API; + +/** + * Thrown when required information about a class or primary label is requested from the {@link Schema} and those information + * is not available. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public final class UnknownEntityException extends IllegalStateException { + + private final Class targetClass; + + UnknownEntityException(Class targetClass) { + super(String.format("%s is not a known entity", targetClass.getName())); + this.targetClass = targetClass; + } + + public Class getTargetClass() { + return targetClass; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index f8a2271d1..34a6aa0a7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -59,6 +59,11 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati */ private String generatedNodeManagerBeanName; + /** + * Holds the name of the shared Neo4j mapping context.. + */ + private String generatedMappingContextBeanName; + /* * (non-Javadoc) * @see org.springframework.data.repository.config14.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() @@ -98,6 +103,7 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); builder.addPropertyReference("nodeManager", this.generatedNodeManagerBeanName); + builder.addPropertyReference("neo4jMappingContext", this.generatedMappingContextBeanName); } @Override @@ -108,7 +114,7 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati AbstractBeanDefinition neo4jMappingContextBeanDefinition = BeanDefinitionBuilder .rootBeanDefinition(Neo4jMappingContext.class) .getBeanDefinition(); - String nameOfMappingContextBean = registerWithSourceAndGeneratedBeanName( + this.generatedMappingContextBeanName = registerWithSourceAndGeneratedBeanName( neo4jMappingContextBeanDefinition, registry, config); // Augmented node manager factory (creating injectable, shared instances of NodeManager that are aware of the mapping context). @@ -117,7 +123,7 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati AbstractBeanDefinition sharedSessionCreatorBeanDefinition = BeanDefinitionBuilder .rootBeanDefinition(NodeManagerFactoryBean.class) .addConstructorArgReference(nameOfNodeManagerFactory) - .addConstructorArgReference(nameOfMappingContextBean) + .addConstructorArgReference(generatedMappingContextBeanName) .getBeanDefinition(); this.generatedNodeManagerBeanName = registerWithSourceAndGeneratedBeanName( sharedSessionCreatorBeanDefinition, registry, config); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java index 6bcf06f54..a38f6e9a3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -18,12 +18,72 @@ */ package org.springframework.data.neo4j.repository.query; +import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.PreparedQuery; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.util.Assert; /** * Base class for {@link RepositoryQuery} implementations for Neo4j. * * @author Gerrit Meier - **/ + * @author Michael J. Simons + * @since 1.0 + */ abstract class AbstractNeo4jQuery implements RepositoryQuery { + + protected final NodeManager nodeManager; + protected final Neo4jMappingContext mappingContext; + protected final Neo4jQueryMethod queryMethod; + protected final Class domainType; + + AbstractNeo4jQuery(NodeManager nodeManager, + Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod) { + + Assert.notNull(nodeManager, "The node manager is required."); + Assert.notNull(mappingContext, "The mapping context is required."); + Assert.notNull(queryMethod, "Query method must not be null!"); + + this.nodeManager = nodeManager; + this.mappingContext = mappingContext; + this.queryMethod = queryMethod; + this.domainType = queryMethod.getReturnedObjectType(); + } + + @Override + public QueryMethod getQueryMethod() { + return this.queryMethod; + } + + @Override + public final Object execute(Object[] parameters) { + return new Neo4jQueryExecution.DefaultQueryExecution(nodeManager) + .execute(prepareQuery(parameters), queryMethod.isCollectionQuery()); + } + + protected abstract PreparedQuery prepareQuery(Object[] parameters); + + /** + * Returns whether the query should get a count projection applied. + * + * @return + */ + protected abstract boolean isCountQuery(); + + /** + * @return True if the query should get an exists projection applied. + */ + protected abstract boolean isExistsQuery(); + + /** + * @return True if the query should delete matching nodes. + */ + protected abstract boolean isDeleteQuery(); + + /** + * @return True if the query has an explicit limit set. + */ + protected abstract boolean isLimiting(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java index 8f4de8e77..aa9f204c1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java @@ -24,14 +24,18 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; + /** * Specialized version of {@link Query} whose values is always used as count projection. * * @author Michael J. Simons + * @since 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Documented @Query(count = true) +@API(status = API.Status.STABLE, since = "1.0") public @interface CountQuery { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java new file mode 100644 index 000000000..70bfff07b --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import static org.springframework.data.neo4j.core.cypher.Cypher.*; + +import java.util.Iterator; +import java.util.Optional; + +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.neo4j.core.cypher.Condition; +import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Property; +import org.springframework.data.neo4j.core.cypher.Statement; +import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameter; +import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * A Cypher-DSL based implementation of the {@link AbstractQueryCreator} that eventually creates Cypher queries as strings + * to be used by a Neo4j client or driver as statement template. + * + * @author Michael J. Simons + * @since 1.0 + */ +final class CypherQueryCreator extends AbstractQueryCreator { + + private final Neo4jMappingContext mappingContext; + private final Class domainType; + private final NodeDescription nodeDescription; + + private Iterator formalParameters; + + CypherQueryCreator(Neo4jMappingContext mappingContext, Class domainType, PartTree tree, + Parameters formalParameters, ParameterAccessor actualParameters + ) { + super(tree, actualParameters); + + this.mappingContext = mappingContext; + this.domainType = domainType; + this.nodeDescription = this.mappingContext.getRequiredNodeDescription(this.domainType); + this.formalParameters = formalParameters.iterator(); + } + + @Override + protected Condition create(Part part, Iterator actualParameters) { + return createImpl(part); + } + + @Override + protected Condition and(Part part, Condition base, Iterator actualParameters) { + + if (base == null) { + return create(part, actualParameters); + } + + return base.and(createImpl(part)); + } + + @Override + protected Condition or(Condition base, Condition condition) { + return base.or(condition); + } + + @Override + protected String complete(Condition condition, Sort sort) { + + Statement statement = mappingContext + .prepareMatchOf(nodeDescription, Optional.of(condition)) + .returning(Cypher.symbolicName("n")) + .build(); + + return CypherRenderer.create().render(statement); + } + + private Condition createImpl(Part part) { + + PersistentPropertyPath path = mappingContext + .getPersistentPropertyPath(part.getProperty()); + Neo4jPersistentProperty persistentProperty = path.getLeafProperty(); + + // TODO case insensitive (like, notlike, simpleProperty, negatedSimpleProperty) + + switch (part.getType()) { + case CONTAINING: + return property(persistentProperty) + .contains(parameter(nextRequiredParameterNameOrIndex())); + case ENDING_WITH: + return property(persistentProperty) + .endsWith(parameter(nextRequiredParameterNameOrIndex())); + case FALSE: + return property(persistentProperty).isFalse(); + case LIKE: + return likeCondition(persistentProperty); + case SIMPLE_PROPERTY: + return property(persistentProperty) + .isEqualTo(parameter(nextRequiredParameterNameOrIndex())); + case STARTING_WITH: + return property(persistentProperty) + .startsWith(parameter(nextRequiredParameterNameOrIndex())); + case REGEX: + return property(persistentProperty) + .matches(parameter(nextRequiredParameterNameOrIndex())); + case NEGATING_SIMPLE_PROPERTY: + return property(persistentProperty) + .isNotEqualTo(parameter(nextRequiredParameterNameOrIndex())); + case NOT_CONTAINING: + return property(persistentProperty) + .contains(parameter(nextRequiredParameterNameOrIndex())).not(); + case NOT_LIKE: + return likeCondition(persistentProperty).not(); + case TRUE: + return property(persistentProperty).isTrue(); + default: + throw new IllegalArgumentException("Unsupported part type: " + part.getType()); + } + } + + private Condition likeCondition(Neo4jPersistentProperty persistentProperty) { + return property(persistentProperty) + .matches(literalOf(".*").plus(parameter(nextRequiredParameterNameOrIndex())).plus(literalOf(".*"))); + } + + private Property property(Neo4jPersistentProperty persistentProperty) { + return Cypher.property("n", persistentProperty.getPropertyName()); + } + + private String nextRequiredParameterNameOrIndex() { + if (!formalParameters.hasNext()) { + throw new IllegalStateException("Not enough formal, bindable parameters for parts"); + } + + return formalParameters.next().getNameOrIndex(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java index 3b9c20f53..58c5e2daf 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java @@ -24,14 +24,18 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; + /** * Specialized version of {@link Query} whose values is always used as exists projection. * * @author Michael J. Simons + * @since 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Documented @Query(exists = true) +@API(status = API.Status.STABLE, since = "1.0") public @interface ExistsQuery { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java new file mode 100644 index 000000000..b291f3af9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import lombok.RequiredArgsConstructor; + +import org.springframework.data.neo4j.core.ExecutableQuery; +import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.PreparedQuery; + +/** + * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a + * {@link org.springframework.data.repository.query.QueryMethod} a {@link AbstractNeo4jQuery} can be executed in various + * flavors. + * + * @author Michael J. Simons + * @since 1.0 + */ +@FunctionalInterface +interface Neo4jQueryExecution { + + Object execute(PreparedQuery description, boolean asCollectionQuery); + + @RequiredArgsConstructor + class DefaultQueryExecution implements Neo4jQueryExecution { + + private final NodeManager nodeManager; + + @Override + public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { + + ExecutableQuery executableQuery = nodeManager.toExecutableQuery(preparedQuery); + if (asCollectionQuery) { + return executableQuery.getResults(); + } else { + return executableQuery.getSingleResult(); + } + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java new file mode 100644 index 000000000..e1da8cf3c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import lombok.RequiredArgsConstructor; + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.apiguardian.api.API; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; + +/** + * Lookup strategy for queries. This is the internal api of the {@code query package}. + * + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +@RequiredArgsConstructor +public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { + + private final NodeManager nodeManager; + private final Neo4jMappingContext mappingContext; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) + */ + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + NamedQueries namedQueries) { + + Neo4jQueryMethod queryMethod = new Neo4jQueryMethod(method, metadata, factory); + + Optional optionalQueryAnnotation = getQueryAnnotationOf(method); + if (optionalQueryAnnotation.isPresent()) { + return new StringBasedNeo4jQuery(nodeManager, mappingContext, queryMethod, + getCypherQuery(optionalQueryAnnotation), optionalQueryAnnotation); + } + + return new PartTreeNeo4jQuery(nodeManager, mappingContext, queryMethod); + } + + /** + * @return the {@link Query} annotation that is applied to the method or an empty {@link Optional} if none available. + */ + static Optional getQueryAnnotationOf(Method method) { + return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)); + } + + static String getCypherQuery(Optional optionalQueryAnnotation) { + return optionalQueryAnnotation.map(Query::value).filter(s -> !s.isEmpty()) + .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not.")); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java index c10d12926..c038a6a46 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java @@ -18,23 +18,27 @@ */ package org.springframework.data.neo4j.repository.query; -import java.lang.reflect.Method; -import java.util.Optional; +import static java.lang.String.*; -import org.springframework.core.annotation.AnnotatedElementUtils; +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.core.MethodParameter; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.lang.Nullable; /** - * Neo4j specific implementation of {@link QueryMethod}. + * Neo4j specific implementation of {@link QueryMethod}. It contains a custom implementation of {@link Parameter} which + * supports Neo4js specific placeholder as well as a convinient method to return either the parameters index or name without placeholder. * * @author Gerrit Meier - **/ -public class Neo4jQueryMethod extends QueryMethod { - - private final Method method; + * @author Michael J. Simons + * @since 1.0 + */ +final class Neo4jQueryMethod extends QueryMethod { /** * Creates a new {@link Neo4jQueryMethod} from the given parameters. Looks up the correct query to use for following @@ -44,27 +48,61 @@ public class Neo4jQueryMethod extends QueryMethod { * @param metadata must not be {@literal null}. * @param factory must not be {@literal null}. */ - public Neo4jQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + Neo4jQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { super(method, metadata, factory); - this.method = method; } - /** - * Determines if this is a custom Cypher query method. - * - * @return true, if method has {@link Query} annotation. Otherwise false. - */ - public boolean hasAnnotatedQuery() { - return getQueryAnnotation().isPresent(); + @Override + protected Parameters createParameters(Method method) { + return new Neo4jParameters(method); } - @Nullable - String getAnnotatedQuery() { - return getQueryAnnotation().map(Query::value).orElse(null); + static class Neo4jParameters extends Parameters { + + Neo4jParameters(Method method) { + super(method); + } + + private Neo4jParameters(List originals) { + super(originals); + } + + @Override + protected Neo4jParameter createParameter(MethodParameter parameter) { + return new Neo4jParameter(parameter); + } + + @Override + protected Neo4jParameters createFrom(List parameters) { + return new Neo4jParameters(parameters); + } } - private Optional getQueryAnnotation() { - return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)); - } + static class Neo4jParameter extends Parameter { + private static final String NAMED_PARAMETER_TEMPLATE = "$%s"; + private static final String POSITION_PARAMETER_TEMPLATE = "$%d"; + + /** + * Creates a new {@link Parameter} for the given {@link MethodParameter}. + * + * @param parameter must not be {@literal null}. + */ + Neo4jParameter(MethodParameter parameter) { + super(parameter); + } + + public String getPlaceholder() { + + if (isNamedParameter()) { + return format(NAMED_PARAMETER_TEMPLATE, getName().get()); + } else { + return format(POSITION_PARAMETER_TEMPLATE, getIndex()); + } + } + + public String getNameOrIndex() { + return this.getName().orElseGet(() -> Integer.toString(this.getIndex())); + } + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 7013a6daa..f16ee08ae 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -18,28 +18,80 @@ */ package org.springframework.data.neo4j.repository.query; +import static java.util.stream.Collectors.*; + +import java.util.Map; + import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.neo4j.core.PreparedQuery; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.parser.PartTree; /** * Implementation of {@link RepositoryQuery} for derived finder methods. * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ -public class PartTreeNeo4jQuery extends AbstractNeo4jQuery { +final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { - public PartTreeNeo4jQuery(Neo4jQueryMethod queryMethod, NodeManager nodeManager) { + private final ResultProcessor processor; + private final PartTree tree; + + PartTreeNeo4jQuery( + NodeManager nodeManager, + Neo4jMappingContext mappingContext, + Neo4jQueryMethod queryMethod + ) { + super(nodeManager, mappingContext, queryMethod); + + this.processor = queryMethod.getResultProcessor(); + this.tree = new PartTree(queryMethod.getName(), domainType); } @Override - public Object execute(Object[] parameters) { - throw new UnsupportedOperationException("Not there yet."); + protected PreparedQuery prepareQuery(Object[] parameters) { + + Neo4jParameters formalParameters = (Neo4jParameters) this.queryMethod.getParameters(); + ParameterAccessor actualParameters = new ParametersParameterAccessor(formalParameters, parameters); + CypherQueryCreator queryCreator = new CypherQueryCreator( + mappingContext, domainType, tree, formalParameters, actualParameters + ); + + String cypherQuery = queryCreator.createQuery(); + Map boundedParameters = formalParameters + .getBindableParameters().stream() + .collect(toMap(Neo4jQueryMethod.Neo4jParameter::getNameOrIndex, formalParameter -> parameters[formalParameter.getIndex()])); + + return PreparedQuery.queryFor(super.domainType).withCypherQuery(cypherQuery) + .withParameters(boundedParameters) + .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) + .build(); } @Override - public QueryMethod getQueryMethod() { - return null; + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + @Override + protected boolean isLimiting() { + return tree.isLimiting(); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java index fe0fb5fc4..6036a3e6d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java @@ -24,6 +24,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; import org.springframework.data.annotation.QueryAnnotation; /** @@ -32,11 +33,13 @@ import org.springframework.data.annotation.QueryAnnotation; * parameters will get bound to the arguments of the annotated method. * * @author Michael J. Simons + * @since 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @QueryAnnotation @Documented +@API(status = API.Status.STABLE, since = "1.0") public @interface Query { /** @@ -53,4 +56,10 @@ public @interface Query { * @return whether the query defined should be executed as exists projection. */ boolean exists() default false; + + + /** + * @return whether the query defined should be used to delete nodes or relationships. + */ + boolean delete() default false; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index a3fc40f7a..d81875be1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -18,8 +18,12 @@ */ package org.springframework.data.neo4j.repository.query; +import java.util.Collections; +import java.util.Optional; + import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.neo4j.core.PreparedQuery; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.query.RepositoryQuery; /** @@ -27,33 +31,63 @@ import org.springframework.data.repository.query.RepositoryQuery; * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ -public class StringBasedNeo4jQuery extends AbstractNeo4jQuery { +final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { - private final Neo4jQueryMethod queryMethod; - private final NodeManager nodeManager; + private final String cypherQuery; - public StringBasedNeo4jQuery(Neo4jQueryMethod queryMethod, NodeManager nodeManager) { + private final boolean countQuery; + private final boolean existsQuery; + private final boolean deleteQuery; - this.queryMethod = queryMethod; - this.nodeManager = nodeManager; - } + StringBasedNeo4jQuery(NodeManager nodeManager, Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod, + String cypherQuery, + Optional optionalQueryAnnotation) { - @Override - public Object execute(Object[] parameters) { + super(nodeManager, mappingContext, queryMethod); - Class returnedType = queryMethod.getReturnedObjectType(); - boolean collectionQuery = queryMethod.isCollectionQuery(); + this.cypherQuery = cypherQuery; - if (collectionQuery) { - return nodeManager.executeTypedQueryForObjects(queryMethod.getAnnotatedQuery(), returnedType); + if (optionalQueryAnnotation.isPresent()) { + Query queryAnnotation = optionalQueryAnnotation.get(); + countQuery = queryAnnotation.count(); + existsQuery = queryAnnotation.exists(); + deleteQuery = queryAnnotation.delete(); } else { - return nodeManager.executeTypedQueryForObject(queryMethod.getAnnotatedQuery(), returnedType); + countQuery = false; + existsQuery = false; + deleteQuery = false; } } @Override - public QueryMethod getQueryMethod() { - return null; + protected PreparedQuery prepareQuery(Object[] parameters) { + + return PreparedQuery.queryFor(super.domainType) + .withCypherQuery(cypherQuery) + .withParameters(Collections.emptyMap()) // TODO Map parameters. + .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) // Null is fine + .build(); + } + + @Override + public boolean isCountQuery() { + return countQuery; + } + + @Override + public boolean isExistsQuery() { + return existsQuery; + } + + @Override + public boolean isDeleteQuery() { + return deleteQuery; + } + + @Override + protected boolean isLimiting() { + return false; } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/MappingNeo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/MappingNeo4jEntityInformation.java deleted file mode 100644 index 44ccff690..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/MappingNeo4jEntityInformation.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import org.springframework.data.neo4j.repository.query.Neo4jEntityInformation; -import org.springframework.data.repository.core.support.PersistentEntityInformation; - -/** - * @author Gerrit Meier - */ -public class MappingNeo4jEntityInformation extends PersistentEntityInformation - implements Neo4jEntityInformation { - - private final Neo4jPersistentEntity persistentEntity; - - public MappingNeo4jEntityInformation(Neo4jPersistentEntity persistentEntity, Class idClass) { - super(persistentEntity); - this.persistentEntity = persistentEntity; - } - - @Override - public String getIdPropertyName() { - return persistentEntity.getRequiredIdProperty().getName(); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index b8ca9d8e7..910a403ce 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -18,37 +18,36 @@ */ package org.springframework.data.neo4j.repository.support; -import java.lang.reflect.Method; import java.util.Optional; import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod; -import org.springframework.data.neo4j.repository.query.PartTreeNeo4jQuery; -import org.springframework.data.neo4j.repository.query.StringBasedNeo4jQuery; -import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.neo4j.repository.query.Neo4jQueryLookupStrategy; import org.springframework.data.repository.core.EntityInformation; -import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.RepositoryQuery; /** * Factory to create {@link Neo4jRepository} instances. * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { private final NodeManager nodeManager; - Neo4jRepositoryFactory(NodeManager nodeManager) { + private final Neo4jMappingContext mappingContext; + + Neo4jRepositoryFactory(NodeManager nodeManager, Neo4jMappingContext mappingContext) { this.nodeManager = nodeManager; + this.mappingContext = mappingContext; } @Override @@ -58,7 +57,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { @Override protected Object getTargetRepository(RepositoryInformation metadata) { - return getTargetRepositoryViaReflection(metadata, nodeManager, metadata.getDomainType()); + return getTargetRepositoryViaReflection(metadata, nodeManager, mappingContext, metadata.getDomainType()); } @Override @@ -74,34 +73,6 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { protected Optional getQueryLookupStrategy(Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new Neo4jQueryLookupStrategy(nodeManager, evaluationContextProvider)); - } - - private class Neo4jQueryLookupStrategy implements QueryLookupStrategy { - - private final NodeManager nodeManager; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; - - private Neo4jQueryLookupStrategy(NodeManager nodeManager, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - - this.nodeManager = nodeManager; - this.evaluationContextProvider = evaluationContextProvider; - } - - /* (non-Javadoc) - * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) - */ - @Override - public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, - NamedQueries namedQueries) { - - Neo4jQueryMethod queryMethod = new Neo4jQueryMethod(method, metadata, factory); - if (queryMethod.hasAnnotatedQuery()) { - return new StringBasedNeo4jQuery(queryMethod, nodeManager); - } - - return new PartTreeNeo4jQuery(queryMethod, nodeManager); - } + return Optional.of(new Neo4jQueryLookupStrategy(nodeManager, mappingContext, evaluationContextProvider)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java index 5849262a0..3ee4eb141 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.repository.support; import java.io.Serializable; import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; @@ -37,6 +38,8 @@ public class Neo4jRepositoryFactoryBean, S, ID exten private NodeManager nodeManager; + private Neo4jMappingContext neo4jMappingContext; + /** * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface. * @@ -50,9 +53,13 @@ public class Neo4jRepositoryFactoryBean, S, ID exten this.nodeManager = nodeManager; } + public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) { + this.neo4jMappingContext = neo4jMappingContext; + } + @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { - return new Neo4jRepositoryFactory(nodeManager); + return new Neo4jRepositoryFactory(nodeManager, neo4jMappingContext); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index a33ccea20..b72cde0af 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -24,25 +24,29 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.function.Function; import java.util.function.LongSupplier; +import org.neo4j.driver.Record; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.Node; import org.springframework.data.neo4j.core.cypher.SortItem; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; +import org.springframework.data.neo4j.core.cypher.SymbolicName; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; @@ -66,28 +70,33 @@ class SimpleNeo4jRepository implements Neo4jRepository { private final NodeManager nodeManager; + private final Neo4jMappingContext mappingContext; + private final Class nodeClass; - private final NodeDescription nodeDescription; - private Node node; + private final NodeDescription nodeDescription; + private final Function mappingFunction; private Expression idExpression; - SimpleNeo4jRepository(NodeManager nodeManager, Class nodeClass) { + SimpleNeo4jRepository(NodeManager nodeManager, Neo4jMappingContext mappingContext, Class nodeClass) { + this.nodeManager = nodeManager; + this.mappingContext = mappingContext; this.nodeClass = nodeClass; - this.nodeDescription = nodeManager.describe(nodeClass); - this.node = Cypher.node(nodeDescription.getPrimaryLabel()).named("n"); + this.nodeDescription = mappingContext.getRequiredNodeDescription(nodeClass); + this.mappingFunction = mappingContext.getRequiredMappingFunctionFor(nodeClass); - IdDescription idDescription = this.nodeDescription.getIdDescription(); + final SymbolicName rootNode = Cypher.symbolicName("n"); + final IdDescription idDescription = this.nodeDescription.getIdDescription(); switch (idDescription.getIdStrategy()) { case INTERNAL: - idExpression = node.internalId(); + idExpression = Functions.id(rootNode); break; case ASSIGNED: case GENERATED: idExpression = idDescription.getGraphPropertyName() - .map(this.node::property).get(); + .map(propertyName -> property(rootNode.getName(), propertyName)).get(); break; default: throw new IllegalStateException("Unsupported ID strategy: %s" + idDescription.getIdStrategy()); @@ -97,20 +106,25 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Iterable findAll(Sort sort) { - Statement statement = match(node).returning(node).orderBy(createSort(sort)).build(); - return nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); + Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) + .returning(asterisk()) + .orderBy(createSort(sort)) + .build(); + + return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); } @Override public Page findAll(Pageable pageable) { - StatementBuilder.OngoingMatchAndReturn returning = match(node).returning(node); + StatementBuilder.OngoingMatchAndReturn returning = mappingContext + .prepareMatchOf(nodeDescription, Optional.empty()).returning(asterisk()); StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); Statement statement = returningWithPaging.build(); - List allResult = new ArrayList<>(nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass)); + List allResult = nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); LongSupplier totalCountSupplier = this::count; return PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier); } @@ -118,13 +132,14 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Page findAll(Example example, Pageable pageable) { - StatementBuilder.OngoingMatchAndReturn returning = match(node).where(createAndConditions(example)).returning(node); + StatementBuilder.OngoingMatchAndReturn returning = mappingContext + .prepareMatchOf(nodeDescription, Optional.of(createAndConditions(example))).returning(asterisk()); StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); Statement statement = returningWithPaging.build(); - List allResult = new ArrayList<>(nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass)); + List allResult = nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); LongSupplier totalCountSupplier = this::count; return (Page) PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier); } @@ -154,9 +169,12 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Optional findById(ID id) { - Statement statement = match(node).where(idExpression.isEqualTo(literalOf(id))).returning(node) - .build(); - return nodeManager.executeTypedQueryForObject(renderer.render(statement), nodeClass); + + Statement statement = mappingContext + .prepareMatchOf(nodeDescription, Optional.of(idExpression.isEqualTo(literalOf(id)))) + .returning(asterisk()) + .build(); + return nodeManager.toExecutableQuery(prepareQuery(statement)).getSingleResult(); } @Override @@ -167,23 +185,27 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Iterable findAll() { - Statement statement = match(node).returning(node).build(); - return nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); + Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()).returning(asterisk()) + .build(); + return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); } @Override public Iterable findAllById(Iterable ids) { - Statement statement = match(node).where(idExpression.isIn((Iterable) ids)).returning(node) - .build(); - return nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); + + Statement statement = mappingContext + .prepareMatchOf(nodeDescription, Optional.of(idExpression.isIn((Iterable) ids))).returning(asterisk()) + .build(); + return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); } @Override public long count() { - Statement statement = match(node).returning(Functions.count(node)).build(); + Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) + .returning(Functions.count(asterisk())).build(); - return nodeManager.executeTypedQueryForObject(renderer.render(statement), Long.class).get(); + return nodeManager.toExecutableQuery(prepareQuery(Long.class, statement)).getRequiredSingleResult(); } @Override @@ -217,30 +239,50 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Optional findOne(Example example) { - Statement statement = match(node).where(createAndConditions(example)).returning(node).build(); + NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + Statement statement = mappingContext + .prepareMatchOf(probeNodeDescription, Optional.of(createAndConditions(example))) + .returning(asterisk()) + .build(); - return (Optional) nodeManager.executeTypedQueryForObject(renderer.render(statement), nodeClass); + return nodeManager.toExecutableQuery(prepareQuery(example.getProbeType(), statement)) + .getSingleResult(); } @Override public Iterable findAll(Example example) { - Statement statement = match(node).where(createAndConditions(example)).returning(node).build(); - return (Collection) nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); + NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + Statement statement = mappingContext.prepareMatchOf(probeNodeDescription, Optional.of(createAndConditions(example))) + .returning(asterisk()) + .build(); + + return nodeManager.toExecutableQuery( + prepareQuery(example.getProbeType(), statement)).getResults(); } @Override public Iterable findAll(Example example, Sort sort) { - Statement statement = match(node).where(createAndConditions(example)).returning(node).orderBy(createSort(sort)).build(); - return (Collection) nodeManager.executeTypedQueryForObjects(renderer.render(statement), nodeClass); + NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + Statement statement = mappingContext + .prepareMatchOf(probeNodeDescription, Optional.of(createAndConditions(example))) + .returning(asterisk()) + .orderBy(createSort(sort)).build(); + + return nodeManager.toExecutableQuery(prepareQuery(example.getProbeType(), statement)).getResults(); } @Override public long count(Example example) { - Statement statement = match(node).where(createAndConditions(example)).returning(Functions.count(node)).build(); - return nodeManager.executeTypedQueryForObject(renderer.render(statement), Long.class).get(); + NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + Statement statement = mappingContext + .prepareMatchOf(probeNodeDescription, Optional.of(createAndConditions(example))) + .returning(Functions.count(asterisk())) + .build(); + + return nodeManager.toExecutableQuery(prepareQuery(Long.class, statement)).getRequiredSingleResult(); } @Override @@ -260,10 +302,11 @@ class SimpleNeo4jRepository implements Neo4jRepository { } + private List createConditionsFromProperties(Example example) { - private List createConditionsFromProperties(Example example) { - - Collection graphProperties = nodeDescription.getGraphProperties(); + NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + SymbolicName rootNode = Cypher.symbolicName("n"); + Collection graphProperties = probeNodeDescription.getGraphProperties(); DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); ExampleMatcher matcher = example.getMatcher(); @@ -283,7 +326,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { String propertyName = graphProperty.getPropertyName(); - Condition condition = node.property(propertyName).isEqualTo(literalOf(propertyValue)); + Condition condition = property(rootNode, propertyName).isEqualTo(literalOf(propertyValue)); conditionList.add(condition); } @@ -292,9 +335,11 @@ class SimpleNeo4jRepository implements Neo4jRepository { private SortItem[] createSort(Sort sort) { + SymbolicName rootNode = Cypher.symbolicName("n"); + return sort.stream().map(order -> { String property = order.getProperty(); - SortItem sortItem = Cypher.sort(node.property(property)); + SortItem sortItem = Cypher.sort(property(rootNode, property)); // Spring's Sort.Order defaults to ascending, so we just need to change this if we have descending order. if (order.isDescending()) { @@ -303,4 +348,21 @@ class SimpleNeo4jRepository implements Neo4jRepository { return sortItem; }).toArray(SortItem[]::new); } + + private PreparedQuery prepareQuery(Statement statement) { + return prepareQuery(nodeClass, statement); + } + + private PreparedQuery prepareQuery(Class resultType, Statement statement) { + + Function mappingFunctionToUse = this.mappingFunction; + if (!this.nodeClass.equals(resultType)) { + mappingFunctionToUse = mappingContext.getMappingFunctionFor(resultType).orElse(null); + } + + return PreparedQuery.queryFor(resultType) + .withCypherQuery(renderer.render(statement)) + .usingMappingFunction(mappingFunctionToUse) + .build(); + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java index a64842353..1560594c9 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java @@ -28,6 +28,8 @@ import org.junit.jupiter.api.Test; import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.Schema; /** * @author Gerrit Meier @@ -35,6 +37,7 @@ import org.springframework.data.neo4j.core.schema.Id; */ class DefaultPersistenceContextTest { + private Schema schema; private PersistenceContext context; private EntityTrackingStrategy entityTrackingStrategy; @@ -44,12 +47,13 @@ class DefaultPersistenceContextTest { when(entityTrackingStrategy.getObjectIdentifier(any())) .thenAnswer(invocation -> System.identityHashCode(invocation.getArguments()[0])); - Neo4jMappingContext schema = new Neo4jMappingContext(); - schema.setInitialEntitySet(new HashSet>(Arrays.asList(Something.class))); - schema.initialize(); + Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + mappingContext.setInitialEntitySet(new HashSet>(Arrays.asList(Something.class))); + mappingContext.initialize(); + schema = mappingContext; // override method to return a verifiable mock - context = new DefaultPersistenceContext(schema) { + context = new DefaultPersistenceContext() { @Override EntityTrackingStrategy getEntityTrackingStrategy() { return entityTrackingStrategy; @@ -59,35 +63,46 @@ class DefaultPersistenceContextTest { @Test void registerAddsEntityToTrackingStrategy() { - context.register(new Something()); + + NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); + + context.register(new Something(), nodeDescription); verify(entityTrackingStrategy).track(any(), any()); } @Test void registerTheSameObjectMultipleTimesCallsTrackJustOnce() { + + NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); Something entity = new Something(); - context.register(entity); - context.register(entity); + + context.register(entity, nodeDescription); + context.register(entity, nodeDescription); verify(entityTrackingStrategy).track(any(), any()); } @Test void registerTwoObjectOfTheSameTypeCallsTrackTwice() { + + NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); Something entity1 = new Something(); Something entity2 = new Something(); - context.register(entity1); - context.register(entity2); + context.register(entity1, nodeDescription); + context.register(entity2, nodeDescription); verify(entityTrackingStrategy, times(2)).track(any(), any()); } @Test void triggersDeltaCalculationOnDeltaCall() { + + NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); Something entity = new Something(); - context.register(entity); + + context.register(entity, nodeDescription); context.getEntityChanges(entity); verify(entityTrackingStrategy).getAggregatedEntityChangeEvents(entity); @@ -95,9 +110,11 @@ class DefaultPersistenceContextTest { @Test void deregisterRemovesEntityFromTracking() { - Something entity = new Something(); - context.register(entity); + NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); + Something entity = new Something(); + + context.register(entity, nodeDescription); context.deregister(entity); verify(entityTrackingStrategy).untrack(entity); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java index 54b1a3f72..588ecac9b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java @@ -71,7 +71,7 @@ class EntityComparisonStrategyTest { EntityComparisonStrategy strategy = new EntityComparisonStrategy(); Something something = new Something("oldValue"); - strategy.track(description, something); + strategy.track(something, description); String fieldName = "value"; String newValue = "newValue"; @@ -89,7 +89,7 @@ class EntityComparisonStrategyTest { EntityComparisonStrategy strategy = new EntityComparisonStrategy(); Something something = new Something("oldValue"); - strategy.track(description, something); + strategy.track(something, description); String fieldName = "information"; something.information.add("additional entry"); @@ -108,7 +108,7 @@ class EntityComparisonStrategyTest { something.information.add("entry 1"); something.information.add("entry 2"); - strategy.track(description, something); + strategy.track(something, description); something.information.sort(Comparator.reverseOrder()); @@ -126,7 +126,7 @@ class EntityComparisonStrategyTest { EntityComparisonStrategy strategy = new EntityComparisonStrategy(); Something something = new Something("oldValue"); - strategy.track(description, something); + strategy.track(something, description); String fieldName = "parentValue"; String newValue = "newValue"; @@ -145,8 +145,8 @@ class EntityComparisonStrategyTest { Something something1 = new Something("oldValue"); Something something2 = new Something("oldValue"); - strategy.track(description, something1); - strategy.track(description, something2); + strategy.track(something1, description); + strategy.track(something2, description); String fieldName = "value"; String newValue1 = "newValue1"; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 3de3d538d..d915b2e8f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -54,6 +54,16 @@ public class CypherTest { .isEqualTo("MATCH (b:`Bike`), (u:`User`), (o:`U`) RETURN b, u"); } + @Test + void asterikShouldWork() { + Statement statement = Cypher.match(bikeNode, userNode, Cypher.node("U").named("o")) + .returning(Cypher.asterisk()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`), (u:`User`), (o:`U`) RETURN *"); + } + @Test void aliasedExpressionsInReturn() { Node unnamedNode = Cypher.node("ANode"); @@ -248,6 +258,94 @@ public class CypherTest { .isEqualTo( "MATCH (u:`User`) RETURN u SKIP 1 LIMIT 1"); } + + @Test + void distinct() { + Statement statement = Cypher.match(userNode).returningDistinct(userNode).skip(1).limit(1).build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN DISTINCT u SKIP 1 LIMIT 1"); + } + } + } + + @Nested + class SingleQueryMultiPart { + @Test + void simpleWith() { + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .where(userNode.property("a").isNull()) + .with(bikeNode, userNode) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u RETURN b"); + } + + @Test + void simpleWithChained() { + + Node tripNode = Cypher.node("Trip").named("t"); + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .where(userNode.property("a").isNull()) + .with(bikeNode, userNode) + .match(tripNode) + .where(tripNode.property("name").isEqualTo(literalOf("Festive500"))) + .with(tripNode) + .returning(bikeNode, userNode, tripNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u MATCH (t:`Trip`) WHERE t.name = 'Festive500' WITH t RETURN b, u, t"); + } + + @Test + void deletingSimpleWith() { + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .where(userNode.property("a").isNull()) + .delete(userNode) + .with(bikeNode, userNode) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL DELETE u WITH b, u RETURN b, u"); + } + + @Test + void deletingSimpleWithReverse() { + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .where(userNode.property("a").isNull()) + .with(bikeNode, userNode) + .delete(userNode) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u DELETE u RETURN b, u"); + } + + @Test + void mixedClausesWithWith() { + + Node tripNode = Cypher.node("Trip").named("t"); + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .match(tripNode) + .delete(tripNode) + .with(bikeNode, tripNode) + .match(userNode) + .with(bikeNode, userNode) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) MATCH (t:`Trip`) DELETE t WITH b, t MATCH (u) WITH b, u RETURN b, u"); } } @@ -310,8 +408,7 @@ public class CypherTest { @Test void optional() { Statement statement = Cypher - .optional() - .match(bikeNode) + .optionalMatch(bikeNode) .match(userNode, Cypher.node("U").named("o")) .where(userNode.property("a").isNull()) .returning(bikeNode) @@ -325,8 +422,7 @@ public class CypherTest { void optionalNext() { Statement statement = Cypher .match(bikeNode) - .optional() - .match(userNode, Cypher.node("U").named("o")) + .optionalMatch(userNode, Cypher.node("U").named("o")) .where(userNode.property("a").isNull()) .returning(bikeNode) .build(); @@ -724,7 +820,7 @@ public class CypherTest { Statement statement; statement = Cypher.match(userNode) - .detach().delete(userNode) + .detachDelete(userNode) .build(); assertThat(cypherRenderer.render(statement)) @@ -754,7 +850,7 @@ public class CypherTest { Statement statement; statement = Cypher.match(userNode) - .detach().delete(userNode) + .detachDelete(userNode) .returning(userNode) .build(); @@ -764,7 +860,7 @@ public class CypherTest { statement = Cypher.match(userNode) .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) - .detach().delete(userNode) + .detachDelete(userNode) .returning(userNode).orderBy(userNode.property("a").ascending()).skip(2).limit(1) .build(); @@ -772,6 +868,15 @@ public class CypherTest { .isEqualTo( "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DETACH DELETE u RETURN u ORDER BY u.a ASC SKIP 2 LIMIT 1"); + statement = Cypher.match(userNode) + .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) + .detachDelete(userNode) + .returningDistinct(userNode).orderBy(userNode.property("a").ascending()).skip(2).limit(1) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DETACH DELETE u RETURN DISTINCT u ORDER BY u.a ASC SKIP 2 LIMIT 1"); } @Test @@ -780,7 +885,7 @@ public class CypherTest { Relationship r = n.relationshipBetween(anyNode()).named("r0"); Statement statement = Cypher .match(n).where(n.internalId().isEqualTo(literalOf(4711))) - .optional().match(r) + .optionalMatch(r) .delete(r, n) .build(); @@ -788,6 +893,23 @@ public class CypherTest { .isEqualTo( "MATCH (n) WHERE id(n) = 4711 OPTIONAL MATCH (n)-[r0]-() DELETE r0, n"); } + + @Test + void shouldRenderChainedDeletes() { + Node n = anyNode("n"); + Relationship r = n.relationshipBetween(anyNode()).named("r0"); + Statement statement = Cypher + .match(n).where(n.internalId().isEqualTo(literalOf(4711))) + .optionalMatch(r) + .delete(r, n) + .delete(bikeNode) + .detachDelete(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) WHERE id(n) = 4711 OPTIONAL MATCH (n)-[r0]-() DELETE r0, n DELETE b DETACH DELETE u"); + } } @Nested @@ -797,7 +919,7 @@ public class CypherTest { Statement statement; statement = Cypher.match(userNode) .where(userNode.property("a").isEqualTo(parameter("aParameter"))) - .detach().delete(userNode) + .detachDelete(userNode) .returning(userNode) .build(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java index aea3e220d..7aa2dc793 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java @@ -32,7 +32,7 @@ class DefaultStatementBuilderTest { void matchPreconditionsShouldBeAsserted() { DefaultStatementBuilder builder = new DefaultStatementBuilder(); - assertThatIllegalArgumentException().isThrownBy(() -> builder.match(null)) + assertThatIllegalArgumentException().isThrownBy(() -> builder.match((PatternElement[]) null)) .withMessage("Patterns to match are required."); assertThatIllegalArgumentException().isThrownBy(() -> builder.match()) .withMessage("At least one pattern to match is required."); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java index 2cc80297f..eb1a868f1 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java @@ -59,7 +59,7 @@ class FunctionsTest { @Test void preconditionsShouldBeAsserted() { - assertThatIllegalArgumentException().isThrownBy(() -> Functions.coalesce(null)) + assertThatIllegalArgumentException().isThrownBy(() -> Functions.coalesce((Expression[]) null)) .withMessage("At least one expression is required."); assertThatIllegalArgumentException().isThrownBy(() -> Functions.coalesce(new Expression[0])) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 6727977bb..0f3cd089f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -19,7 +19,6 @@ package org.springframework.data.neo4j.integration; import java.util.List; -import java.util.Map; import java.util.Optional; import org.springframework.data.neo4j.repository.Neo4jRepository; @@ -63,5 +62,30 @@ public interface PersonRepository extends Neo4jRepository getOptionalPersonsWithWitherViaQuery(); - List> findByName(String name); + // Derived finders, should be extracted into another repo. + Optional findOneByNameAndFirstName(String name, String firstName); + + List findAllByNameOrName(String aName, String anotherName); + + List findAllBySameValue(String sameValue); + + List findAllByNameNot(String name); + + List findAllByFirstNameLike(String name); + + List findAllByFirstNameMatches(String name); + + List findAllByFirstNameNotLike(String name); + + List findAllByCoolTrue(); + + List findAllByCoolFalse(); + + List findAllByFirstNameStartingWith(String name); + + List findAllByFirstNameContaining(String name); + + List findAllByFirstNameNotContaining(String name); + + List findAllByFirstNameEndingWith(String name); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java index 948554ee8..c506746ca 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java @@ -28,6 +28,10 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Property; +/** + * @author Gerrit Meier + * @author Michael J. Simons + */ @Getter @Setter @Node @@ -45,4 +49,6 @@ public class PersonWithAllConstructor { private final String firstName; private final String sameValue; + + private final Boolean cool; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 28aeb1b44..c80269c32 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -59,12 +59,12 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; class RepositoryIT { private static final String TEST_PERSON1_NAME = "Test"; private static final String TEST_PERSON2_NAME = "Test2"; - private static final String TEST_PERSON1_FIRST_NAME = "A"; - private static final String TEST_PERSON2_FIRST_NAME = "B"; + private static final String TEST_PERSON1_FIRST_NAME = "Ernie"; + private static final String TEST_PERSON2_FIRST_NAME = "Bert"; private static final String TEST_PERSON_SAMEVALUE = "SameValue"; static PersonWithAllConstructor personExample(String sameValue) { - return new PersonWithAllConstructor(null, null, null, sameValue); + return new PersonWithAllConstructor(null, null, null, sameValue, null); } private static Neo4jConnectionSupport neo4jConnectionSupport; @@ -91,12 +91,14 @@ class RepositoryIT { transaction.run("MATCH (n) detach delete n"); id1 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName return id(n)", - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME) + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool return id(n)", + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true) ).next().get(0).asLong(); id2 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName return id(n)", - Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME) + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool return id(n)", + Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON2_FIRST_NAME, "cool", false) ).next().get(0).asLong(); transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = $name, n.first_name = $firstName", Values.parameters("name", TEST_PERSON1_NAME, "firstName", TEST_PERSON1_FIRST_NAME)); @@ -106,8 +108,10 @@ class RepositoryIT { transaction.close(); // note that there is no id setting in the mapping right now - person1 = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE); - person2 = new PersonWithAllConstructor(null, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE); + person1 = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, + true); + person2 = new PersonWithAllConstructor(null, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, + false); } @Test @@ -307,8 +311,100 @@ class RepositoryIT { @Test void findBySimpleProperty() { - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> repository.findByName(TEST_PERSON1_NAME)); + + List persons = repository.findAllBySameValue(TEST_PERSON_SAMEVALUE); + assertThat(persons).containsExactlyInAnyOrder(person1, person2); + } + + @Test + void findBySimplePropertiesAnded() { + + Optional optionalPerson = repository.findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME); + assertThat(optionalPerson).isPresent().contains(person1); + } + + @Test + void findBySimplePropertiesOred() { + + List persons = repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME); + assertThat(persons).containsExactlyInAnyOrder(person1, person2); + } + + @Test + void findByNegatedSimpleProperty() { + + List persons = repository.findAllByNameNot(TEST_PERSON1_NAME); + assertThat(persons).doesNotContain(person1); + } + + @Test + void findByTrueAndFalse() { + + List coolPeople = repository.findAllByCoolTrue(); + List theRest = repository.findAllByCoolFalse(); + assertThat(coolPeople).doesNotContain(person2); + assertThat(theRest).doesNotContain(person1); + } + + @Test + void findByLike() { + + List persons = repository.findAllByFirstNameLike("Ern"); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByMatches() { + + List persons = repository.findAllByFirstNameMatches("(?i)ern.*"); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByNotLike() { + + List persons = repository.findAllByFirstNameNotLike("Ern"); + assertThat(persons).doesNotContain(person1); + } + + @Test + void findByStartingWith() { + + List persons = repository.findAllByFirstNameStartingWith("Er"); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByContaining() { + + List persons = repository.findAllByFirstNameContaining("ni"); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByNotContaining() { + + List persons = repository.findAllByFirstNameNotContaining("ni"); + assertThat(persons) + .hasSize(1) + .contains(person2); + } + + @Test + void findByEndingWith() { + + List persons = repository.findAllByFirstNameContaining("nie"); + assertThat(persons) + .hasSize(1) + .contains(person1); } @Configuration diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategyTest.java similarity index 52% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategyTest.java index d6799462b..c272ad706 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethodTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategyTest.java @@ -22,44 +22,59 @@ import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.Test; -import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.mapping.MappingException; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; /** * @author Gerrit Meier - **/ -class Neo4jQueryMethodTest { + * @author Michael J. Simons + */ +class Neo4jQueryLookupStrategyTest { private static final String CUSTOM_CYPHER_QUERY = "MATCH (n) return n"; @Test void shouldFindAnnotatedQuery() throws Exception { - Neo4jQueryMethod queryMethod = queryMethod("annotatedQuery"); - assertThat(queryMethod.getAnnotatedQuery()).isEqualTo(CUSTOM_CYPHER_QUERY); + + Method method = queryMethod("annotatedQuery"); + Optional optionalQueryAnnotation = Neo4jQueryLookupStrategy.getQueryAnnotationOf(method); + assertThat(Neo4jQueryLookupStrategy.getCypherQuery(optionalQueryAnnotation)).isEqualTo(CUSTOM_CYPHER_QUERY); + } + + @Test + void shouldDetectInvalidAnnotation() throws Exception { + + Method method = queryMethod("invalidAnnotatedQuery"); + Optional optionalQueryAnnotation = Neo4jQueryLookupStrategy.getQueryAnnotationOf(method); + assertThatExceptionOfType(MappingException.class) + .isThrownBy(() -> Neo4jQueryLookupStrategy.getCypherQuery(optionalQueryAnnotation)) + .withMessage("Expected @Query annotation to have a value, but it did not."); } @Test void findQueryAnnotation() throws Exception { - Neo4jQueryMethod queryMethod = queryMethod("annotatedQuery"); - assertThat(queryMethod.hasAnnotatedQuery()).isTrue(); + + Method method = queryMethod("annotatedQuery"); + Optional optionalQueryAnnotation = Neo4jQueryLookupStrategy.getQueryAnnotationOf(method); + assertThat(optionalQueryAnnotation).isPresent(); } - private Neo4jQueryMethod queryMethod(String name, Class... parameters) throws Exception { + private Method queryMethod(String name, Class... parameters) throws Exception { Class repositoryClass = PersonRepository.class; - Method method = repositoryClass.getMethod(name, parameters); - ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); - return new Neo4jQueryMethod(method, new DefaultRepositoryMetadata(repositoryClass), factory); + return repositoryClass.getMethod(name, parameters); } interface PersonRepository extends Repository { @Query(CUSTOM_CYPHER_QUERY) List annotatedQuery(); + + @Query + List invalidAnnotatedQuery(); } class Person { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java index 9f530b617..7b6abd1b2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java @@ -26,6 +26,7 @@ import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.Node; @@ -39,6 +40,8 @@ class SimpleNeo4jRepositoryTest { private NodeManager nodeManager; + private Neo4jMappingContext mappingContext; + private NodeDescription nodeDescription; private SimpleNeo4jRepository repository; @@ -50,8 +53,10 @@ class SimpleNeo4jRepositoryTest { nodeDescription = mock(NodeDescription.class); when(nodeDescription.getPrimaryLabel()).thenReturn("TestNode"); when(nodeDescription.getIdDescription()).thenReturn(new IdDescription()); - when(nodeManager.describe(TestNode.class)).thenReturn(nodeDescription); - repository = new SimpleNeo4jRepository(this.nodeManager, TestNode.class); + + mappingContext = mock(Neo4jMappingContext.class); + when(mappingContext.getRequiredNodeDescription(TestNode.class)).thenReturn(nodeDescription); + repository = new SimpleNeo4jRepository(this.nodeManager, this.mappingContext, TestNode.class); } @Test From 23f96b99c31db82ad5569a82fa33c08a2f4341c5 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 14 May 2019 21:22:40 +0200 Subject: [PATCH 076/342] Add support for Ranges and ordinal comparators for derived queries. --- .../data/neo4j/core/NamedParameters.java | 2 +- .../data/neo4j/core/cypher/Comparison.java | 51 ++++++--- .../neo4j/core/cypher/CompoundCondition.java | 43 ++----- .../data/neo4j/core/cypher/Condition.java | 12 +- .../data/neo4j/core/cypher/Conditions.java | 60 ++++++++-- .../data/neo4j/core/cypher/Expression.java | 18 ++- .../data/neo4j/core/cypher/IsNull.java | 75 ------------- .../neo4j/core/cypher/NestedExpression.java | 46 ++++++++ .../data/neo4j/core/cypher/Operations.java | 2 +- .../data/neo4j/core/cypher/Operator.java | 56 ++++++++- .../cypher/renderer/RenderingVisitor.java | 54 +++------ .../repository/query/CypherQueryCreator.java | 86 +++++++++++--- .../repository/query/PartTreeNeo4jQuery.java | 17 ++- .../neo4j/core/cypher/ComparisonTest.java | 8 +- .../data/neo4j/core/cypher/CypherTest.java | 45 ++++++++ .../neo4j/integration/PersonRepository.java | 13 +++ .../integration/PersonWithAllConstructor.java | 2 + .../data/neo4j/integration/RepositoryIT.java | 106 ++++++++++++++++-- 18 files changed, 482 insertions(+), 214 deletions(-) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NestedExpression.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java index 116d6759f..e0fdc06c4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java @@ -83,6 +83,6 @@ final class NamedParameters { .entrySet() .stream() .map(e -> String.format("%s: %s", e.getKey(), e.getValue())) - .collect(Collectors.joining(",", "params {", "}")); + .collect(Collectors.joining(", ", "params {", "}")); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java index b9415c8af..9591ad683 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java @@ -18,8 +18,11 @@ */ package org.springframework.data.neo4j.core.cypher; + import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -32,39 +35,53 @@ import org.springframework.util.Assert; @API(status = API.Status.INTERNAL, since = "1.0") public final class Comparison implements Condition { - static Comparison create(Expression lhs, String comparator, Expression rhs) { + static Comparison create(Operator operator, Expression expression) { + + Assert.state(operator.isUnary(), "Operator must be unary."); + Assert.notNull(expression, "Expression must not be null."); + + switch (operator.getType()) { + case PREFIX: + return new Comparison(null, operator, expression); + case POSTFIX: + return new Comparison(expression, operator, null); + default: + throw new IllegalArgumentException("Invalid operator type " + operator.getType()); + } + } + + static Comparison create(Expression lhs, Operator operator, Expression rhs) { Assert.notNull(lhs, "Left expression must not be null."); - Assert.hasText(comparator, "Comparator must not be empty."); + Assert.notNull(operator, "Operator must not be empty."); Assert.notNull(rhs, "Right expression must not be null."); - return new Comparison(lhs, comparator, rhs); + return new Comparison(lhs, operator, rhs); + } + + private static Expression nestedIfCondition(Expression expression) { + return expression instanceof Condition ? new NestedExpression(expression) : expression; } private final Expression left; - private final String comparator; - private final Expression right; + private final Operator comparator; + private @Nullable final Expression right; - private Comparison(Expression left, String comparator, Expression right) { + private Comparison(Expression left, Operator operator, Expression right) { - this.left = left; - this.comparator = comparator; - this.right = right; - } - - public String getComparator() { - return comparator; + this.left = nestedIfCondition(left); + this.comparator = operator; + this.right = nestedIfCondition(right); } @Override public void accept(Visitor visitor) { - left.accept(visitor); - visitor.enter(this); + Visitable.visitIfNotNull(left, visitor); + comparator.accept(visitor); + Visitable.visitIfNotNull(right, visitor); visitor.leave(this); - - right.accept(visitor); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java index e3a388775..827b8277f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java @@ -18,9 +18,10 @@ */ package org.springframework.data.neo4j.core.cypher; -import static org.springframework.data.neo4j.core.cypher.CompoundCondition.LogicalOperator.*; +import static org.springframework.data.neo4j.core.cypher.Operator.*; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import org.apiguardian.api.API; @@ -43,8 +44,12 @@ public final class CompoundCondition implements Condition { * The empty, compound condition. */ static final CompoundCondition EMPTY_CONDITION = new CompoundCondition(null); + static final EnumSet VALID_OPERATORS = EnumSet.of(AND, OR, XOR); - static CompoundCondition create(Condition left, LogicalOperator operator, Condition right) { + static CompoundCondition create(Condition left, Operator operator, Condition right) { + + Assert.state(VALID_OPERATORS.contains(operator), + "Operator " + operator + " is not a valid operator for a compound condition."); Assert.notNull(left, "Left hand side condition is required."); Assert.notNull(operator, "Operator is required."); @@ -60,11 +65,11 @@ public final class CompoundCondition implements Condition { return EMPTY_CONDITION; } - private @Nullable final LogicalOperator operator; + private @Nullable final Operator operator; private final List conditions; - private CompoundCondition(@Nullable LogicalOperator operator) { + private CompoundCondition(@Nullable Operator operator) { this.operator = operator; this.conditions = new ArrayList<>(); } @@ -85,7 +90,7 @@ public final class CompoundCondition implements Condition { } private CompoundCondition add( - LogicalOperator chainingOperator, + Operator chainingOperator, Condition condition ) { if (this == EMPTY_CONDITION) { @@ -125,7 +130,7 @@ public final class CompoundCondition implements Condition { visitor.enter(this); } - LogicalOperator currentOperator = null; + Operator currentOperator = null; for (Condition condition : conditions) { Visitable.visitIfNotNull(currentOperator, visitor); condition.accept(visitor); @@ -136,30 +141,4 @@ public final class CompoundCondition implements Condition { visitor.leave(this); } } - - /** - * Shared interface for all logical operators. - */ - public enum LogicalOperator implements org.springframework.data.neo4j.core.cypher.support.Visitable { - // Note the full import above: JDK 8 fails to compile this class otherwise. - /* - sdn-rx/spring-data-neo4j-rx/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java:[140,48] cannot find symbol - symbol: class Visitable - location: class org.springframework.data.neo4j.core.cypher.CompoundCondition - */ - - AND("AND"), - OR("OR"), - XOR("XOR"); - - private final String symbol; - - LogicalOperator(String symbol) { - this.symbol = symbol; - } - - public String getSymbol() { - return symbol; - } - } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java index 322188f60..034de8fb5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java @@ -19,8 +19,6 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.CompoundCondition.LogicalOperator; -import org.springframework.data.neo4j.core.cypher.support.Visitable; /** * Shared interface for all conditions. @@ -29,21 +27,21 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public interface Condition extends Visitable { +public interface Condition extends Expression { default Condition and(Condition condition) { - return CompoundCondition.create(this, LogicalOperator.AND, condition); + return CompoundCondition.create(this, Operator.AND, condition); } default Condition or(Condition condition) { - return CompoundCondition.create(this, LogicalOperator.OR, condition); + return CompoundCondition.create(this, Operator.OR, condition); } default Condition xor(Condition condition) { - return CompoundCondition.create(this, LogicalOperator.XOR, condition); + return CompoundCondition.create(this, Operator.XOR, condition); } default Condition not() { - return new NotCondition(this); + return Comparison.create(Operator.NOT, this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java index 8901e298a..c66684d2f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java @@ -41,7 +41,7 @@ public final class Conditions { * @return A "matches" comparision */ static Condition matches(Expression lhs, Expression rhs) { - return Comparison.create(lhs, "=~", rhs); + return Comparison.create(lhs, Operator.MATCHES, rhs); } /** @@ -52,7 +52,7 @@ public final class Conditions { * @return An "equals" comparision */ static Condition isEqualTo(Expression lhs, Expression rhs) { - return Comparison.create(lhs, "=", rhs); + return Comparison.create(lhs, Operator.EQUALITY, rhs); } /** @@ -63,7 +63,51 @@ public final class Conditions { * @return An "not equals" comparision */ static Condition isNotEqualTo(Expression lhs, Expression rhs) { - return Comparison.create(lhs, "<>", rhs); + return Comparison.create(lhs, Operator.INEQUALITY, rhs); + } + + /** + * Creates a condition that matches if the left hand side is less than the right hand side.. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return An "less than" comparision + */ + static Condition lt(Expression lhs, Expression rhs) { + return Comparison.create(lhs, Operator.LESS_THAN, rhs); + } + + /** + * Creates a condition that matches if the left hand side is less than or equal the right hand side.. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return An "less than or equal" comparision + */ + static Condition lte(Expression lhs, Expression rhs) { + return Comparison.create(lhs, Operator.LESS_THAN_OR_EQUAL_TO, rhs); + } + + /** + * Creates a condition that matches if the left hand side is greater than or equal the right hand side.. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return An "greater than or equal" comparision + */ + static Condition gte(Expression lhs, Expression rhs) { + return Comparison.create(lhs, Operator.GREATER_THAN_OR_EQUAL_TO, rhs); + } + + /** + * Creates a condition that matches if the left hand side is greater than the right hand side.. + * + * @param lhs The left hand side of the comparision + * @param rhs The right hand side of the comparision + * @return An "greater than" comparision + */ + static Condition gt(Expression lhs, Expression rhs) { + return Comparison.create(lhs, Operator.GREATER_THAN, rhs); } /** @@ -86,7 +130,7 @@ public final class Conditions { * @return A new condition. */ static Condition startsWith(Expression lhs, Expression rhs) { - return Comparison.create(lhs, "STARTS WITH", rhs); + return Comparison.create(lhs, Operator.STARTS_WITH, rhs); } /** @@ -97,7 +141,7 @@ public final class Conditions { * @return A new condition. */ static Condition contains(Expression lhs, Expression rhs) { - return Comparison.create(lhs, "CONTAINS", rhs); + return Comparison.create(lhs, Operator.CONTAINS, rhs); } /** @@ -108,7 +152,7 @@ public final class Conditions { * @return A new condition. */ static Condition endsWith(Expression lhs, Expression rhs) { - return Comparison.create(lhs, "ENDS WITH", rhs); + return Comparison.create(lhs, Operator.ENDS_WITH, rhs); } /** @@ -130,7 +174,7 @@ public final class Conditions { */ static Condition isNull(Expression expression) { - return IsNull.create(expression, false); + return Comparison.create(Operator.IS_NULL, expression); } /** @@ -141,7 +185,7 @@ public final class Conditions { */ static Condition isNotNull(Expression expression) { - return IsNull.create(expression, true); + return Comparison.create(Operator.IS_NOT_NULL, expression); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java index 3dff7d00c..39a25a1c2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java @@ -53,6 +53,22 @@ public interface Expression extends Visitable { return Conditions.isNotEqualTo(this, rhs); } + default Condition lt(Expression rhs) { + return Conditions.lt(this, rhs); + } + + default Condition lte(Expression rhs) { + return Conditions.lte(this, rhs); + } + + default Condition gt(Expression rhs) { + return Conditions.gt(this, rhs); + } + + default Condition gte(Expression rhs) { + return Conditions.gte(this, rhs); + } + /** * Creates a condition that checks whether this {@code expression} is {@literal true}. * @@ -138,7 +154,7 @@ public interface Expression extends Visitable { default Condition isIn(Iterable ids) { return Comparison.create( this, - "in", + Operator.IN, new ListLiteral(ids)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java deleted file mode 100644 index 15671b7d3..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/IsNull.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.cypher; - -import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; -import org.springframework.util.Assert; - -/** - * A condition to check whether an expression is null. By negating it effectively becomes {@literal IsNotNull}. - * This condition can either be negated itself or through a {@link NotCondition}, which is the same style - * that Cypher supports as well. - * - * @author Michael J. Simons - * @since 1.0 - */ -@API(status = API.Status.INTERNAL, since = "1.0") -public final class IsNull implements Condition { - - /** - * Creates a new {@link IsNull IsNull-condition}. - * - * @param expression The expression to check for null, must not be {@literal null} - * @param negated A flag whether to turn this into a {@literal IsNotNull} - * @return A new condition - */ - static IsNull create(Expression expression, boolean negated) { - - Assert.notNull(expression, "Expression must not be null."); - - return new IsNull(expression, negated); - } - - /** The expression to check for null, must not be be {@literal null}. */ - private final Expression expression; - - /** - * Flag, if this is actually a IsNotNull. - */ - private final boolean negated; - - private IsNull(Expression expression, boolean negated) { - - this.expression = expression; - this.negated = negated; - } - - public boolean isNegated() { - return negated; - } - - @Override - public void accept(Visitor visitor) { - - visitor.enter(this); - expression.accept(visitor); - visitor.leave(this); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NestedExpression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NestedExpression.java new file mode 100644 index 000000000..73a6c2d61 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NestedExpression.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.springframework.data.neo4j.core.cypher.Expressions.*; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class NestedExpression implements Expression { + + private final Expression delegate; + + NestedExpression(Expression delegate) { + this.delegate = delegate; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + nameOrExpression(this.delegate).accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java index d899e605a..5ad102f14 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java @@ -38,7 +38,7 @@ public final class Operations { */ static Operation plus(Expression op1, Expression op2) { - return Operation.create(op1, Operator.PLUS, op2); + return Operation.create(op1, Operator.ADDITION, op2); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java index 79340c7bc..3505f3f65 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java @@ -22,7 +22,7 @@ import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; /** - * An operator + * An operator. See Operators. * * @author Michael J. Simons * @since 1.0 @@ -30,18 +30,64 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; @API(status = API.Status.INTERNAL, since = "1.0") public enum Operator implements Visitable { - /** - * The {@code +} operator. - */ - PLUS("+"); + // Mathematical operators + ADDITION("+"), + SUBTRACTION("-"), + + // Comparison operators + EQUALITY("="), + INEQUALITY("<>"), + LESS_THAN("<"), + GREATER_THAN(">"), + LESS_THAN_OR_EQUAL_TO("<="), + GREATER_THAN_OR_EQUAL_TO(">="), + IS_NULL("IS NULL", Type.POSTFIX), + IS_NOT_NULL("IS NOT NULL", Type.POSTFIX), + + STARTS_WITH("STARTS WITH"), + ENDS_WITH("ENDS WITH"), + CONTAINS("CONTAINS"), + + // Boolean operators + AND("AND"), + OR("OR"), + XOR("XOR"), + NOT("NOT", Type.PREFIX), + + // String operators + MATCHES("=~"), + + // List operators + IN("IN"); private final String representation; + private final Type type; + Operator(String representation) { + this(representation, Type.BINARY); + } + + Operator(String representation, Type type) { this.representation = representation; + this.type = type; } public String getRepresentation() { return representation; } + + public boolean isUnary() { + return type != Type.BINARY; + } + + public Type getType() { + return type; + } + + public enum Type { + BINARY, + PREFIX, + POSTFIX, + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index 63dee3a0a..725f445e2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -28,7 +28,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.data.neo4j.core.cypher.*; -import org.springframework.data.neo4j.core.cypher.CompoundCondition.LogicalOperator; import org.springframework.data.neo4j.core.cypher.support.ReflectiveVisitor; import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; import org.springframework.data.neo4j.core.cypher.support.Visitable; @@ -170,6 +169,14 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append(" AS ").append(aliased.getAlias()); } + void enter(NestedExpression nested) { + builder.append("("); + } + + void leave(NestedExpression nested) { + builder.append(")"); + } + void enter(Order order) { builder.append(" ORDER BY "); } @@ -205,35 +212,26 @@ class RenderingVisitor extends ReflectiveVisitor { .append(")"); } - void enter(Comparison comparison) { - builder - .append(" ") - .append(comparison.getComparator()) - .append(" "); - } - void enter(Operation operation) { builder.append("("); } void enter(Operator operator) { - builder - .append(" ") - .append(operator.getRepresentation()) - .append(" "); + + Operator.Type type = operator.getType(); + if (type == Operator.Type.BINARY || type == Operator.Type.POSTFIX) { + builder.append(" "); + } + builder.append(operator.getRepresentation()); + if (type == Operator.Type.BINARY || type == Operator.Type.PREFIX) { + builder.append(" "); + } } void leave(Operation operation) { builder.append(")"); } - void leave(IsNull isNull) { - builder - .append(" IS ") - .append(isNull.isNegated() ? "NOT " : "") - .append("NULL"); - } - void enter(CompoundCondition compoundCondition) { builder.append("("); } @@ -242,30 +240,12 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append(")"); } - void enter(LogicalOperator logicalOperator) { - builder - .append(" ") - .append(logicalOperator.getSymbol()) - .append(" "); - } - - void enter(NotCondition notCondition) { - builder - .append("NOT ("); - } - - void leave(NotCondition notCondition) { - builder - .append(")"); - } - void enter(Literal expression) { builder.append(expression.asString()); } void enter(Node node) { builder.append("("); - } void leave(Node node) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 70bfff07b..80faac30b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -18,15 +18,21 @@ */ package org.springframework.data.neo4j.repository.query; +import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import lombok.RequiredArgsConstructor; + import java.util.Iterator; import java.util.Optional; +import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.neo4j.core.cypher.Condition; +import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Property; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; @@ -69,7 +75,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { @Override protected Condition create(Part part, Iterator actualParameters) { - return createImpl(part); + return createImpl(part, actualParameters); } @Override @@ -79,7 +85,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { return create(part, actualParameters); } - return base.and(createImpl(part)); + return base.and(createImpl(part, actualParameters)); } @Override @@ -98,7 +104,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { return CypherRenderer.create().render(statement); } - private Condition createImpl(Part part) { + private Condition createImpl(Part part, Iterator actualParameters) { PersistentPropertyPath path = mappingContext .getPersistentPropertyPath(part.getProperty()); @@ -107,33 +113,43 @@ final class CypherQueryCreator extends AbstractQueryCreator { // TODO case insensitive (like, notlike, simpleProperty, negatedSimpleProperty) switch (part.getType()) { + case BETWEEN: + return betweenCondition(persistentProperty, actualParameters); case CONTAINING: return property(persistentProperty) - .contains(parameter(nextRequiredParameterNameOrIndex())); + .contains(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case ENDING_WITH: return property(persistentProperty) - .endsWith(parameter(nextRequiredParameterNameOrIndex())); + .endsWith(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case FALSE: return property(persistentProperty).isFalse(); + case GREATER_THAN: + return property(persistentProperty).gt(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + case GREATER_THAN_EQUAL: + return property(persistentProperty).gte(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + case LESS_THAN: + return property(persistentProperty).lt(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + case LESS_THAN_EQUAL: + return property(persistentProperty).lte(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case LIKE: - return likeCondition(persistentProperty); + return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex); case SIMPLE_PROPERTY: return property(persistentProperty) - .isEqualTo(parameter(nextRequiredParameterNameOrIndex())); + .isEqualTo(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case STARTING_WITH: return property(persistentProperty) - .startsWith(parameter(nextRequiredParameterNameOrIndex())); + .startsWith(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case REGEX: return property(persistentProperty) - .matches(parameter(nextRequiredParameterNameOrIndex())); + .matches(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case NEGATING_SIMPLE_PROPERTY: return property(persistentProperty) - .isNotEqualTo(parameter(nextRequiredParameterNameOrIndex())); + .isNotEqualTo(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case NOT_CONTAINING: return property(persistentProperty) - .contains(parameter(nextRequiredParameterNameOrIndex())).not(); + .contains(parameter(nextRequiredParameter(actualParameters).nameOrIndex)).not(); case NOT_LIKE: - return likeCondition(persistentProperty).not(); + return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex).not(); case TRUE: return property(persistentProperty).isTrue(); default: @@ -141,20 +157,58 @@ final class CypherQueryCreator extends AbstractQueryCreator { } } - private Condition likeCondition(Neo4jPersistentProperty persistentProperty) { + private Condition likeCondition(Neo4jPersistentProperty persistentProperty, String parameterName) { return property(persistentProperty) - .matches(literalOf(".*").plus(parameter(nextRequiredParameterNameOrIndex())).plus(literalOf(".*"))); + .matches(literalOf(".*").plus(parameter(parameterName)).plus(literalOf(".*"))); + } + + private Condition betweenCondition(Neo4jPersistentProperty persistentProperty, Iterator actualParameters) { + + Parameter lowerBoundOrRange = nextRequiredParameter(actualParameters); + + Property property = property(persistentProperty); + Condition betweenCondition = Conditions.noCondition(); + if (lowerBoundOrRange.value instanceof Range) { + Range range = (Range) lowerBoundOrRange.value; + + if (range.getLowerBound().isBounded()) { + Expression parameterPlaceholder = parameter(lowerBoundOrRange.nameOrIndex + ".lb"); + betweenCondition = betweenCondition.and(range.getLowerBound().isInclusive() ? + property.gte(parameterPlaceholder) : + property.gt(parameterPlaceholder)); + } + + if (range.getUpperBound().isBounded()) { + Expression parameterPlaceholder = parameter(lowerBoundOrRange.nameOrIndex + ".ub"); + betweenCondition = betweenCondition.and(range.getUpperBound().isInclusive() ? + property.lte(parameterPlaceholder) : + property.lt(parameterPlaceholder)); + } + } else { + Parameter upperBound = nextRequiredParameter(actualParameters); + betweenCondition = property.gte(parameter(lowerBoundOrRange.nameOrIndex)) + .and(property.lte(parameter(upperBound.nameOrIndex))); + } + return betweenCondition; } private Property property(Neo4jPersistentProperty persistentProperty) { return Cypher.property("n", persistentProperty.getPropertyName()); } - private String nextRequiredParameterNameOrIndex() { + private Parameter nextRequiredParameter(Iterator actualParameters) { if (!formalParameters.hasNext()) { throw new IllegalStateException("Not enough formal, bindable parameters for parts"); } - return formalParameters.next().getNameOrIndex(); + //System.out.println(actualParameters()); + return new Parameter(formalParameters.next().getNameOrIndex(), actualParameters.next()); + } + + @RequiredArgsConstructor(access = PACKAGE) + static class Parameter { + final String nameOrIndex; + + final Object value; } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index f16ee08ae..afc6ff3a1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -20,8 +20,10 @@ package org.springframework.data.neo4j.repository.query; import static java.util.stream.Collectors.*; +import java.util.HashMap; import java.util.Map; +import org.springframework.data.domain.Range; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -67,7 +69,8 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { String cypherQuery = queryCreator.createQuery(); Map boundedParameters = formalParameters .getBindableParameters().stream() - .collect(toMap(Neo4jQueryMethod.Neo4jParameter::getNameOrIndex, formalParameter -> parameters[formalParameter.getIndex()])); + .collect(toMap(Neo4jQueryMethod.Neo4jParameter::getNameOrIndex, + formalParameter -> convertParameter(parameters[formalParameter.getIndex()]))); return PreparedQuery.queryFor(super.domainType).withCypherQuery(cypherQuery) .withParameters(boundedParameters) @@ -75,6 +78,18 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { .build(); } + // TODO Have fun with a bunch of conversion services + static Object convertParameter(Object parameter) { + if (parameter instanceof Range) { + Range range = (Range) parameter; + Map map = new HashMap<>(); + range.getLowerBound().getValue().ifPresent(v -> map.put("lb", v)); + range.getUpperBound().getValue().ifPresent(v -> map.put("ub", v)); + return map; + } + return parameter; + } + @Override protected boolean isCountQuery() { return tree.isCountProjection(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java index 1d34d968a..9b581c7ae 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java @@ -32,13 +32,11 @@ class ComparisonTest { void preconditionsShouldBeAsserted() { Expression expression = Cypher.literalOf("Arbitrary expression"); - assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(null, "=", expression)) + assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(null, Operator.EQUALITY, expression)) .withMessage("Left expression must not be null."); - assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(expression, "=", null)) + assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(expression, Operator.EQUALITY, null)) .withMessage("Right expression must not be null."); assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(expression, null, expression)) - .withMessage("Comparator must not be empty."); - assertThatIllegalArgumentException().isThrownBy(() -> Comparison.create(expression, "", expression)) - .withMessage("Comparator must not be empty."); + .withMessage("Operator must not be empty."); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index d915b2e8f..d3535c737 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -928,4 +928,49 @@ public class CypherTest { "MATCH (u:`User`) WHERE u.a = $aParameter DETACH DELETE u RETURN u"); } } + + @Nested + class OperationsAndComparisions { + + @Test + void shouldRenderOperations() { + Statement statement; + statement = Cypher.match(Cypher.anyNode("n")) + .returning(literalOf(1).plus(literalOf(2))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN (1 + 2)"); + } + + @Test + void shouldRenderComparision() { + Statement statement; + statement = Cypher.match(Cypher.anyNode("n")) + .returning(literalOf(1).gt(literalOf(2))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN 1 > 2"); + + statement = Cypher.match(Cypher.anyNode("n")) + .returning(literalOf(1).gt(literalOf(2)).isTrue()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN (1 > 2) = true"); + + statement = Cypher.match(Cypher.anyNode("n")) + .returning(literalOf(1).gt(literalOf(2)).isTrue().isFalse()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN ((1 > 2) = true) = false"); + } + + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 0f3cd089f..0c5b47577 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.integration; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Range; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.transaction.annotation.Transactional; @@ -88,4 +89,16 @@ public interface PersonRepository extends Neo4jRepository findAllByFirstNameNotContaining(String name); List findAllByFirstNameEndingWith(String name); + + List findAllByPersonNumberIsLessThan(Long number); + + List findAllByPersonNumberIsLessThanEqual(Long number); + + List findAllByPersonNumberIsGreaterThanEqual(Long number); + + List findAllByPersonNumberIsGreaterThan(Long number); + + List findAllByPersonNumberIsBetween(Range range); + + List findAllByPersonNumberIsBetween(Long low, Long high); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java index c506746ca..16871f968 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java @@ -51,4 +51,6 @@ public class PersonWithAllConstructor { private final String sameValue; private final Boolean cool; + + private final Long personNumber; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index c80269c32..efeb2774b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.integration; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Range.Bound.*; import java.util.Arrays; import java.util.List; @@ -38,6 +39,8 @@ import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -64,7 +67,7 @@ class RepositoryIT { private static final String TEST_PERSON_SAMEVALUE = "SameValue"; static PersonWithAllConstructor personExample(String sameValue) { - return new PersonWithAllConstructor(null, null, null, sameValue, null); + return new PersonWithAllConstructor(null, null, null, sameValue, null, null); } private static Neo4jConnectionSupport neo4jConnectionSupport; @@ -91,14 +94,14 @@ class RepositoryIT { transaction.run("MATCH (n) detach delete n"); id1 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool return id(n)", + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber return id(n)", Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true) + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1) ).next().get(0).asLong(); id2 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool return id(n)", + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false) + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2) ).next().get(0).asLong(); transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = $name, n.first_name = $firstName", Values.parameters("name", TEST_PERSON1_NAME, "firstName", TEST_PERSON1_FIRST_NAME)); @@ -109,9 +112,9 @@ class RepositoryIT { // note that there is no id setting in the mapping right now person1 = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true); + true, 1L); person2 = new PersonWithAllConstructor(null, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false); + false, 2L); } @Test @@ -401,12 +404,99 @@ class RepositoryIT { @Test void findByEndingWith() { - List persons = repository.findAllByFirstNameContaining("nie"); + List persons = repository.findAllByFirstNameEndingWith("nie"); assertThat(persons) .hasSize(1) .contains(person1); } + @Test + void findByLessThan() { + + List persons = repository.findAllByPersonNumberIsLessThan(2L); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByLessThanEqual() { + + List persons = repository.findAllByPersonNumberIsLessThanEqual(2L); + assertThat(persons) + .containsExactlyInAnyOrder(person1, person2); + } + + @Test + void findByGreaterThanEqual() { + + List persons = repository.findAllByPersonNumberIsGreaterThanEqual(1L); + assertThat(persons) + .containsExactlyInAnyOrder(person1, person2); + } + + @Test + void findByGreaterThan() { + + List persons = repository.findAllByPersonNumberIsGreaterThan(1L); + assertThat(persons) + .hasSize(1) + .contains(person2); + } + + @Test + void findByBetweenRange() { + + List persons; + persons = repository.findAllByPersonNumberIsBetween(Range.from(inclusive(1L)).to(inclusive(2L))); + assertThat(persons) + .containsExactlyInAnyOrder(person1, person2); + + persons = repository.findAllByPersonNumberIsBetween(Range.from(inclusive(1L)).to(exclusive(2L))); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByPersonNumberIsBetween(Range.from(inclusive(1L)).to(unbounded())); + assertThat(persons) + .containsExactlyInAnyOrder(person1, person2); + + persons = repository.findAllByPersonNumberIsBetween(Range.from(exclusive(1L)).to(unbounded())); + assertThat(persons) + .hasSize(1) + .contains(person2); + + persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.unbounded()).to(inclusive(2L))); + assertThat(persons) + .containsExactlyInAnyOrder(person1, person2); + + persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.unbounded()).to(exclusive(2L))); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByPersonNumberIsBetween(Range.unbounded()); + assertThat(persons) + .containsExactlyInAnyOrder(person1, person2); + } + + @Test + void findByBetween() { + + List persons; + persons = repository.findAllByPersonNumberIsBetween(1L, 2L); + assertThat(persons) + .containsExactlyInAnyOrder(person1, person2); + + persons = repository.findAllByPersonNumberIsBetween(3L, 5L); + assertThat(persons).isEmpty(); + + persons = repository.findAllByPersonNumberIsBetween(2L, 3L); + assertThat(persons) + .hasSize(1) + .contains(person2); + } + @Configuration @EnableNeo4jRepositories @EnableTransactionManagement From ab86fdb520fba5ea4b90850b23dd06cff91814a1 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 15 May 2019 11:49:03 +0200 Subject: [PATCH 077/342] Provide the Neo4j Java-Driver type system to mapping functions. --- .../data/neo4j/core/DefaultNeo4jClient.java | 30 ++++++++++++---- .../data/neo4j/core/DefaultPreparedQuery.java | 9 ++--- .../core/DefaultReactiveNeo4jClient.java | 18 +++++++--- ...elegatingMappingFunctionWithNullCheck.java | 13 +++---- .../data/neo4j/core/Neo4jClient.java | 13 +++++-- .../data/neo4j/core/PreparedQuery.java | 10 +++--- .../core/SingleValueMappingFunction.java | 7 ++-- .../mapping/DefaultNeo4jMappingFunction.java | 7 ++-- .../core/mapping/Neo4jMappingContext.java | 5 +-- .../data/neo4j/core/schema/Schema.java | 7 ++-- .../support/SimpleNeo4jRepository.java | 7 ++-- ...atingMappingFunctionWithNullCheckTest.java | 9 ++--- .../data/neo4j/core/Neo4jClientTest.java | 32 ++++++++++------- .../neo4j/core/ReactiveNeo4jClientTest.java | 13 ++++++- .../core/SingleValueMappingFunctionTest.java | 16 +++++---- .../neo4j/core/TransactionHandlingTest.java | 34 ++++++++++++++----- .../Neo4jTransactionManagerTest.java | 8 +++-- 17 files changed, 164 insertions(+), 74 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index 267b942f0..4e903448f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -33,15 +33,18 @@ import java.util.Collection; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; import org.neo4j.driver.StatementRunner; import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.types.TypeSystem; /** * Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to and interact with the database. @@ -53,10 +56,15 @@ import org.neo4j.driver.summary.ResultSummary; class DefaultNeo4jClient implements Neo4jClient { private final Driver driver; + private final TypeSystem typeSystem; DefaultNeo4jClient(Driver driver) { this.driver = driver; + // This will go away + try (Session session = this.driver.session(t -> t.withDefaultAccessMode(AccessMode.READ))) { + typeSystem = session.typeSystem(); + } } AutoCloseableStatementRunner getStatementRunner(final String targetDatabase) { @@ -184,7 +192,7 @@ class DefaultNeo4jClient implements Neo4jClient { return new DefaultRecordFetchSpec<>( this.targetDatabase, this.cypherSupplier, - this.parameters, Record::asMap); + this.parameters, (t, r) -> r.asMap()); } @Override @@ -207,11 +215,11 @@ class DefaultNeo4jClient implements Neo4jClient { private final NamedParameters parameters; - private Function mappingFunction; + private BiFunction mappingFunction; @Override public RecordFetchSpec, Collection, T> mappedBy( - @SuppressWarnings("HiddenField") Function mappingFunction) { + @SuppressWarnings("HiddenField") BiFunction mappingFunction) { this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction); return this; @@ -222,7 +230,9 @@ class DefaultNeo4jClient implements Neo4jClient { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { StatementResult result = runWith(statementRunner); - return result.hasNext() ? Optional.of(mappingFunction.apply(result.single())) : Optional.empty(); + return result.hasNext() ? + Optional.of(mappingFunction.apply(typeSystem, result.single())) : + Optional.empty(); } } @@ -231,7 +241,7 @@ class DefaultNeo4jClient implements Neo4jClient { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { StatementResult result = runWith(statementRunner); - return result.stream().map(mappingFunction).findFirst(); + return result.stream().map(partialMappingFunction(typeSystem)).findFirst(); } } @@ -240,7 +250,7 @@ class DefaultNeo4jClient implements Neo4jClient { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { StatementResult result = runWith(statementRunner); - return result.stream().map(mappingFunction).collect(toList()); + return result.stream().map(partialMappingFunction(typeSystem)).collect(toList()); } } @@ -258,6 +268,14 @@ class DefaultNeo4jClient implements Neo4jClient { StatementResult result = statementRunner.run(statementTemplate, parameters.get()); return result; } + + /** + * @param typeSystem The actual type system + * @return The partially evaluated mapping function + */ + private Function partialMappingFunction(TypeSystem typeSystem) { + return r -> mappingFunction.apply(typeSystem, r); + } } @AllArgsConstructor diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java index eb66a2079..e473311ea 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java @@ -20,9 +20,10 @@ package org.springframework.data.neo4j.core; import java.util.Map; import java.util.Optional; -import java.util.function.Function; +import java.util.function.BiFunction; import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; import org.springframework.lang.Nullable; /** @@ -42,11 +43,11 @@ final class DefaultPreparedQuery implements PreparedQuery { private final Class resultType; private final String cypherQuery; private final Map parameters; - private final @Nullable Function mappingFunction; + private final @Nullable BiFunction mappingFunction; DefaultPreparedQuery(OptionalBuildSteps optionalBuildSteps) { this.resultType = optionalBuildSteps.resultType; - this.mappingFunction = (Function) optionalBuildSteps.mappingFunction; + this.mappingFunction = (BiFunction) optionalBuildSteps.mappingFunction; this.cypherQuery = optionalBuildSteps.cypherQuery; this.parameters = optionalBuildSteps.parameters; } @@ -57,7 +58,7 @@ final class DefaultPreparedQuery implements PreparedQuery { } @Override - public Optional> getOptionalMappingFunction() { + public Optional> getOptionalMappingFunction() { return Optional.ofNullable(mappingFunction); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 61f11e8a9..5fab5eb5a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -28,15 +28,19 @@ import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; +import org.neo4j.driver.Session; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.types.TypeSystem; import org.reactivestreams.Publisher; import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; import org.springframework.data.neo4j.core.Neo4jClient.OngoingBindSpec; @@ -55,10 +59,15 @@ import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { private final Driver driver; + private final TypeSystem typeSystem; DefaultReactiveNeo4jClient(Driver driver) { this.driver = driver; + // This will go away + try (Session session = this.driver.session(t -> t.withDefaultAccessMode(AccessMode.READ))) { + typeSystem = session.typeSystem(); + } } // Internal helper methods for managing transactional state @@ -173,7 +182,8 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { @Override public RecordFetchSpec>, Flux>, Map> fetch() { - return new DefaultReactiveRecordFetchSpec<>(targetDatabase, cypherSupplier, parameters, Record::asMap); + return new DefaultReactiveRecordFetchSpec<>(targetDatabase, cypherSupplier, parameters, + (t, r) -> r.asMap()); } @Override @@ -197,10 +207,10 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { private final NamedParameters parameters; - private Function mappingFunction; + private BiFunction mappingFunction; @Override - public RecordFetchSpec, Flux, T> mappedBy(Function mappingFunction) { + public RecordFetchSpec, Flux, T> mappedBy(BiFunction mappingFunction) { this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction); return this; @@ -213,7 +223,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { Flux executeWith(Tuple2> t, RxStatementRunner runner) { - return Flux.from(runner.run(t.getT1(), t.getT2()).records()).map(mappingFunction); + return Flux.from(runner.run(t.getT1(), t.getT2()).records()).map(r -> mappingFunction.apply(typeSystem, r)); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java index 6f64e2006..a7e2569f6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java @@ -18,9 +18,10 @@ */ package org.springframework.data.neo4j.core; -import java.util.function.Function; +import java.util.function.BiFunction; import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; /** * A delegating mapping function that first calls the delegate to get a record map and than checks the returned @@ -34,17 +35,17 @@ import org.neo4j.driver.Record; * @soundtrack Manowar - Fighting The World * @since 1.0 */ -class DelegatingMappingFunctionWithNullCheck implements Function { +class DelegatingMappingFunctionWithNullCheck implements BiFunction { - Function delegate; + BiFunction delegate; - DelegatingMappingFunctionWithNullCheck(Function delegate) { + DelegatingMappingFunctionWithNullCheck(BiFunction delegate) { this.delegate = delegate; } @Override - public T apply(Record record) { - T t = delegate.apply(record); + public T apply(TypeSystem typeSystem, Record record) { + T t = delegate.apply(typeSystem, record); if (t == null) { throw new IllegalStateException( "Mapping function " + delegate + " returned illegal null value for record " + record); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java index 817f4dd2d..dbd1e3cf1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core; import java.util.Collection; import java.util.Map; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -29,6 +30,7 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.StatementRunner; import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.types.TypeSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +47,7 @@ import org.slf4j.LoggerFactory; @API(status = API.Status.STABLE, since = "1.0") public interface Neo4jClient { - Logger cypherLog = LoggerFactory.getLogger(Neo4jClient.class.getPackage().getName() + ".cypher"); + Logger cypherLog = LoggerFactory.getLogger("org.springframework.data.neo4j.cypher"); static Neo4jClient create(Driver driver) { @@ -170,7 +172,14 @@ public interface Neo4jClient { */ interface MappingSpec extends RecordFetchSpec { - RecordFetchSpec mappedBy(Function mappingFunction); + /** + * The mapping function is responsible to turn one record into one domain object. It will receive the record + * itself and in addition, the type system that the Neo4j Java-Driver used while executing the query. + * + * @param mappingFunction The mapping function used to create new domain objects + * @return A specification how to fetch one or more records + */ + RecordFetchSpec mappedBy(BiFunction mappingFunction); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java index 7bb72f69a..f9d1782ef 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java @@ -22,10 +22,11 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.function.Function; +import java.util.function.BiFunction; import org.apiguardian.api.API; import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; import org.springframework.lang.Nullable; /** @@ -43,7 +44,7 @@ public interface PreparedQuery { Class getResultType(); - Optional> getOptionalMappingFunction(); + Optional> getOptionalMappingFunction(); String getCypherQuery(); @@ -72,7 +73,7 @@ public interface PreparedQuery { final Class resultType; final String cypherQuery; Map parameters = Collections.emptyMap(); - @Nullable Function mappingFunction; + @Nullable BiFunction mappingFunction; OptionalBuildSteps(Class resultType, String cypherQuery) { this.resultType = resultType; @@ -90,7 +91,8 @@ public interface PreparedQuery { return this; } - public OptionalBuildSteps usingMappingFunction(@Nullable Function newMappingFunction) { + public OptionalBuildSteps usingMappingFunction( + @Nullable BiFunction newMappingFunction) { this.mappingFunction = newMappingFunction; return this; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java index d2a43bdd6..a4ce19154 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java @@ -20,10 +20,11 @@ package org.springframework.data.neo4j.core; import lombok.RequiredArgsConstructor; -import java.util.function.Function; +import java.util.function.BiFunction; import org.neo4j.driver.Record; import org.neo4j.driver.Value; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes; /** @@ -33,12 +34,12 @@ import org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes; * @author Michael J. Simons */ @RequiredArgsConstructor -final class SingleValueMappingFunction implements Function { +final class SingleValueMappingFunction implements BiFunction { private final Class targetClass; @Override - public T apply(Record record) { + public T apply(TypeSystem typeSystem, Record record) { if (record.size() == 0) { throw new IllegalArgumentException("Record has no elements, cannot map nothing."); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java index 2b5d0e714..f38ddbe8c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java @@ -21,10 +21,11 @@ package org.springframework.data.neo4j.core.mapping; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.function.Function; +import java.util.function.BiFunction; import org.neo4j.driver.Record; import org.neo4j.driver.Value; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -44,14 +45,14 @@ import org.springframework.data.neo4j.core.schema.NodeDescription; */ @RequiredArgsConstructor @Slf4j -class DefaultNeo4jMappingFunction implements Function { +class DefaultNeo4jMappingFunction implements BiFunction { private final EntityInstantiators instantiators; private final Neo4jPersistentEntity nodeDescription; @Override - public T apply(Record record) { + public T apply(TypeSystem typeSystem, Record record) { try { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 377e0c7cc..01f8ea50e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -29,10 +29,11 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.function.Predicate; import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; @@ -159,7 +160,7 @@ public class Neo4jMappingContext } @Override - public Optional> getMappingFunctionFor(Class targetClass) { + public Optional> getMappingFunctionFor(Class targetClass) { if (!this.hasPersistentEntityFor(targetClass)) { return Optional.empty(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java index 9280ffad1..7eb9f469c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -21,10 +21,11 @@ package org.springframework.data.neo4j.core.schema; import java.util.Collection; import java.util.Optional; import java.util.Set; -import java.util.function.Function; +import java.util.function.BiFunction; import org.apiguardian.api.API; import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Cypher; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; @@ -93,7 +94,7 @@ public interface Schema { * @param Type of the target class * @return An empty optional if the target class is unknown, otherwise an optional containing a stateless, reusable mapping function */ - Optional> getMappingFunctionFor(Class targetClass); + Optional> getMappingFunctionFor(Class targetClass); /** * @param targetClass The target class to which to map to. @@ -102,7 +103,7 @@ public interface Schema { * @throws IllegalStateException When {@code targetClass} is not a managed class * @see #getMappingFunctionFor(Class) */ - default Function getRequiredMappingFunctionFor(Class targetClass) { + default BiFunction getRequiredMappingFunctionFor(Class targetClass) { return getMappingFunctionFor(targetClass).orElseThrow(() -> new UnknownEntityException(targetClass)); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index b72cde0af..517d7c096 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -24,10 +24,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.function.LongSupplier; import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Page; @@ -75,7 +76,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { private final Class nodeClass; private final NodeDescription nodeDescription; - private final Function mappingFunction; + private final BiFunction mappingFunction; private Expression idExpression; SimpleNeo4jRepository(NodeManager nodeManager, Neo4jMappingContext mappingContext, Class nodeClass) { @@ -355,7 +356,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { private PreparedQuery prepareQuery(Class resultType, Statement statement) { - Function mappingFunctionToUse = this.mappingFunction; + BiFunction mappingFunctionToUse = this.mappingFunction; if (!this.nodeClass.equals(resultType)) { mappingFunctionToUse = mappingContext.getMappingFunctionFor(resultType).orElse(null); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java index 2d56c92d5..bf9c30b26 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.*; import org.junit.jupiter.api.Test; import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; /** * @author Michael J. Simons @@ -32,15 +33,15 @@ class DelegatingMappingFunctionWithNullCheckTest { @Test void shouldBeHappyWithNonNullValues() { DelegatingMappingFunctionWithNullCheck function = new DelegatingMappingFunctionWithNullCheck( - record -> "Tada."); - assertThat(function.apply(mock(Record.class))).isEqualTo("Tada."); + (typeSystem, record) -> "Tada."); + assertThat(function.apply(mock(TypeSystem.class), mock(Record.class))).isEqualTo("Tada."); } @Test void shouldThrowExceptions() { DelegatingMappingFunctionWithNullCheck function = new DelegatingMappingFunctionWithNullCheck( - record -> null); - assertThatIllegalStateException().isThrownBy(() -> function.apply(mock(Record.class))) + (typeSystem, record) -> null); + assertThatIllegalStateException().isThrownBy(() -> function.apply(mock(TypeSystem.class), mock(Record.class))) .withMessageMatching("Mapping function .* returned illegal null value for record .*"); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java index 2daa512a5..9c344a4bb 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java @@ -32,6 +32,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; @@ -54,6 +55,7 @@ import org.neo4j.driver.StatementResult; import org.neo4j.driver.Values; import org.neo4j.driver.internal.SessionParameters; import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.types.TypeSystem; /** * @author Michael J. Simons @@ -72,6 +74,9 @@ class Neo4jClientTest { @Mock private Session session; + @Mock + private TypeSystem typeSystem; + @Mock private StatementResult statementResult; @@ -93,6 +98,7 @@ class Neo4jClientTest { .thenReturn(sessionParametersTemplate); when(driver.session(any(Consumer.class))).thenReturn(session); + when(session.typeSystem()).thenReturn(typeSystem); } @AfterEach @@ -140,7 +146,7 @@ class Neo4jClientTest { verify(statementResult).stream(); verify(record1).asMap(); verify(record2).asMap(); - verify(session).close(); + verify(session, times(2)).close(); } @Test @@ -170,7 +176,7 @@ class Neo4jClientTest { verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).stream(); verify(record1).asMap(); - verify(session).close(); + verify(session, times(2)).close(); } @Nested @@ -189,7 +195,7 @@ class Neo4jClientTest { verifyDatabaseSelection(DEFAULT_DATABASE_NAME); - verify(session).close(); + verify(session, times(2)).close(); } @Test @@ -205,7 +211,7 @@ class Neo4jClientTest { verifyDatabaseSelection("aDatabase"); - verify(session).close(); + verify(session, times(2)).close(); } } @@ -243,7 +249,7 @@ class Neo4jClientTest { verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).stream(); verify(record1).get("name"); - verify(session).close(); + verify(session, times(2)).close(); } @Test @@ -257,7 +263,7 @@ class Neo4jClientTest { assertThatIllegalStateException().isThrownBy(() -> client .newQuery("MATCH (n) RETURN n") - .fetchAs(BikeOwner.class).mappedBy(r -> { + .fetchAs(BikeOwner.class).mappedBy((t, r) -> { if (r == record1) { return new BikeOwner(r.get("name").asString(), Collections.emptyList()); } else { @@ -271,7 +277,7 @@ class Neo4jClientTest { verify(session).run(eq("MATCH (n) RETURN n"), argThat(new MapAssertionMatcher(Collections.emptyMap()))); verify(statementResult).stream(); verify(record1).get("name"); - verify(session).close(); + verify(session, times(2)).close(); } @Test @@ -299,7 +305,7 @@ class Neo4jClientTest { verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).consume(); - verify(session).close(); + verify(session, times(2)).close(); } @Test @@ -327,7 +333,7 @@ class Neo4jClientTest { verify(session).run(eq(cypher), anyMap()); verify(statementResult).hasNext(); verify(statementResult).single(); - verify(session).close(); + verify(session, times(2)).close(); } } @@ -354,7 +360,7 @@ class Neo4jClientTest { verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).consume(); - verify(session).close(); + verify(session, times(2)).close(); } static class BikeOwner { @@ -390,10 +396,10 @@ class Neo4jClientTest { } } - static class BikeOwnerReader implements Function { + static class BikeOwnerReader implements BiFunction { @Override - public BikeOwner apply(Record record) { + public BikeOwner apply(TypeSystem typeSystem, Record record) { return new BikeOwner(record.get("name").asString(), Collections.emptyList()); } } @@ -411,7 +417,7 @@ class Neo4jClientTest { } void verifyDatabaseSelection(String targetDatabase) { - verify(driver).session(sessionTemplateCaptor.capture()); + verify(driver, times(2)).session(sessionTemplateCaptor.capture()); sessionTemplateCaptor.getValue().accept(sessionParametersTemplate); verify(sessionParametersTemplate).withDatabase(targetDatabase); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java index 54b7fd582..632e14b2a 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java @@ -46,12 +46,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; +import org.neo4j.driver.Session; import org.neo4j.driver.Values; import org.neo4j.driver.internal.SessionParameters; import org.neo4j.driver.reactive.RxResult; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.Neo4jClientTest.Bike; import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwner; import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwnerBinder; @@ -67,6 +69,12 @@ class ReactiveNeo4jClientTest { @Mock private Driver driver; + @Mock + private Session goAwaySession; + + @Mock + private TypeSystem typeSystem; + private ArgumentCaptor sessionTemplateCaptor = ArgumentCaptor.forClass(Consumer.class); @Mock @@ -93,6 +101,9 @@ class ReactiveNeo4jClientTest { @BeforeEach void prepareMocks() { + when(driver.session(any(Consumer.class))).thenReturn(goAwaySession); + when(goAwaySession.typeSystem()).thenReturn(typeSystem); + when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) @@ -287,7 +298,7 @@ class ReactiveNeo4jClientTest { ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); Flux bikeOwners = client .newQuery("MATCH (n) RETURN n") - .fetchAs(BikeOwner.class).mappedBy(r -> { + .fetchAs(BikeOwner.class).mappedBy((t, r) -> { if (r == record1) { return new BikeOwner(r.get("name").asString(), Collections.emptyList()); } else { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java index d874cd35a..406965c4a 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java @@ -31,6 +31,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.neo4j.driver.Record; import org.neo4j.driver.Values; +import org.neo4j.driver.types.TypeSystem; /** * @author Michael J. Simons @@ -38,9 +39,12 @@ import org.neo4j.driver.Values; @ExtendWith(MockitoExtension.class) class SingleValueMappingFunctionTest { + private final TypeSystem typeSystem; + private final Record record; - SingleValueMappingFunctionTest(@Mock Record record) { + SingleValueMappingFunctionTest(@Mock TypeSystem typeSystem, @Mock Record record) { + this.typeSystem = typeSystem; this.record = record; } @@ -53,7 +57,7 @@ class SingleValueMappingFunctionTest { when(record.size()).thenReturn(0); SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(String.class); - assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(record)) + assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(typeSystem, record)) .withMessage("Record has no elements, cannot map nothing."); } @@ -63,7 +67,7 @@ class SingleValueMappingFunctionTest { when(record.size()).thenReturn(23); SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(String.class); - assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(record)) + assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(typeSystem, record)) .withMessage("Records with more than one value cannot be converted without a mapper."); } } @@ -75,7 +79,7 @@ class SingleValueMappingFunctionTest { when(record.get(0)).thenReturn(Values.NULL); SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(String.class); - assertThat(mappingFunction.apply(record)).isNull(); + assertThat(mappingFunction.apply(typeSystem, record)).isNull(); } @Test @@ -85,7 +89,7 @@ class SingleValueMappingFunctionTest { when(record.get(0)).thenReturn(Values.value("Guten Tag.")); SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(Period.class); - assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(record)) + assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(typeSystem, record)) .withMessage("java.time.Period is not assignable from java.lang.String"); } @@ -98,6 +102,6 @@ class SingleValueMappingFunctionTest { when(record.get(0)).thenReturn(Values.value(aDate)); SingleValueMappingFunction mappingFunction = new SingleValueMappingFunction<>(LocalDate.class); - assertThat(mappingFunction.apply(record)).isEqualTo(aDate); + assertThat(mappingFunction.apply(typeSystem, record)).isEqualTo(aDate); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java index a236849d5..4e8720df1 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java @@ -27,6 +27,8 @@ import reactor.test.StepVerifier; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,6 +44,7 @@ import org.neo4j.driver.internal.SessionParameters; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.reactive.RxTransaction; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.transaction.support.TransactionTemplate; @@ -56,15 +59,32 @@ class TransactionHandlingTest { @Mock private Driver driver; + @Mock + private Session session; + + @Mock + private TypeSystem typeSystem; + @Mock private SessionParameters.Template sessionParametersTemplate; + @BeforeEach + void prepareMocks() { + + when(driver.session(any(Consumer.class))).thenReturn(session); + when(session.typeSystem()).thenReturn(typeSystem); + } + + + @AfterEach + void verifyTypeSystemOnSession() { + + verify(session).typeSystem(); + } + @Nested class Neo4jClientTest { - @Mock - private Session session; - @Mock private Transaction transaction; @@ -75,7 +95,6 @@ class TransactionHandlingTest { public void shouldCallCloseOnSession() { ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); - when(driver.session(any(Consumer.class))).thenReturn(session); when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); @@ -88,12 +107,12 @@ class TransactionHandlingTest { s.run("MATCH (n) RETURN n"); } - verify(driver).session(consumerCaptor.capture()); + verify(driver, times(2)).session(consumerCaptor.capture()); consumerCaptor.getValue().accept(sessionParametersTemplate); verify(sessionParametersTemplate).withDatabase("aDatabase"); verify(session).run(any(String.class)); - verify(session).close(); + verify(session, times(2)).close(); verifyNoMoreInteractions(driver, sessionParametersTemplate, session, transaction); } @@ -103,7 +122,6 @@ class TransactionHandlingTest { AtomicBoolean transactionIsOpen = new AtomicBoolean(true); - when(driver.session(any(Consumer.class))).thenReturn(session); when(session.isOpen()).thenReturn(true); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); // Mock closing of the transaction @@ -130,7 +148,7 @@ class TransactionHandlingTest { verify(transaction).success(); verify(transaction).close(); verify(session).isOpen(); - verify(session).close(); + verify(session, times(2)).close(); verifyNoMoreInteractions(driver, session, transaction); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java index 03f4c1e93..7c85318fa 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java @@ -31,6 +31,7 @@ import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; @@ -46,6 +47,8 @@ class Neo4jTransactionManagerTest { @Mock private Session session; @Mock + private TypeSystem typeSystem; + @Mock private Transaction transaction; @Mock private StatementResult statementResult; @@ -57,6 +60,7 @@ class Neo4jTransactionManagerTest { when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(session.isOpen()).thenReturn(true); + when(session.typeSystem()).thenReturn(typeSystem); when(transaction.isOpen()).thenReturn(true, false); Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); @@ -67,7 +71,7 @@ class Neo4jTransactionManagerTest { txManager.commit(txStatus); - verify(driver).session(any(Consumer.class)); + verify(driver, times(2)).session(any(Consumer.class)); verify(session).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -76,6 +80,6 @@ class Neo4jTransactionManagerTest { verify(transaction).success(); verify(transaction).close(); - verify(session).close(); + verify(session, times(2)).close(); } } From 77bf59d78fb950a91a21f3f7a6f175be594d45e9 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 15 May 2019 12:19:05 +0200 Subject: [PATCH 078/342] Add support for internal id mapping. --- .../mapping/DefaultNeo4jMappingFunction.java | 87 ++++++++++++------- .../mapping/DefaultNeo4jPersistentEntity.java | 2 +- .../core/mapping/Neo4jMappingContext.java | 18 +++- .../core/schema/GraphPropertyDescription.java | 5 ++ .../data/neo4j/core/schema/IdDescription.java | 10 +-- .../neo4j/core/schema/NodeDescription.java | 10 +++ .../repository/query/CypherQueryCreator.java | 2 +- .../support/SimpleNeo4jRepository.java | 12 ++- .../data/neo4j/integration/RepositoryIT.java | 5 +- 9 files changed, 104 insertions(+), 47 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java index f38ddbe8c..56b1358cf 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java @@ -18,13 +18,20 @@ */ package org.springframework.data.neo4j.core.mapping; +import static java.util.stream.Collectors.*; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import java.util.List; +import java.util.Optional; import java.util.function.BiFunction; +import java.util.function.Predicate; import org.neo4j.driver.Record; import org.neo4j.driver.Value; +import org.neo4j.driver.types.Entity; +import org.neo4j.driver.types.Node; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.MappingException; @@ -55,51 +62,59 @@ class DefaultNeo4jMappingFunction implements BiFunction isNode = v -> v.hasType(typeSystem.NODE()); + List nodes = record.values().stream().filter(isNode).map(Value::asNode).collect(toList()); - for (Value value : record.values()) { - // todo - // 1. determine type of returned value: node or relationship - // 2. find most fitting label value.asNode().labels().forEach(); - // strictly assume just to work with nodes for now and every matching type label is the root node - if (value.asNode().hasLabel(nodeDescription.getPrimaryLabel())) { + // todo + // 1. determine type of returned value: node or relationship + // 2. find most fitting label value.asNode().labels().forEach(); + // For now strictly assume just to work with nodes for now and every matching type label is the root node + Optional optionalRootNode = nodes.stream() + .filter(node -> node.hasLabel(nodeDescription.getPrimaryLabel())).findFirst(); - PreferredConstructor persistenceConstructor = nodeDescription - .getPersistenceConstructor(); + return optionalRootNode.map(value -> { + PreferredConstructor persistenceConstructor = nodeDescription + .getPersistenceConstructor(); - // todo: neo4jPersistentEntity.requiresPropertyPopulation() + // todo: neo4jPersistentEntity.requiresPropertyPopulation() - T instance = instantiators.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, - new ParameterValueProvider() { - @Override - public Object getParameterValue(PreferredConstructor.Parameter parameter) { + T instance = instantiators.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, + new ParameterValueProvider() { + @Override + public Object getParameterValue(PreferredConstructor.Parameter parameter) { + boolean isInternalIdProperty = isInternalIdProperty(parameter); + if (isInternalIdProperty) { + return record.get(NodeDescription.NAME_OF_INTERNAL_ID, value.id()); + } else { String graphPropertyName = getGraphPropertyNameFor(parameter); return getValueFor(graphPropertyName, value); } - }); + } + }); - PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(instance); - if (nodeDescription.requiresPropertyPopulation()) { - nodeDescription.doWithProperties((Neo4jPersistentProperty property) -> { - if (persistenceConstructor.isConstructorParameter(property)) { - return; - } + PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(instance); + if (nodeDescription.requiresPropertyPopulation()) { + nodeDescription.doWithProperties((Neo4jPersistentProperty property) -> { + if (persistenceConstructor.isConstructorParameter(property)) { + return; + } - String graphPropertyName = property.getPropertyName(); - propertyAccessor.setProperty(property, getValueFor(graphPropertyName, value)); - }); - instance = propertyAccessor.getBean(); - } - - return instance; + String graphPropertyName = property.getPropertyName(); + propertyAccessor.setProperty(property, getValueFor(graphPropertyName, value)); + }); + instance = propertyAccessor.getBean(); } - } + + return instance; + }).orElseGet(() -> { + log.warn("Could not find mappable nodes or relationships inside {} for {}", record, nodeDescription); + return null; + }); } catch (Exception e) { throw new MappingException("Error mapping " + record.toString(), e); } - log.warn("Could not find mappable nodes or relationships inside {} for {}", record, nodeDescription); - return null; } /** @@ -119,7 +134,17 @@ class DefaultNeo4jMappingFunction implements BiFunction extends BasicPersistentEntity new IdDescription()); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 01f8ea50e..b8fb5083f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.mapping; +import static org.springframework.data.neo4j.core.schema.NodeDescription.*; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -43,8 +45,12 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Expression; +import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Node; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Relationship; import org.springframework.data.neo4j.core.schema.RelationshipDescription; @@ -173,8 +179,16 @@ public class Neo4jMappingContext @Override public OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition) { - Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named("n"); - return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())).with(rootNode); + Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); + IdDescription idDescription = nodeDescription.getIdDescription(); + + List expressions = new ArrayList<>(); + expressions.add(rootNode); + if (idDescription.getIdStrategy() == Id.Strategy.INTERNAL) { + expressions.add(Functions.id(rootNode).as(NAME_OF_INTERNAL_ID)); + } + return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())) + .with(expressions.toArray(new Expression[expressions.size()])); } private Collection computeRelationshipsOf(String primaryLabel) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java index 7ef0354c5..f45806828 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java @@ -43,4 +43,9 @@ public interface GraphPropertyDescription { * @return The name of the property as stored in the graph. */ String getPropertyName(); + + /** + * @return True if this property is the id property. + */ + boolean isIdProperty(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java index 1234aa1ca..f6954edbe 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java @@ -45,17 +45,17 @@ public final class IdDescription { /** * The property that stores the id if applicable. */ - private @Nullable final String graphPropertyName; + private @Nullable final GraphPropertyDescription graphProperty; public IdDescription() { this(Id.Strategy.INTERNAL, null, null); } public IdDescription(Id.Strategy idStrategy, - @Nullable Class idGeneratorClass, @Nullable String graphPropertyName) { + @Nullable Class idGeneratorClass, @Nullable GraphPropertyDescription graphProperty) { this.idStrategy = idStrategy; this.idGeneratorClass = idGeneratorClass; - this.graphPropertyName = graphPropertyName; + this.graphProperty = graphProperty; } public Id.Strategy getIdStrategy() { @@ -66,7 +66,7 @@ public final class IdDescription { return Optional.ofNullable(idGeneratorClass); } - public Optional getGraphPropertyName() { - return Optional.ofNullable(graphPropertyName); + public Optional getOptionalGraphPropertyName() { + return Optional.ofNullable(graphProperty).map(GraphPropertyDescription::getPropertyName); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java index 090676a5f..710e7993c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -34,6 +34,9 @@ import org.apiguardian.api.API; @API(status = API.Status.INTERNAL, since = "1.0") public interface NodeDescription { + String NAME_OF_ROOT_NODE = "n"; + String NAME_OF_INTERNAL_ID = "__internalNeo4jId__"; + /** * @return The primary label of this entity inside Neo4j. */ @@ -61,4 +64,11 @@ public interface NodeDescription { * @return An empty optional if there is no property known for the given field. */ Optional getGraphProperty(String fieldName); + + /** + * @return True if entities for this node use Neo4j internal ids. + */ + default boolean useInternalIds() { + return this.getIdDescription().getIdStrategy() == Id.Strategy.INTERNAL; + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 80faac30b..d6d5d0ab8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -98,7 +98,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { Statement statement = mappingContext .prepareMatchOf(nodeDescription, Optional.of(condition)) - .returning(Cypher.symbolicName("n")) + .returning(Cypher.asterisk()) .build(); return CypherRenderer.create().render(statement); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 517d7c096..a172fe229 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -96,7 +96,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { break; case ASSIGNED: case GENERATED: - idExpression = idDescription.getGraphPropertyName() + idExpression = idDescription.getOptionalGraphPropertyName() .map(propertyName -> property(rootNode.getName(), propertyName)).get(); break; default: @@ -325,9 +325,13 @@ class SimpleNeo4jRepository implements Neo4jRepository { continue; } - String propertyName = graphProperty.getPropertyName(); - - Condition condition = property(rootNode, propertyName).isEqualTo(literalOf(propertyValue)); + Condition condition; + if (graphProperty.isIdProperty() && probeNodeDescription.useInternalIds()) { + condition = idExpression.isEqualTo(literalOf(propertyValue)); + } else { + String propertyName = graphProperty.getPropertyName(); + condition = property(rootNode, propertyName).isEqualTo(literalOf(propertyValue)); + } conditionList.add(condition); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index efeb2774b..790f59c44 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -110,10 +110,9 @@ class RepositoryIT { transaction.success(); transaction.close(); - // note that there is no id setting in the mapping right now - person1 = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, + person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L); - person2 = new PersonWithAllConstructor(null, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, + person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L); } From c0e863326d8ea639b6ed9a6739a291e042c363c5 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 15 May 2019 16:06:50 +0200 Subject: [PATCH 079/342] Polish property mapping. --- .../mapping/DefaultNeo4jMappingFunction.java | 50 +++++------------ .../DefaultNeo4jPersistentProperty.java | 6 +++ .../core/schema/GraphPropertyDescription.java | 5 ++ .../DefaultNeo4jMappingFunctionTest.java | 54 ------------------- 4 files changed, 24 insertions(+), 91 deletions(-) delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunctionTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java index 56b1358cf..22ce6774c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java @@ -38,7 +38,6 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.model.ParameterValueProvider; -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; /** @@ -72,7 +71,7 @@ class DefaultNeo4jMappingFunction implements BiFunction optionalRootNode = nodes.stream() .filter(node -> node.hasLabel(nodeDescription.getPrimaryLabel())).findFirst(); - return optionalRootNode.map(value -> { + return optionalRootNode.map(rootNode -> { PreferredConstructor persistenceConstructor = nodeDescription .getPersistenceConstructor(); @@ -83,13 +82,9 @@ class DefaultNeo4jMappingFunction implements BiFunction implements BiFunction implements BiFunction - * Otherwise the name of the property in the graph will be returned. - * - * @param parameter The constructor parameter for which the property name is needed - * @return The name of the graph property or the name of {@code parameter} if a corresponding property was not found - */ - String getGraphPropertyNameFor(PreferredConstructor.Parameter parameter) { - return nodeDescription - .getGraphProperty(parameter.getName()) - .map(GraphPropertyDescription::getPropertyName) - .orElse(parameter.getName()); - } - - /** - * @param parameter - * @return True if the given parameter fits the internal id property. - */ - Boolean isInternalIdProperty(PreferredConstructor.Parameter parameter) { - - return nodeDescription.useInternalIds() && nodeDescription.getRequiredIdProperty().getFieldName() - .equals(parameter.getName()); + Object extractValueOf(Neo4jPersistentProperty property, Record record, Entity propertyContainer) { + if (property.isInternalIdProperty()) { + return record.get(NodeDescription.NAME_OF_INTERNAL_ID, propertyContainer.id()); + } else { + String graphPropertyName = property.getPropertyName(); + return getValueFor(graphPropertyName, propertyContainer); + } } Object getValueFor(String graphProperty, Entity from) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java index c5b6ffd90..058d1eae4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java @@ -95,4 +95,10 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty new MappingException("This property is not mapped to a Graph property!")); } + + @Override + public boolean isInternalIdProperty() { + + return this.isIdProperty() && ((Neo4jPersistentEntity) this.getOwner()).useInternalIds(); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java index f45806828..442000e2f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/GraphPropertyDescription.java @@ -48,4 +48,9 @@ public interface GraphPropertyDescription { * @return True if this property is the id property. */ boolean isIdProperty(); + + /** + * @return True, if this property is the id property and the owner uses internal ids. + */ + boolean isInternalIdProperty(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunctionTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunctionTest.java deleted file mode 100644 index 4bc7301a5..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunctionTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.mapping; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.util.Optional; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.mapping.PreferredConstructor; - -/** - * @author Michael J. Simons - */ -class DefaultNeo4jMappingFunctionTest { - - @Nested - class PropertyNames { - - @Test - void shouldDefaultToParameterNames() { - Neo4jPersistentEntity nodeDescription = mock(Neo4jPersistentEntity.class); - when(nodeDescription.getGraphProperty(Mockito.eq("aParameter"))).thenReturn(Optional.empty()); - - PreferredConstructor.Parameter parameter = mock(PreferredConstructor.Parameter.class); - when(parameter.getName()).thenReturn("aParameter"); - - DefaultNeo4jMappingFunction mappingFunction = new DefaultNeo4jMappingFunction( - mock(EntityInstantiators.class), nodeDescription); - - assertThat(mappingFunction.getGraphPropertyNameFor(parameter)).isEqualTo("aParameter"); - } - } -} From 639acd90b0f0fc4b4b9ff17c653da2aac80a61ae Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 15 May 2019 17:33:38 +0200 Subject: [PATCH 080/342] Polish detection of associations. This includes a dedicated instance of SimpleTypeHolder and checks for collections and maps. --- .../DefaultNeo4jPersistentProperty.java | 15 ++++- .../DefaultRelationshipDescription.java | 6 ++ .../core/mapping/Neo4jMappingContext.java | 8 ++- .../neo4j/core/schema/Neo4jSimpleTypes.java | 58 +++++++++++++++++-- .../core/mapping/Neo4jMappingContextTest.java | 32 +++++++++- 5 files changed, 111 insertions(+), 8 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java index 058d1eae4..607a45829 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java @@ -35,6 +35,7 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty> graphPropertyName; + private final Lazy isAssociation; /** * Creates a new {@link AnnotationBasedPersistentProperty}. @@ -50,6 +51,17 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty computeGraphPropertyName()); + this.isAssociation = Lazy.of(() -> { + + Class targetType = getType(); + if (isCollectionLike()) { + targetType = getComponentType(); + } else if (isMap()) { + targetType = getMapValueType(); + } + + return !simpleTypeHolder.isSimpleType(targetType); + }); } @Override @@ -60,7 +72,8 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty> relationshipsByPrimaryLabel = new ConcurrentHashMap<>(); + public Neo4jMappingContext() { + super.setSimpleTypeHolder(Neo4jSimpleTypes.SIMPLE_TYPE_HOLDER); + } + /* * (non-Javadoc) * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) @@ -211,7 +216,8 @@ public class Neo4jMappingContext type = association.getInverse().getName(); } - relationships.add(new DefaultRelationshipDescription(type, obverseOwner.getPrimaryLabel())); + relationships + .add(new DefaultRelationshipDescription(type, obverseOwner.getPrimaryLabel(), association.getInverse().isMap())); }); return Collections.unmodifiableCollection(relationships); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java index f67568794..8b92b9ed2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java @@ -18,14 +18,32 @@ */ package org.springframework.data.neo4j.core.schema; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; import java.util.Optional; +import java.util.Set; import org.apiguardian.api.API; import org.neo4j.driver.Value; +import org.neo4j.driver.types.IsoDuration; +import org.neo4j.driver.types.Path; +import org.neo4j.driver.types.Point; +import org.neo4j.driver.types.Relationship; +import org.springframework.data.mapping.model.SimpleTypeHolder; /** - * A list of Neo4j simple types: All attributes that can be mapped to a property. There is never a relationship - * established for attributes of a node that are simple types. + * A list of Neo4j simple types: All attributes that can be mapped to a property. Some special logic has to be applied + * for domain attributes of the collection types {@link java.util.List} and {@link java.util.Map}. Those can be mapped + * to simple properties as well as to relationships to other things. + *

+ * The Java driver itself has a good overview of the supported types: + * The Cypher type system. * * @author Michael J. Simons * @since 1.0 @@ -33,6 +51,40 @@ import org.neo4j.driver.Value; @API(status = API.Status.INTERNAL, since = "1.0") public final class Neo4jSimpleTypes { + public static final Set> NEO4J_NATIVE_TYPES; + + static { + Set> neo4jNativeTypes = new HashSet<>(); + neo4jNativeTypes.add(void.class); + neo4jNativeTypes.add(Void.class); + neo4jNativeTypes.add(Map.class); + neo4jNativeTypes.add(boolean.class); + neo4jNativeTypes.add(Boolean.class); + neo4jNativeTypes.add(long.class); + neo4jNativeTypes.add(Long.class); + neo4jNativeTypes.add(String.class); + neo4jNativeTypes.add(byte[].class); + neo4jNativeTypes.add(LocalDate.class); + neo4jNativeTypes.add(OffsetTime.class); + neo4jNativeTypes.add(LocalTime.class); + neo4jNativeTypes.add(ZonedDateTime.class); + neo4jNativeTypes.add(LocalDateTime.class); + neo4jNativeTypes.add(IsoDuration.class); + neo4jNativeTypes.add(Point.class); + neo4jNativeTypes.add(Node.class); + neo4jNativeTypes.add(Relationship.class); + neo4jNativeTypes.add(Path.class); + + NEO4J_NATIVE_TYPES = Collections.unmodifiableSet(neo4jNativeTypes); + } + + /** + * The simple types we support plus all the simple types recognized by Spring. + */ + // TODO We need a conversion for some. + // TODO Add some special treatment for Period/Duration vs IsoDuration as well as the spatial types. + public static final SimpleTypeHolder SIMPLE_TYPE_HOLDER = new SimpleTypeHolder(NEO4J_NATIVE_TYPES, true); + /** * Converts the given Neo4j driver value into the designated type. * @@ -49,8 +101,6 @@ public final class Neo4jSimpleTypes { return null; } - // TODO Add some special treatment for Period/Duration vs IsoDuration as well as the spatial types. - return o.filter(v -> targetClass.isAssignableFrom(v.getClass())) .map(targetClass::cast) .orElseThrow(() -> new IllegalArgumentException( diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index 442c01bb7..34f06a39d 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -24,9 +24,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.springframework.data.mapping.Association; import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -74,7 +76,8 @@ public class Neo4jMappingContextTest { assertThat(description.getIdDescription().getIdStrategy()) .isEqualTo(Id.Strategy.ASSIGNED); - Collection expectedRelationships = Arrays.asList("[:owner] -> (:User)", "[:renter] -> (:User)"); + Collection expectedRelationships = Arrays + .asList("[:owner] -> (:User)", "[:renter] -> (:User)", "[:dynamicRelationships] -> (:User)"); Collection relationships = schema .getRelationshipsOf(description.getPrimaryLabel()); @@ -82,8 +85,17 @@ public class Neo4jMappingContextTest { .allMatch(d -> expectedRelationships .contains(String.format("[:%s] -> (:%s)", d.getType(), d.getTarget()))); }); - } + Neo4jPersistentEntity bikeNodeEntity = schema.getPersistentEntity(BikeNode.class); + + assertThat(bikeNodeEntity.getPersistentProperty("owner").isAssociation()).isTrue(); + assertThat(bikeNodeEntity.getPersistentProperty("renter").isAssociation()).isTrue(); + assertThat(bikeNodeEntity.getPersistentProperty("dynamicRelationships").isAssociation()).isTrue(); + assertThat(bikeNodeEntity.getPersistentProperty("someValues").isAssociation()).isFalse(); + assertThat(bikeNodeEntity.getPersistentProperty("someMoreValues").isAssociation()).isFalse(); + assertThat(bikeNodeEntity.getPersistentProperty("evenMoreValues").isAssociation()).isFalse(); + assertThat(bikeNodeEntity.getPersistentProperty("funnyDynamicProperties").isAssociation()).isFalse(); + } @Test void shouldPreventIllegalIdAnnotations() { @@ -102,6 +114,15 @@ public class Neo4jMappingContextTest { assertThat(schema.getMappingFunctionFor(UserNode.class)).isEmpty(); } + @Test + void targetTypeOfAssociationsShouldBeKnownToTheMappingContext() { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + Neo4jPersistentEntity bikeNodeEntity = schema.getPersistentEntity(BikeNode.class); + bikeNodeEntity.doWithAssociations((Association association) -> + assertThat(schema.getMappingFunctionFor(association.getInverse().getAssociationTargetType())).isPresent()); + } + @Node("User") static class UserNode { @@ -125,6 +146,13 @@ public class Neo4jMappingContextTest { UserNode owner; List renter; + + Map dynamicRelationships; + + List someValues; + String[] someMoreValues; + byte[] evenMoreValues; + Map funnyDynamicProperties; } static class TripNode { From e489bcc7285b2c65d9360ea6de043f72d60d1dba Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 15 May 2019 22:02:34 +0200 Subject: [PATCH 081/342] Polish findByExample support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findByExample now uses prepared statements with parameters and supports all matcher configurations for strings. This was necessary as I wanted to add date properties to the person, which don’t work as ObjectLiterals. --- .../data/neo4j/core/NamedParameters.java | 2 +- .../data/neo4j/core/cypher/Functions.java | 16 +- .../DefaultNeo4jPersistentProperty.java | 14 +- .../core/schema/GraphPropertyDescription.java | 12 ++ .../support/SimpleNeo4jRepository.java | 166 +++++++++++++----- .../data/neo4j/integration/RepositoryIT.java | 46 +++++ 6 files changed, 204 insertions(+), 52 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java index e0fdc06c4..b045a3fcb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java @@ -83,6 +83,6 @@ final class NamedParameters { .entrySet() .stream() .map(e -> String.format("%s: %s", e.getKey(), e.getValue())) - .collect(Collectors.joining(", ", "params {", "}")); + .collect(Collectors.joining(", ", ":params {", "}")); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java index 9bd2bfec0..f1c94b7d1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java @@ -83,7 +83,7 @@ public final class Functions { } /** - * Creates a function invocation for the {@code } function. + * Creates a function invocation for the {@code coalesce()} function. * See coalesce. * * @param expressions One or more expressions to be coalesced @@ -97,6 +97,20 @@ public final class Functions { return new FunctionInvocation("coalesce", expressions); } + /** + * Creates a function invocation for the {@code toLower()} function. + * See toLower. + * + * @param expression An expression resolving to a string + * @return A function call for {@code toLower()} for one expression + */ + public static FunctionInvocation toLower(Expression expression) { + + Assert.notNull(expression, "The expression for toLower() is required."); + + return new FunctionInvocation("toLower", expression); + } + private Functions() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java index 607a45829..75fe4f104 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java @@ -53,13 +53,7 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty computeGraphPropertyName()); this.isAssociation = Lazy.of(() -> { - Class targetType = getType(); - if (isCollectionLike()) { - targetType = getComponentType(); - } else if (isMap()) { - targetType = getMapValueType(); - } - + Class targetType = getActualType(); return !simpleTypeHolder.isSimpleType(targetType); }); } @@ -114,4 +108,10 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty getActualType(); + + /** + * @return Whether this property describes a relationship or not. + */ + boolean isRelationship(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index a172fe229..0bb903f5a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -18,11 +18,18 @@ */ package org.springframework.data.neo4j.repository.support; +import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.core.schema.NodeDescription.*; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; -import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; import java.util.function.LongSupplier; @@ -31,6 +38,8 @@ import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.ExampleMatcher.MatchMode; +import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -53,6 +62,7 @@ import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.support.PageableExecutionUtils; +import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -65,6 +75,7 @@ import org.springframework.transaction.annotation.Transactional; */ @Repository @Transactional(readOnly = true) +@Slf4j class SimpleNeo4jRepository implements Neo4jRepository { private static final Renderer renderer = CypherRenderer.create(); @@ -133,16 +144,18 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Page findAll(Example example, Pageable pageable) { + Predicate predicate = createPredicate(example); StatementBuilder.OngoingMatchAndReturn returning = mappingContext - .prepareMatchOf(nodeDescription, Optional.of(createAndConditions(example))).returning(asterisk()); + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)).returning(asterisk()); StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); Statement statement = returningWithPaging.build(); - List allResult = nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); + List page = nodeManager + .toExecutableQuery(prepareQuery(example.getProbeType(), statement, predicate.parameters)).getResults(); LongSupplier totalCountSupplier = this::count; - return (Page) PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier); + return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); } private StatementBuilder.BuildableMatch addPagingParameter(Pageable pageable, StatementBuilder.OngoingMatchAndReturn returning) { @@ -206,7 +219,8 @@ class SimpleNeo4jRepository implements Neo4jRepository { Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) .returning(Functions.count(asterisk())).build(); - return nodeManager.toExecutableQuery(prepareQuery(Long.class, statement)).getRequiredSingleResult(); + return nodeManager.toExecutableQuery(prepareQuery(Long.class, statement, Collections.emptyMap())) + .getRequiredSingleResult(); } @Override @@ -240,50 +254,52 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Optional findOne(Example example) { - NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + Predicate predicate = createPredicate(example); Statement statement = mappingContext - .prepareMatchOf(probeNodeDescription, Optional.of(createAndConditions(example))) + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(asterisk()) .build(); - return nodeManager.toExecutableQuery(prepareQuery(example.getProbeType(), statement)) - .getSingleResult(); + PreparedQuery preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters); + return nodeManager.toExecutableQuery(preparedQuery).getSingleResult(); } @Override public Iterable findAll(Example example) { - NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); - Statement statement = mappingContext.prepareMatchOf(probeNodeDescription, Optional.of(createAndConditions(example))) + Predicate predicate = createPredicate(example); + Statement statement = mappingContext.prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(asterisk()) .build(); - return nodeManager.toExecutableQuery( - prepareQuery(example.getProbeType(), statement)).getResults(); + PreparedQuery preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters); + return nodeManager.toExecutableQuery(preparedQuery).getResults(); } @Override public Iterable findAll(Example example, Sort sort) { - NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + Predicate predicate = createPredicate(example); Statement statement = mappingContext - .prepareMatchOf(probeNodeDescription, Optional.of(createAndConditions(example))) + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(asterisk()) .orderBy(createSort(sort)).build(); - return nodeManager.toExecutableQuery(prepareQuery(example.getProbeType(), statement)).getResults(); + PreparedQuery preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters); + return nodeManager.toExecutableQuery(preparedQuery).getResults(); } @Override public long count(Example example) { - NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + Predicate predicate = createPredicate(example); Statement statement = mappingContext - .prepareMatchOf(probeNodeDescription, Optional.of(createAndConditions(example))) + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(Functions.count(asterisk())) .build(); - return nodeManager.toExecutableQuery(prepareQuery(Long.class, statement)).getRequiredSingleResult(); + PreparedQuery preparedQuery = prepareQuery(Long.class, statement, predicate.parameters); + return nodeManager.toExecutableQuery(preparedQuery).getRequiredSingleResult(); } @Override @@ -291,51 +307,113 @@ class SimpleNeo4jRepository implements Neo4jRepository { return findAll(example).iterator().hasNext(); } - private Condition createAndConditions(Example example) { + /** + * This wraps all information necessary to predicate a match: A root condition and actual parameters to fill + * in formal parameters inside the condition. + */ + @RequiredArgsConstructor(access = PRIVATE) + private static class Predicate { - List conditionsFromProperties = createConditionsFromProperties(example); + private final NodeDescription nodeDescription; - Condition result = Conditions.noCondition(); - for (Condition condition : conditionsFromProperties) { - result = result.and(condition); + private Condition condition = Conditions.noCondition(); + + private final Map parameters = new HashMap<>(); + + void add(MatchMode matchMode, Condition additionalCondition) { + + switch (matchMode) { + case ALL: + this.condition = this.condition.and(additionalCondition); + break; + case ANY: + this.condition = this.condition.or(additionalCondition); + break; + default: + throw new IllegalArgumentException("Unsupported match mode: " + matchMode); + } } - return result; - } - private List createConditionsFromProperties(Example example) { + private Predicate createPredicate(Example example) { NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); - SymbolicName rootNode = Cypher.symbolicName("n"); + SymbolicName rootNode = Cypher.symbolicName(NAME_OF_ROOT_NODE); Collection graphProperties = probeNodeDescription.getGraphProperties(); DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); ExampleMatcher matcher = example.getMatcher(); + MatchMode mode = matcher.getMatchMode(); + ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(matcher); - List conditionList = new ArrayList<>(); - + Predicate predicate = new Predicate(probeNodeDescription); for (GraphPropertyDescription graphProperty : graphProperties) { - String fieldName = graphProperty.getFieldName(); - if (matcher.isIgnoredPath(fieldName)) { + + // TODO Relationships are not traversed. + + String currentPath = graphProperty.getFieldName(); + if (matcherAccessor.isIgnoredPath(currentPath)) { continue; } - Object propertyValue = beanWrapper.getPropertyValue(fieldName); + boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.useInternalIds(); + String propertyName = graphProperty.getPropertyName(); - if (matcher.getNullHandler() != ExampleMatcher.NullHandler.INCLUDE && propertyValue == null) { + PropertyValueTransformer transformer = matcherAccessor.getValueTransformerForPath(currentPath); + Optional optionalValue = transformer + .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); + + if (!optionalValue.isPresent()) { + if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) { + predicate.add(mode, property(rootNode, propertyName).isNull()); + } continue; } - Condition condition; - if (graphProperty.isIdProperty() && probeNodeDescription.useInternalIds()) { - condition = idExpression.isEqualTo(literalOf(propertyValue)); + if (graphProperty.isRelationship()) { + log.error("Querying by example does not support traversing of relationships."); + } else if (graphProperty.isIdProperty() && probeNodeDescription.useInternalIds()) { + predicate.add(mode, idExpression.isEqualTo(literalOf(optionalValue.get()))); } else { - String propertyName = graphProperty.getPropertyName(); - condition = property(rootNode, propertyName).isEqualTo(literalOf(propertyValue)); + Expression property = property(rootNode, propertyName); + Expression parameter = parameter(propertyName); + Condition condition = property.isEqualTo(parameter); + + if (String.class.equals(graphProperty.getActualType())) { + + if (matcherAccessor.isIgnoreCaseForPath(currentPath)) { + property = Functions.toLower(property); + parameter = Functions.toLower(parameter); + } + + switch (matcherAccessor.getStringMatcherForPath(currentPath)) { + case DEFAULT: + case EXACT: + // This needs to be recreated as both property and parameter might have changed above + condition = property.isEqualTo(parameter); + break; + case CONTAINING: + condition = property.contains(parameter); + break; + case STARTING: + condition = property.startsWith(parameter); + break; + case ENDING: + condition = property.endsWith(parameter); + break; + case REGEX: + condition = property.matches(parameter); + break; + default: + throw new IllegalArgumentException( + "Unsupported StringMatcher " + matcherAccessor.getStringMatcherForPath(currentPath)); + } + } + predicate.add(mode, condition); + predicate.parameters.put(propertyName, optionalValue.get()); } - conditionList.add(condition); } - return conditionList; + return predicate; } private SortItem[] createSort(Sort sort) { @@ -355,10 +433,11 @@ class SimpleNeo4jRepository implements Neo4jRepository { } private PreparedQuery prepareQuery(Statement statement) { - return prepareQuery(nodeClass, statement); + return prepareQuery(nodeClass, statement, Collections.emptyMap()); } - private PreparedQuery prepareQuery(Class resultType, Statement statement) { + private PreparedQuery prepareQuery(Class resultType, Statement statement, + Map parameters) { BiFunction mappingFunctionToUse = this.mappingFunction; if (!this.nodeClass.equals(resultType)) { @@ -367,6 +446,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { return PreparedQuery.queryFor(resultType) .withCypherQuery(renderer.render(statement)) + .withParameters(parameters) .usingMappingFunction(mappingFunctionToUse) .build(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 790f59c44..43a533679 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -37,6 +37,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.ExampleMatcher.StringMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Range; @@ -207,6 +208,51 @@ class RepositoryIT { assertThat(persons).containsExactly(person1); } + @Test + void findAllByExampleWithDifferentMatchers() { + PersonWithAllConstructor person; + Example example; + Iterable persons; + + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null); + example = Example.of(person, ExampleMatcher.matchingAny()); + + persons = repository.findAll(example); + assertThat(persons).containsExactlyInAnyOrder(person1, person2); + + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, + null, null); + example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name")); + + persons = repository.findAll(example); + assertThat(persons).containsExactlyInAnyOrder(person1, person2); + + person = new PersonWithAllConstructor(null, + TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(), + TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null); + example = Example.of(person, ExampleMatcher + .matchingAll() + .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.ENDING, true)) + .withMatcher("firstName", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.STARTING)) + .withMatcher("sameValue", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.CONTAINING)) + ); + + persons = repository.findAll(example); + assertThat(persons).containsExactlyInAnyOrder(person2); + + person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null); + example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(StringMatcher.REGEX)); + + persons = repository.findAll(example); + assertThat(persons).containsExactlyInAnyOrder(person1); + + example = Example + .of(person, ExampleMatcher.matchingAll().withStringMatcher(StringMatcher.REGEX).withIncludeNullValues()); + + persons = repository.findAll(example); + assertThat(persons).isEmpty(); + } + @Test void findAllByExampleWithSort() { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); From 2fa95924f4ff9f61098068720bb2ccfd0926b198 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 15 May 2019 22:10:43 +0200 Subject: [PATCH 082/342] Add support for after and before for derived queries. --- .../repository/query/CypherQueryCreator.java | 10 ++-- .../repository/query/PartTreeNeo4jQuery.java | 42 ++++++++++++++++ .../neo4j/integration/PersonRepository.java | 5 ++ .../integration/PersonWithAllConstructor.java | 4 ++ .../data/neo4j/integration/RepositoryIT.java | 50 +++++++++++++------ 5 files changed, 93 insertions(+), 18 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index d6d5d0ab8..3b3aaf5cc 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -113,6 +113,12 @@ final class CypherQueryCreator extends AbstractQueryCreator { // TODO case insensitive (like, notlike, simpleProperty, negatedSimpleProperty) switch (part.getType()) { + case AFTER: + case GREATER_THAN: + return property(persistentProperty).gt(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + case BEFORE: + case LESS_THAN: + return property(persistentProperty).lt(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case BETWEEN: return betweenCondition(persistentProperty, actualParameters); case CONTAINING: @@ -123,12 +129,8 @@ final class CypherQueryCreator extends AbstractQueryCreator { .endsWith(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case FALSE: return property(persistentProperty).isFalse(); - case GREATER_THAN: - return property(persistentProperty).gt(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case GREATER_THAN_EQUAL: return property(persistentProperty).gte(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); - case LESS_THAN: - return property(persistentProperty).lt(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case LESS_THAN_EQUAL: return property(persistentProperty).lte(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case LIKE: diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index afc6ff3a1..48ff39393 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -20,8 +20,18 @@ package org.springframework.data.neo4j.repository.query; import static java.util.stream.Collectors.*; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.springframework.data.domain.Range; import org.springframework.data.neo4j.core.NodeManager; @@ -32,7 +42,9 @@ import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.util.Assert; /** * Implementation of {@link RepositoryQuery} for derived finder methods. @@ -41,8 +53,19 @@ import org.springframework.data.repository.query.parser.PartTree; * @author Michael J. Simons * @since 1.0 */ +@Slf4j final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { + // + /** + * A set of the temporal types that are directly passable to the driver and support a meaningful comparision in a + * temporal sense (after, before). + * See > COMPARABLE_TEMPORAL_TYPES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(LocalDate.class, OffsetTime.class, ZonedDateTime.class, + LocalDateTime.class))); + private final ResultProcessor processor; private final PartTree tree; @@ -55,6 +78,8 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { this.processor = queryMethod.getResultProcessor(); this.tree = new PartTree(queryMethod.getName(), domainType); + + this.tree.flatMap(op -> op.stream()).forEach(this::validatePart); } @Override @@ -78,6 +103,23 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { .build(); } + void validatePart(Part part) { + + switch (part.getType()) { + case AFTER: + case BEFORE: + validateTemporal(part); + break; + } + } + + void validateTemporal(Part part) { + + Assert.state(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String + .format("%s works only with properties with one of the following types: %s", part.getType(), + COMPARABLE_TEMPORAL_TYPES)); + } + // TODO Have fun with a bunch of conversion services static Object convertParameter(Object parameter) { if (parameter instanceof Range) { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 0c5b47577..f2c7c480a 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.integration; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -101,4 +102,8 @@ public interface PersonRepository extends Neo4jRepository findAllByPersonNumberIsBetween(Range range); List findAllByPersonNumberIsBetween(Long low, Long high); + + List findAllByBornOnAfter(LocalDate date); + + List findAllByBornOnBefore(LocalDate date); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java index 16871f968..54af03175 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java @@ -24,6 +24,8 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import java.time.LocalDate; + import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Property; @@ -53,4 +55,6 @@ public class PersonWithAllConstructor { private final Boolean cool; private final Long personNumber; + + private final LocalDate bornOn; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 43a533679..c09ac9257 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.integration; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Range.Bound.*; +import java.time.LocalDate; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -65,10 +66,12 @@ class RepositoryIT { private static final String TEST_PERSON2_NAME = "Test2"; private static final String TEST_PERSON1_FIRST_NAME = "Ernie"; private static final String TEST_PERSON2_FIRST_NAME = "Bert"; + private static final LocalDate TEST_PERSON1_BORN_ON = LocalDate.of(2019, 1, 1); + private static final LocalDate TEST_PERSON2_BORN_ON = LocalDate.of(2019, 2, 1); private static final String TEST_PERSON_SAMEVALUE = "SameValue"; static PersonWithAllConstructor personExample(String sameValue) { - return new PersonWithAllConstructor(null, null, null, sameValue, null, null); + return new PersonWithAllConstructor(null, null, null, sameValue, null, null, null); } private static Neo4jConnectionSupport neo4jConnectionSupport; @@ -95,14 +98,14 @@ class RepositoryIT { transaction.run("MATCH (n) detach delete n"); id1 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber return id(n)", + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn return id(n)", Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1) + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON) ).next().get(0).asLong(); id2 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber return id(n)", + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2) + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON) ).next().get(0).asLong(); transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = $name, n.first_name = $firstName", Values.parameters("name", TEST_PERSON1_NAME, "firstName", TEST_PERSON1_FIRST_NAME)); @@ -112,9 +115,9 @@ class RepositoryIT { transaction.close(); person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L); + true, 1L, TEST_PERSON1_BORN_ON); person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L); + false, 2L, TEST_PERSON2_BORN_ON); } @Test @@ -193,7 +196,8 @@ class RepositoryIT { @Test void findOneByExample() { - Example example = Example.of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); + Example example = Example + .of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); Optional person = repository.findOne(example); assertThat(person).isPresent(); @@ -202,7 +206,8 @@ class RepositoryIT { @Test void findAllByExample() { - Example example = Example.of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); + Example example = Example + .of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); Iterable persons = repository.findAll(example); assertThat(persons).containsExactly(person1); @@ -214,14 +219,14 @@ class RepositoryIT { Example example; Iterable persons; - person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null); + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny()); persons = repository.findAll(example); assertThat(persons).containsExactlyInAnyOrder(person1, person2); person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, - null, null); + null, null, null); example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name")); persons = repository.findAll(example); @@ -229,7 +234,7 @@ class RepositoryIT { person = new PersonWithAllConstructor(null, TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(), - TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null); + TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null); example = Example.of(person, ExampleMatcher .matchingAll() .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.ENDING, true)) @@ -240,7 +245,7 @@ class RepositoryIT { persons = repository.findAll(example); assertThat(persons).containsExactlyInAnyOrder(person2); - person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null); + person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(StringMatcher.REGEX)); persons = repository.findAll(example); @@ -367,7 +372,8 @@ class RepositoryIT { @Test void findBySimplePropertiesAnded() { - Optional optionalPerson = repository.findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME); + Optional optionalPerson = repository + .findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME); assertThat(optionalPerson).isPresent().contains(person1); } @@ -542,6 +548,22 @@ class RepositoryIT { .contains(person2); } + @Test + void findByAfter() { + List persons = repository.findAllByBornOnAfter(TEST_PERSON1_BORN_ON); + assertThat(persons) + .hasSize(1) + .contains(person2); + } + + @Test + void findByBefore() { + List persons = repository.findAllByBornOnBefore(TEST_PERSON2_BORN_ON); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + @Configuration @EnableNeo4jRepositories @EnableTransactionManagement From 790f3f342c948a2f0af5b00bd7e3d7c204e96837 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 15 May 2019 22:18:43 +0200 Subject: [PATCH 083/342] Remove the object literal. This is error prone (as with the date literals discovered in the two previous commits) and may also lead to security issues due to Cypher injections. --- .../data/neo4j/core/cypher/Cypher.java | 31 +++++-------------- .../{ObjectLiteral.java => NullLiteral.java} | 13 ++++---- .../data/neo4j/core/cypher/CypherTest.java | 31 +++++++------------ 3 files changed, 26 insertions(+), 49 deletions(-) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/{ObjectLiteral.java => NullLiteral.java} (75%) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index d8279eea4..57493a922 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -126,39 +126,24 @@ public final class Cypher { } /** - * Creates a new {@link StringLiteral} from a {@code content}. - * - * @param content the literal content. - * @return a new {@link StringLiteral}. - */ - public static StringLiteral literalOf(@Nullable CharSequence content) { - return new StringLiteral(content); - } - - /** - * Creates a new {@link NumberLiteral} from the given {@code number}. - * - * @param number the number. - * @return a new {@link NumberLiteral}. - */ - public static NumberLiteral literalOf(@Nullable Number number) { - return new NumberLiteral(number); - } - - /** - * Creates a new {@link ObjectLiteral} from the given {@code object}. + * Creates a new {@link NullLiteral} from the given {@code object}. * * @param object the object to represent. - * @return a new {@link ObjectLiteral}. + * @return a new {@link NullLiteral}. + * @throws IllegalArgumentException when the object cannot be represented as a literal */ public static Literal literalOf(@Nullable Object object) { + + if (object == null) { + return NullLiteral.INSTANCE; + } if (object instanceof CharSequence) { return new StringLiteral((CharSequence) object); } if (object instanceof Number) { return new NumberLiteral((Number) object); } - return new ObjectLiteral(object); + throw new IllegalArgumentException("Unsupported literal type: " + object.getClass()); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NullLiteral.java similarity index 75% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NullLiteral.java index 7610687cf..49d91ad2a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ObjectLiteral.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NullLiteral.java @@ -21,23 +21,24 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; /** - * The most generic implementation of {@link Literal}. Takes an arbitrary object and calls {@code toString} on it to get - * its value representation. + * Represents the literal value {@literal null}. * * @author Gerrit Meier * @author Michael J. Simons * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class ObjectLiteral extends Literal { +public final class NullLiteral extends Literal { - ObjectLiteral(Object content) { + static final NullLiteral INSTANCE = new NullLiteral(); - super(content); + private NullLiteral() { + + super(null); } @Override public String asString() { - return String.valueOf(getContent()); + return "NULL"; } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index d3535c737..113b22412 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -472,6 +472,17 @@ public class CypherTest { .isEqualTo( "MATCH (u:`User`) RETURN coalesce(u.a, u.b, '¯\\\\_(ツ)_/¯')"); } + + @Test + void literalsShouldDealWithNull() { + Statement statement = Cypher.match(userNode) + .returning(Functions.coalesce(Cypher.literalOf(null), userNode.property("field")).as("p")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN coalesce(NULL, u.field) AS p"); + } } @Nested @@ -500,26 +511,6 @@ public class CypherTest { .isEqualTo( "MATCH (u:`User`) WHERE u.age = 21 RETURN u"); } - - @Test - void equalsWithObjectLiteral() { - Statement statement = Cypher.match(userNode) - .where(userNode.property("field").isEqualTo(Cypher.literalOf(new CustomType()))) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE u.field = Value RETURN u"); - } - - - private class CustomType { - @Override - public String toString() { - return "Value"; - } - } } @Nested From a530c403949d80f80270fa0c54c4cbf6e5159e97 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 16 May 2019 14:45:54 +0200 Subject: [PATCH 084/342] Add more derived finder methods. Including * EXISTS * IN / NOT_IN * IS_EMPTY / IS_NOT_EMPTY * IS_NULL / IS_NOT_NULL * NEAR --- .../core/cypher/BooleanFunctionCondition.java | 44 ++++++ .../data/neo4j/core/cypher/Conditions.java | 40 +++++- .../data/neo4j/core/cypher/Expression.java | 45 +++++-- .../data/neo4j/core/cypher/Functions.java | 51 ++++++- .../data/neo4j/core/cypher/Property.java | 20 --- .../repository/query/CypherQueryCreator.java | 124 ++++++++++++----- .../repository/query/PartTreeNeo4jQuery.java | 71 +++++++++- .../support/SimpleNeo4jRepository.java | 7 +- .../neo4j/integration/PersonRepository.java | 30 +++++ .../integration/PersonWithAllConstructor.java | 8 ++ .../data/neo4j/integration/RepositoryIT.java | 125 ++++++++++++++++-- 11 files changed, 483 insertions(+), 82 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanFunctionCondition.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanFunctionCondition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanFunctionCondition.java new file mode 100644 index 000000000..2001f3804 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanFunctionCondition.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * This wraps a function into a condition so that it can be used in a where clause. The function is supposed to return a + * boolean value. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class BooleanFunctionCondition implements Condition { + + private final FunctionInvocation delegate; + + BooleanFunctionCondition(FunctionInvocation delegate) { + this.delegate = delegate; + } + + @Override + public void accept(Visitor visitor) { + delegate.accept(visitor); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java index c66684d2f..02bc45aba 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java @@ -156,8 +156,8 @@ public final class Conditions { } /** - * Creates a placeholder condition which is not rendered in the final statement but is usefull - * while chaining conditions together. + * Creates a placeholder condition which is not rendered in the final statement but is useful while chaining + * conditions together. * * @return A placeholder condition. */ @@ -188,6 +188,42 @@ public final class Conditions { return Comparison.create(Operator.IS_NOT_NULL, expression); } + /** + * Creates a condition that checkts whether the expression {@code searchFor} is in the expression {@code in}. + * {@code in} is supposed to represent a list object. + * + * @param searchFor The expression to search for. + * @param in The expression to search in. + * @return A new condition. + */ + static Condition in(Expression searchFor, Expression in) { + + return Comparison.create(searchFor, Operator.IN, in); + } + + /** + * A condition that evaluates to true if a list or a string represented by {@code expression} is empty or has the length of 0. + * + * @param expression The expression to test for emptiness. + * @return A new condition. + */ + static Condition isEmpty(Expression expression) { + + return Functions.size(expression).isEqualTo(Cypher.literalOf(0L)); + } + + /** + * A condition that evaluates to true if the expression {@code expression} exists. + * + * @param expression The expression to check whether it exists or not + * @return A new condition. + */ + public static Condition exists(Expression expression) { + + return new BooleanFunctionCondition(Functions.exists(expression)); + } + + /** * Not to be instantiated. */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java index 39a25a1c2..1586bf828 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java @@ -147,14 +147,43 @@ public interface Expression extends Visitable { return Operations.plus(this, expression); } - // WIP - // TODO Make longs a list of expressions or something - // WIP + /** + * Creates a {@code IS NULL} operation for this {@code expression}. + * The expression does not track the condition created here. + * + * @return A condition based on this expression that evaluates to true when this expression is null. + */ + default Condition isNull() { + return Conditions.isNull(this); + } - default Condition isIn(Iterable ids) { - return Comparison.create( - this, - Operator.IN, - new ListLiteral(ids)); + /** + * Creates a {@code IS NOT NULL} operation for this {@code expression}. + * The expression does not track the condition created here. + * + * @return A condition based on this expression that evaluates to true when this expression is not null. + */ + default Condition isNotNull() { + return Conditions.isNotNull(this); + } + + /** + * Creates a {@code IN} operation for this expression and that {@code expression}. + * The expression does not track the condition created here. + * + * @param expression The expression to search for this expression + * @return A new condition. + */ + default Condition in(Expression expression) { + return Conditions.in(this, expression); + } + + /** + * Creates a condition that evaluates to true if this expression is empty. + * + * @return A new condition. + */ + default Condition isEmpty() { + return Conditions.isEmpty(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java index f1c94b7d1..7a530c440 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java @@ -102,7 +102,7 @@ public final class Functions { * See toLower. * * @param expression An expression resolving to a string - * @return A function call for {@code toLower()} for one expression + * @return A function call for {@code toLower()} for one expression */ public static FunctionInvocation toLower(Expression expression) { @@ -111,6 +111,55 @@ public final class Functions { return new FunctionInvocation("toLower", expression); } + /** + * Creates a function invocation for the {@code size()} function. {@code size} can be applied to + * + * + * @param expression The expression whos size is to be returned + * @return A function call for {@code size()} for one expression + */ + public static FunctionInvocation size(Expression expression) { + + Assert.notNull(expression, "The expression for size() is required."); + + return new FunctionInvocation("size", expression); + } + + /** + * Creates a function invocation for the {@code exists()} function. + * See exists + * + * @param expression The expression whos existence is to be evaluated + * @return A function call for {@code exists()} for one expression + */ + public static FunctionInvocation exists(Expression expression) { + + Assert.notNull(expression, "The expression for exists() is required."); + + return new FunctionInvocation("exists", expression); + } + + /** + * Creates a function invocation for the {@code distance()} function. + * See exists + * Both points need to be in the same coordinate system. + * + * @param point1 Point 1 + * @param point2 Point 2 + * @return A function call for {@code distance()} + */ + public static FunctionInvocation distance(Expression point1, Expression point2) { + + Assert.notNull(point1, "The distance function requires two points."); + Assert.notNull(point2, "The distance function requires two points."); + + return new FunctionInvocation("distance", point1, point2); + } + private Functions() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java index 0383c916b..e37425cef 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java @@ -76,26 +76,6 @@ public final class Property implements Expression { visitor.leave(this); } - /** - * The property does not track the condition created here. - * - * @return A condition based on this property that evaluates to true when this property is null - */ - public Condition isNull() { - - return Conditions.isNull(this); - } - - /** - * The property does not track the condition created here. - * - * @return A condition based on this property that evaluates to true when this property is not null - */ - public Condition isNotNull() { - - return Conditions.isNotNull(this); - } - /** * The property does not track the sort items created here. * diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 3b3aaf5cc..78daaf9cd 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -22,17 +22,21 @@ import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; import lombok.RequiredArgsConstructor; +import lombok.ToString; import java.util.Iterator; import java.util.Optional; +import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Distance; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; import org.springframework.data.neo4j.core.cypher.Expression; +import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Property; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; @@ -111,7 +115,6 @@ final class CypherQueryCreator extends AbstractQueryCreator { Neo4jPersistentProperty persistentProperty = path.getLeafProperty(); // TODO case insensitive (like, notlike, simpleProperty, negatedSimpleProperty) - switch (part.getType()) { case AFTER: case GREATER_THAN: @@ -127,14 +130,39 @@ final class CypherQueryCreator extends AbstractQueryCreator { case ENDING_WITH: return property(persistentProperty) .endsWith(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + case EXISTS: + return Conditions.exists(property(persistentProperty)); case FALSE: return property(persistentProperty).isFalse(); case GREATER_THAN_EQUAL: return property(persistentProperty).gte(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + case IN: + return property(persistentProperty).in(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + case IS_EMPTY: + return property(persistentProperty).isEmpty(); + case IS_NOT_EMPTY: + return property(persistentProperty).isEmpty().not(); + case IS_NOT_NULL: + return property(persistentProperty).isNotNull(); + case IS_NULL: + return property(persistentProperty).isNull(); case LESS_THAN_EQUAL: return property(persistentProperty).lte(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case LIKE: return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex); + case NEAR: + return createNearCondition(persistentProperty, actualParameters); + case NEGATING_SIMPLE_PROPERTY: + return property(persistentProperty) + .isNotEqualTo(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + case NOT_CONTAINING: + return property(persistentProperty) + .contains(parameter(nextRequiredParameter(actualParameters).nameOrIndex)).not(); + case NOT_IN: + return property(persistentProperty).in(parameter(nextRequiredParameter(actualParameters).nameOrIndex)) + .not(); + case NOT_LIKE: + return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex).not(); case SIMPLE_PROPERTY: return property(persistentProperty) .isEqualTo(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); @@ -144,14 +172,6 @@ final class CypherQueryCreator extends AbstractQueryCreator { case REGEX: return property(persistentProperty) .matches(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); - case NEGATING_SIMPLE_PROPERTY: - return property(persistentProperty) - .isNotEqualTo(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); - case NOT_CONTAINING: - return property(persistentProperty) - .contains(parameter(nextRequiredParameter(actualParameters).nameOrIndex)).not(); - case NOT_LIKE: - return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex).not(); case TRUE: return property(persistentProperty).isTrue(); default: @@ -169,46 +189,88 @@ final class CypherQueryCreator extends AbstractQueryCreator { Parameter lowerBoundOrRange = nextRequiredParameter(actualParameters); Property property = property(persistentProperty); - Condition betweenCondition = Conditions.noCondition(); if (lowerBoundOrRange.value instanceof Range) { - Range range = (Range) lowerBoundOrRange.value; - - if (range.getLowerBound().isBounded()) { - Expression parameterPlaceholder = parameter(lowerBoundOrRange.nameOrIndex + ".lb"); - betweenCondition = betweenCondition.and(range.getLowerBound().isInclusive() ? - property.gte(parameterPlaceholder) : - property.gt(parameterPlaceholder)); - } - - if (range.getUpperBound().isBounded()) { - Expression parameterPlaceholder = parameter(lowerBoundOrRange.nameOrIndex + ".ub"); - betweenCondition = betweenCondition.and(range.getUpperBound().isInclusive() ? - property.lte(parameterPlaceholder) : - property.lt(parameterPlaceholder)); - } + return createRangeConditionForProperty(property, lowerBoundOrRange); } else { Parameter upperBound = nextRequiredParameter(actualParameters); - betweenCondition = property.gte(parameter(lowerBoundOrRange.nameOrIndex)) + return property.gte(parameter(lowerBoundOrRange.nameOrIndex)) .and(property.lte(parameter(upperBound.nameOrIndex))); } + } + + private Condition createNearCondition(Neo4jPersistentProperty persistentProperty, + Iterator actualParameters) { + + Parameter p1 = nextRequiredParameter(actualParameters); + Parameter p2 = nextRequiredParameter(actualParameters); + + Expression referencePoint; + + Parameter other; + if (p1.value instanceof Point) { + referencePoint = parameter(p1.nameOrIndex); + other = p2; + } else if (p2.value instanceof Point) { + referencePoint = parameter(p2.nameOrIndex); + other = p1; + } else { + throw new IllegalArgumentException( + String.format("The NEAR operation requires a reference point of type %s", Point.class)); + } + + Expression distanceFunction = Functions.distance(property(persistentProperty), referencePoint); + + if (other.value instanceof Distance) { + return distanceFunction.lte(parameter(other.nameOrIndex)); + } else if (other.value instanceof Range) { + return createRangeConditionForProperty(distanceFunction, other); + } else { + throw new IllegalArgumentException( + String.format("The NEAR operation requires a distance or a range of distances.")); + } + } + + /** + * @param property + * @param rangeParameter + * @return The equivalent of a A BETWEEN B AND C expression for a given range. + */ + private static Condition createRangeConditionForProperty(Expression property, Parameter rangeParameter) { + + Range range = (Range) rangeParameter.value; + Condition betweenCondition = Conditions.noCondition(); + if (range.getLowerBound().isBounded()) { + Expression parameterPlaceholder = parameter(rangeParameter.nameOrIndex + ".lb"); + betweenCondition = betweenCondition.and(range.getLowerBound().isInclusive() ? + property.gte(parameterPlaceholder) : + property.gt(parameterPlaceholder)); + } + + if (range.getUpperBound().isBounded()) { + Expression parameterPlaceholder = parameter(rangeParameter.nameOrIndex + ".ub"); + betweenCondition = betweenCondition.and(range.getUpperBound().isInclusive() ? + property.lte(parameterPlaceholder) : + property.lt(parameterPlaceholder)); + } return betweenCondition; } - private Property property(Neo4jPersistentProperty persistentProperty) { - return Cypher.property("n", persistentProperty.getPropertyName()); + private static Property property(Neo4jPersistentProperty persistentProperty) { + return Cypher.property(NodeDescription.NAME_OF_ROOT_NODE, persistentProperty.getPropertyName()); } private Parameter nextRequiredParameter(Iterator actualParameters) { if (!formalParameters.hasNext()) { throw new IllegalStateException("Not enough formal, bindable parameters for parts"); } - - //System.out.println(actualParameters()); - return new Parameter(formalParameters.next().getNameOrIndex(), actualParameters.next()); + final Neo4jParameter parameter = formalParameters.next(); + return new Parameter(parameter.getNameOrIndex(), actualParameters.next()); } @RequiredArgsConstructor(access = PACKAGE) + @ToString static class Parameter { + final String nameOrIndex; final Object value; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 48ff39393..c78c665d9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -32,18 +32,26 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.repository.query.parser.PartTree.OrPart; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** @@ -79,7 +87,9 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { this.processor = queryMethod.getResultProcessor(); this.tree = new PartTree(queryMethod.getName(), domainType); - this.tree.flatMap(op -> op.stream()).forEach(this::validatePart); + AtomicInteger parameterCounter = new AtomicInteger(); + this.tree.flatMap(OrPart::stream) + .forEach(part -> validatePart(parameterCounter, part)); } @Override @@ -103,35 +113,84 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { .build(); } - void validatePart(Part part) { + void validatePart(AtomicInteger index, Part part) { + int currentParameterIndex = index.getAndAdd(part.getNumberOfArguments()); switch (part.getType()) { case AFTER: case BEFORE: - validateTemporal(part); + validateTemporalProperty(part); + break; + case IN: + case NOT_IN: + Parameter parameter = this.queryMethod.getParameters() + .getBindableParameter(currentParameterIndex); + validateCollectionParameter(part, parameter); + break; + case IS_EMPTY: + case IS_NOT_EMPTY: + validateCollectionProperty(part); + break; + case WITHIN: + validatePointPropertyAndParameters(part); break; } } - void validateTemporal(Part part) { + void validateTemporalProperty(Part part) { Assert.state(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String .format("%s works only with properties with one of the following types: %s", part.getType(), COMPARABLE_TEMPORAL_TYPES)); } + void validateCollectionParameter(Part part, Parameter parameter) { + + TypeInformation information = ClassTypeInformation.from(parameter.getType()); + Assert.state(information.isCollectionLike(), + () -> String.format("%s works only with collection parameters", part.getType())); + } + + void validateCollectionProperty(Part part) { + Assert.state(part.getProperty().getLeafProperty().isCollection(), () -> String + .format("%s works only with collection properties", part.getType())); + } + + void validatePointPropertyAndParameters(Part part) { + + Assert.state(ClassTypeInformation.from(Point.class) + .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), () -> String + .format("%s works only with spatial properties", part.getType())); + // TODO Check if 2 or 3 parameters are always bound correctly. + // Assert.state(part.getNumberOfArguments() == 2,"The distance operation needs two arguments: The reference point and the distance!"); + } + // TODO Have fun with a bunch of conversion services static Object convertParameter(Object parameter) { if (parameter instanceof Range) { Range range = (Range) parameter; Map map = new HashMap<>(); - range.getLowerBound().getValue().ifPresent(v -> map.put("lb", v)); - range.getUpperBound().getValue().ifPresent(v -> map.put("ub", v)); + range.getLowerBound().getValue().map(PartTreeNeo4jQuery::convertParameter).ifPresent(v -> map.put("lb", v)); + range.getUpperBound().getValue().map(PartTreeNeo4jQuery::convertParameter).ifPresent(v -> map.put("ub", v)); return map; + } else if (parameter instanceof Distance) { + return calculateDistanceInMeter((Distance) parameter); } return parameter; } + private static double calculateDistanceInMeter(Distance distance) { + + if (distance.getMetric() == Metrics.KILOMETERS) { + return distance.getValue() / 0.001d; + } else if (distance.getMetric() == Metrics.MILES) { + return distance.getValue() / 0.00062137d; + } else { + return distance.getValue(); + } + } + + @Override protected boolean isCountQuery() { return tree.isCountProjection(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 0bb903f5a..7942cc8b1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.repository.support; +import static java.util.Collections.*; import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; import static org.springframework.data.neo4j.core.schema.NodeDescription.*; @@ -208,9 +209,11 @@ class SimpleNeo4jRepository implements Neo4jRepository { public Iterable findAllById(Iterable ids) { Statement statement = mappingContext - .prepareMatchOf(nodeDescription, Optional.of(idExpression.isIn((Iterable) ids))).returning(asterisk()) + .prepareMatchOf(nodeDescription, Optional.of(idExpression.in((parameter("ids"))))) + .returning(asterisk()) .build(); - return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); + + return nodeManager.toExecutableQuery(prepareQuery(nodeClass, statement, singletonMap("ids", ids))).getResults(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index f2c7c480a..28dbafdf3 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -22,7 +22,9 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; +import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; +import org.springframework.data.geo.Distance; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.transaction.annotation.Transactional; @@ -106,4 +108,32 @@ public interface PersonRepository extends Neo4jRepository findAllByBornOnAfter(LocalDate date); List findAllByBornOnBefore(LocalDate date); + + List findAllByNullableIsNotNull(); + + List findAllByNullableIsNull(); + + List findAllByFirstNameIn(List haystack); + + List findAllByFirstNameNotIn(List haystack); + + List findAllByThingsIsEmpty(); + + List findAllByThingsIsNotEmpty(); + + List findAllByNullableExists(); + + List findAllByPlaceNear(Point p, Distance max); + + List findAllByPlaceNear(Point p, Distance min, Distance max); + + List findAllByPlaceNear(Point p, Range between); + + // TODO Integration tests for failed validations + // List findAllByBornOnAfter(String date); + // List findAllByNameOrPersonNumberIsBetweenAndFirstNameNotInAndFirstNameEquals(String name, Long low, Long high, String wrong, List haystack); + // List findAllByNameOrPersonNumberIsBetweenAndCoolIsTrueAndFirstNameNotInAndFirstNameEquals(String name, Long low, Long high, String wrong, List haystack); + // List findAllByNameNotEmpty(); + // List findAllByPlaceNear(Point p); + // List findAllByPlaceNear(Point p, String); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java index 54af03175..dcd2f97af 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java @@ -25,7 +25,9 @@ import lombok.Setter; import lombok.ToString; import java.time.LocalDate; +import java.util.List; +import org.neo4j.driver.types.Point; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Property; @@ -57,4 +59,10 @@ public class PersonWithAllConstructor { private final Long personNumber; private final LocalDate bornOn; + + private final String nullable; + + private final List things; + + private final Point place; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index c09ac9257..294e8bc84 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -23,6 +23,7 @@ import static org.springframework.data.domain.Range.Bound.*; import java.time.LocalDate; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -33,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; +import org.neo4j.driver.types.Point; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,6 +46,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; @@ -69,9 +73,13 @@ class RepositoryIT { private static final LocalDate TEST_PERSON1_BORN_ON = LocalDate.of(2019, 1, 1); private static final LocalDate TEST_PERSON2_BORN_ON = LocalDate.of(2019, 2, 1); private static final String TEST_PERSON_SAMEVALUE = "SameValue"; + private static final Point NEO4J_HQ = Values.point(4326, 12.994823, 55.612191).asPoint(); + private static final Point SFO = Values.point(4326, -122.38681, 37.61649).asPoint(); + private static final Point CLARION = Values.point(4326, 12.994243, 55.607726).asPoint(); + private static final Point MINC = Values.point(4326, 12.994039, 55.611496).asPoint(); static PersonWithAllConstructor personExample(String sameValue) { - return new PersonWithAllConstructor(null, null, null, sameValue, null, null, null); + return new PersonWithAllConstructor(null, null, null, sameValue, null, null, null, null, null, null); } private static Neo4jConnectionSupport neo4jConnectionSupport; @@ -97,15 +105,19 @@ class RepositoryIT { Transaction transaction = driver.session().beginTransaction(); transaction.run("MATCH (n) detach delete n"); - id1 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn return id(n)", + id1 = transaction.run("" + + "CREATE (n:PersonWithAllConstructor) " + + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + + + "RETURN id(n)", Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON) + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", + NEO4J_HQ) ).next().get(0).asLong(); id2 = transaction.run( - "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn return id(n)", + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", - TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON) + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO) ).next().get(0).asLong(); transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = $name, n.first_name = $firstName", Values.parameters("name", TEST_PERSON1_NAME, "firstName", TEST_PERSON1_FIRST_NAME)); @@ -115,9 +127,9 @@ class RepositoryIT { transaction.close(); person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, - true, 1L, TEST_PERSON1_BORN_ON); + true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ); person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, - false, 2L, TEST_PERSON2_BORN_ON); + false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO); } @Test @@ -219,14 +231,15 @@ class RepositoryIT { Example example; Iterable persons; - person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, null); + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, null, + null, null, null); example = Example.of(person, ExampleMatcher.matchingAny()); persons = repository.findAll(example); assertThat(persons).containsExactlyInAnyOrder(person1, person2); person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, - null, null, null); + null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name")); persons = repository.findAll(example); @@ -234,7 +247,8 @@ class RepositoryIT { person = new PersonWithAllConstructor(null, TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(), - TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null); + TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null, null, + null, null); example = Example.of(person, ExampleMatcher .matchingAll() .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.ENDING, true)) @@ -245,7 +259,7 @@ class RepositoryIT { persons = repository.findAll(example); assertThat(persons).containsExactlyInAnyOrder(person2); - person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null); + person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(StringMatcher.REGEX)); persons = repository.findAll(example); @@ -564,6 +578,93 @@ class RepositoryIT { .contains(person1); } + @Test + void findByIsNotNull() { + List persons = repository.findAllByNullableIsNotNull(); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByIsNull() { + List persons = repository.findAllByNullableIsNull(); + assertThat(persons) + .hasSize(1) + .contains(person2); + } + + @Test + void findByIn() { + List persons = repository + .findAllByFirstNameIn(Arrays.asList("a", "b", TEST_PERSON2_FIRST_NAME, "c")); + assertThat(persons) + .hasSize(1) + .contains(person2); + } + + @Test + void findByNotIn() { + List persons = repository + .findAllByFirstNameNotIn(Arrays.asList("a", "b", TEST_PERSON2_FIRST_NAME, "c")); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByEmpty() { + List persons = repository.findAllByThingsIsEmpty(); + assertThat(persons) + .hasSize(1) + .contains(person2); + } + + @Test + void findByNotEmpty() { + List persons = repository.findAllByThingsIsNotEmpty(); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByExists() { + List persons = repository.findAllByNullableExists(); + assertThat(persons) + .hasSize(1) + .contains(person1); + } + + @Test + void findByNear() { + List persons; + + persons = repository.findAllByPlaceNear(MINC, new Distance(200.0 / 1000.0, Metrics.KILOMETERS)); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByPlaceNear(CLARION, new Distance(200.0 / 1000.0, Metrics.KILOMETERS)); + assertThat(persons).isEmpty(); + + persons = repository.findAllByPlaceNear(MINC, + Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByPlaceNear(MINC, + Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); + assertThat(persons).isEmpty(); + + persons = repository.findAllByPlaceNear(MINC, + Range.of(Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), Bound.unbounded())); + assertThat(persons) + .hasSize(1) + .contains(person2); + } + @Configuration @EnableNeo4jRepositories @EnableTransactionManagement From cff28ade1c06999b6669f6e8cf423c5557acb71c Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Fri, 17 May 2019 10:38:20 +0200 Subject: [PATCH 085/342] Introduce Kotlin compatibility tests for immutable mapping. --- pom.xml | 16 ++++++++++ spring-data-neo4j/pom.xml | 8 +++++ .../neo4j/integration/PersonRepository.java | 15 ++++++++-- .../data/neo4j/integration/RepositoryIT.java | 28 +++++++++++++++--- .../data/neo4j/integration/KotlinPerson.kt | 29 +++++++++++++++++++ 5 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/KotlinPerson.kt diff --git a/pom.xml b/pom.xml index c5a97a293..aa91ab839 100644 --- a/pom.xml +++ b/pom.xml @@ -198,11 +198,27 @@ spring-data-commons ${springdata-commons.version} + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin} + org.testcontainers junit-jupiter ${testcontainers.version} test + + + org.jetbrains + annotations + + org.testcontainers diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 3d590338e..bfab0f00b 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -116,6 +116,14 @@ org.springframework.data spring-data-commons + + org.jetbrains.kotlin + kotlin-stdlib + + + org.jetbrains.kotlin + kotlin-reflect + org.testcontainers junit-jupiter diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 28dbafdf3..6e9d6f7cb 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -46,7 +46,7 @@ public interface PersonRepository extends Neo4jRepository getOptionalPersonsViaQuery(); + Optional getOptionalPersonViaQuery(); @Query("MATCH (n:PersonWithNoConstructor) return n") List getAllPersonsWithNoConstructorViaQuery(); @@ -55,7 +55,7 @@ public interface PersonRepository extends Neo4jRepository getOptionalPersonsWithNoConstructorViaQuery(); + Optional getOptionalPersonWithNoConstructorViaQuery(); @Query("MATCH (n:PersonWithWither) return n") List getAllPersonsWithWitherViaQuery(); @@ -64,7 +64,16 @@ public interface PersonRepository extends Neo4jRepository getOptionalPersonsWithWitherViaQuery(); + Optional getOptionalPersonWithWitherViaQuery(); + + @Query("MATCH (n:KotlinPerson) return n") + List getAllKotlinPersonsViaQuery(); + + @Query("MATCH (n:KotlinPerson{name:'Test'}) return n") + KotlinPerson getOneKotlinPersonViaQuery(); + + @Query("MATCH (n:KotlinPerson{name:'Test'}) return n") + Optional getOptionalKotlinPersonViaQuery(); // Derived finders, should be extracted into another repo. Optional findOneByNameAndFirstName(String name, String firstName); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 294e8bc84..98c331df6 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -122,6 +122,7 @@ class RepositoryIT { transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = $name, n.first_name = $firstName", Values.parameters("name", TEST_PERSON1_NAME, "firstName", TEST_PERSON1_FIRST_NAME)); transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'"); + transaction.run("CREATE (n:KotlinPerson) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})"); transaction.success(); transaction.close(); @@ -319,7 +320,7 @@ class RepositoryIT { @Test void loadOptionalPersonWithAllConstructor() { - Optional person = repository.getOptionalPersonsViaQuery(); + Optional person = repository.getOptionalPersonViaQuery(); assertThat(person).isPresent(); assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @@ -344,7 +345,7 @@ class RepositoryIT { @Test void loadOptionalPersonWithNoConstructor() { - Optional person = repository.getOptionalPersonsWithNoConstructorViaQuery(); + Optional person = repository.getOptionalPersonWithNoConstructorViaQuery(); assertThat(person).isPresent(); assertThat(person).map(PersonWithNoConstructor::getName).contains(TEST_PERSON1_NAME); assertThat(person).map(PersonWithNoConstructor::getFirstName).contains(TEST_PERSON1_FIRST_NAME); @@ -365,7 +366,26 @@ class RepositoryIT { @Test void loadOptionalPersonWithWither() { - Optional person = repository.getOptionalPersonsWithWitherViaQuery(); + Optional person = repository.getOptionalPersonWithWitherViaQuery(); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); + } + + @Test + void loadAllKotlinPersons() { + List persons = repository.getAllKotlinPersonsViaQuery(); + assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME)); + } + + @Test + void loadOneKotlinPerson() { + KotlinPerson person = repository.getOneKotlinPersonViaQuery(); + assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME); + } + + @Test + void loadOptionalKotlinPerson() { + Optional person = repository.getOptionalKotlinPersonViaQuery(); assertThat(person).isPresent(); assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } @@ -680,7 +700,7 @@ class RepositoryIT { public NodeManagerFactory nodeManagerFactory(Driver driver) { return new NodeManagerFactory(driver, PersonWithAllConstructor.class, PersonWithNoConstructor.class, - PersonWithWither.class, ThingWithAssignedId.class); + PersonWithWither.class, ThingWithAssignedId.class, KotlinPerson.class); } @Bean diff --git a/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/KotlinPerson.kt b/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/KotlinPerson.kt new file mode 100644 index 000000000..7bf8f11ea --- /dev/null +++ b/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/KotlinPerson.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration + +import org.springframework.data.neo4j.core.schema.Id +import org.springframework.data.neo4j.core.schema.Node + +/** + * @author Gerrit Meier + */ +@Node +data class KotlinPerson(@Id val id: Long, val name: String) From 7f87ef0b0fb6998f02d5de6fed23dd0b508b0b5f Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 17 May 2019 13:57:38 +0200 Subject: [PATCH 086/342] Add support for Map Expression, spatial functions and property dereferencing. --- .../data/neo4j/core/cypher/Cypher.java | 35 +++++++++- .../data/neo4j/core/cypher/Expression.java | 20 ++++++ .../data/neo4j/core/cypher/Functions.java | 14 ++++ .../data/neo4j/core/cypher/MapEntry.java | 55 ++++++++++++++++ .../data/neo4j/core/cypher/MapExpression.java | 64 +++++++++++++++++++ .../data/neo4j/core/cypher/Property.java | 36 +++-------- .../cypher/renderer/RenderingVisitor.java | 15 +++++ .../data/neo4j/core/cypher/CypherTest.java | 22 +++++++ 8 files changed, 231 insertions(+), 30 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapEntry.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index 57493a922..6da668c2c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -69,14 +69,34 @@ public final class Cypher { return Node.create().named(symbolicName); } + /** + * Dereferences a property for a symbolic name, most likely pointing to a property container like a node or a relationship. + * + * @param containerName The symbolic name of a property container + * @param name The name of the property to dereference + * @return A new property + */ public static Property property(String containerName, String name) { return property(symbolicName(containerName), name); } - public static Property property(SymbolicName containerName, String name) { - return Property.create(containerName, name); + /** + * Dereferences a property on a arbitrary expression. + * + * @param expression The expression that describes some sort of accessible map + * @param name The name of the property to dereference + * @return A new property. + */ + public static Property property(Expression expression, String name) { + return Property.create(expression, name); } + /** + * Creates a new symbolic name. + * + * @param value The value of the symbolic name + * @return A new symoblic name + */ public static SymbolicName symbolicName(String value) { return new SymbolicName(value); } @@ -125,6 +145,17 @@ public final class Cypher { return SortItem.create(expression, null); } + /** + * Creates a map of expression from a list of key/value pairs. + * + * @param keysAndValues A list of key and values. Must be an even number, with alternating {@link String} and {@link Expression} + * @return A new map expression. + */ + public static Expression mapOf(Object... keysAndValues) { + + return MapExpression.create(keysAndValues); + } + /** * Creates a new {@link NullLiteral} from the given {@code object}. * diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java index 1586bf828..c9b1efb26 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java @@ -186,4 +186,24 @@ public interface Expression extends Visitable { default Condition isEmpty() { return Conditions.isEmpty(this); } + + /** + * The property does not track the sort items created here. + * + * @return A sort item for this property in descending order + */ + default SortItem descending() { + + return SortItem.create(this, SortItem.Direction.DESC); + } + + /** + * The property does not track the sort items created here. + * + * @return A sort item for this property in ascending order + */ + default SortItem ascending() { + + return SortItem.create(this, SortItem.Direction.ASC); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java index 7a530c440..b4852990b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java @@ -160,6 +160,20 @@ public final class Functions { return new FunctionInvocation("distance", point1, point2); } + /** + * Creates a function invocation for the {@code point()} function. + * See point + * + * @param parameterMap The map of parameters for {@code point()} + * @return A function call for {@code point()} + */ + public static FunctionInvocation point(Expression parameterMap) { + + Assert.notNull(parameterMap, "The parameter map is required."); + + return new FunctionInvocation("point", parameterMap); + } + private Functions() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapEntry.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapEntry.java new file mode 100644 index 000000000..aa662acd1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapEntry.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * Helper class, only for internal use. + * + * @author Michael J. Simons + * @since 1.0 + * @soundtrack Rammstein - RAMMSTEIN + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class MapEntry implements Visitable { + + private final String key; + + private final Expression value; + + MapEntry(String key, Expression value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + value.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java new file mode 100644 index 000000000..938f92760 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; +import org.springframework.util.Assert; + +/** + * A dedicated map expression.

+ * Most of the comparision methods on this expression will not result in a sensible query fragment. + * A {@link MapExpression} will be useful as a concrete parameter to functions. + * + * @author Michael J. Simons + * @soundtrack Rammstein - RAMMSTEIN + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class MapExpression extends TypedSubtree implements Expression { + + static MapExpression create(Object... input) { + + Assert.state(input.length % 2 == 0, "Need an even number of input parameters"); + List newContent = new ArrayList<>(input.length / 2); + Set knownKeys = new HashSet<>(); + + for (int i = 0; i < input.length; i += 2) { + Assert.isInstanceOf(String.class, input[i], "Key needs to be of type String."); + Assert.isInstanceOf(Expression.class, input[i + 1], "Value needs to be of type Expression."); + Assert.state(!knownKeys.contains(input[i]), "Duplicate key '" + input[i] + "'"); + + final MapEntry entry = new MapEntry((String) input[i], (Expression) input[i + 1]); + newContent.add(entry); + knownKeys.add(entry.getKey()); + } + + return new MapExpression(newContent); + } + + public MapExpression(List children) { + super(children); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java index e37425cef..b317c7021 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java @@ -40,28 +40,28 @@ public final class Property implements Expression { return new Property(parentContainer.getSymbolicName().get(), name); } - static Property create(SymbolicName containerName, String name) { + static Property create(Expression container, String name) { - Assert.notNull(containerName, "The property containers name is required."); + Assert.notNull(container, "The property container is required."); Assert.hasText(name, "The properties name is required."); - return new Property(containerName, name); + return new Property(container, name); } /** - * The symbolic name of the parent container. + * The expression describinng the container. */ - private final SymbolicName containerName; + private final Expression container; /** * The name of this property. */ private final String name; - Property(SymbolicName containerName, String name) { + Property(Expression container, String name) { - this.containerName = containerName; + this.container = container; this.name = name; } @@ -72,27 +72,7 @@ public final class Property implements Expression { @Override public void accept(Visitor visitor) { visitor.enter(this); - this.containerName.accept(visitor); + this.container.accept(visitor); visitor.leave(this); } - - /** - * The property does not track the sort items created here. - * - * @return A sort item for this property in descending order - */ - public SortItem descending() { - - return SortItem.create(this, SortItem.Direction.DESC); - } - - /** - * The property does not track the sort items created here. - * - * @return A sort item for this property in ascending order - */ - public SortItem ascending() { - - return SortItem.create(this, SortItem.Direction.ASC); - } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index 725f445e2..a68beecc3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -294,6 +294,21 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append("$").append(parameter.getName()); } + void enter(MapExpression map) { + + builder.append("{"); + } + + void enter(MapEntry map) { + + builder.append(map.getKey()).append(": "); + } + + void leave(MapExpression map) { + + builder.append("}"); + } + public String getRenderedContent() { return this.builder.toString(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 113b22412..9c6de7f23 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -962,6 +962,28 @@ public class CypherTest { .isEqualTo( "MATCH (n) RETURN ((1 > 2) = true) = false"); } + } + @Nested + class ExpressionsRendering { + @Test + void shouldRenderMap() { + Statement statement; + statement = Cypher.match(Cypher.anyNode("n")) + .returning( + Functions.point( + Cypher.mapOf( + "latitude", Cypher.parameter("latitude"), + "longitude", Cypher.parameter("longitude"), + "crs", Cypher.literalOf(4326) + ) + ) + ) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN point({latitude: $latitude, longitude: $longitude, crs: 4326})"); + } } } From 282988f3460449ff0963a40e7800b3a128a8dd83 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 17 May 2019 14:01:50 +0200 Subject: [PATCH 087/342] Add full support for NEAR and WITHIN(Circle). --- .../data/neo4j/core/cypher/Cypher.java | 2 +- .../data/neo4j/core/cypher/Functions.java | 2 +- .../repository/query/CypherQueryCreator.java | 94 ++++++++++++++++--- .../repository/query/PartTreeNeo4jQuery.java | 44 ++++----- .../neo4j/integration/PersonRepository.java | 9 +- .../data/neo4j/integration/RepositoryIT.java | 18 ++++ 6 files changed, 125 insertions(+), 44 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index 6da668c2c..58bc65f9b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -151,7 +151,7 @@ public final class Cypher { * @param keysAndValues A list of key and values. Must be an even number, with alternating {@link String} and {@link Expression} * @return A new map expression. */ - public static Expression mapOf(Object... keysAndValues) { + public static MapExpression mapOf(Object... keysAndValues) { return MapExpression.create(keysAndValues); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java index b4852990b..46266ac36 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java @@ -167,7 +167,7 @@ public final class Functions { * @param parameterMap The map of parameters for {@code point()} * @return A function call for {@code point()} */ - public static FunctionInvocation point(Expression parameterMap) { + public static FunctionInvocation point(MapExpression parameterMap) { Assert.notNull(parameterMap, "The parameter map is required."); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 78daaf9cd..3ff1f6579 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -20,16 +20,22 @@ package org.springframework.data.neo4j.repository.query; import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.core.cypher.Functions.*; import lombok.RequiredArgsConstructor; import lombok.ToString; +import java.util.ArrayList; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.Optional; +import java.util.Queue; import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.neo4j.core.cypher.Condition; @@ -38,6 +44,7 @@ import org.springframework.data.neo4j.core.cypher.Cypher; import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Property; +import org.springframework.data.neo4j.core.cypher.SortItem; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -54,6 +61,8 @@ import org.springframework.data.repository.query.parser.PartTree; /** * A Cypher-DSL based implementation of the {@link AbstractQueryCreator} that eventually creates Cypher queries as strings * to be used by a Neo4j client or driver as statement template. + *

+ * This class is not thread safe and not reusable. * * @author Michael J. Simons * @since 1.0 @@ -65,6 +74,12 @@ final class CypherQueryCreator extends AbstractQueryCreator { private final NodeDescription nodeDescription; private Iterator formalParameters; + private final Queue lastParameter = new LinkedList<>(); + + /** + * Sort items may already be needed for some parts, i.e. of type NEAR. + */ + private final List sortItems = new ArrayList<>(); CypherQueryCreator(Neo4jMappingContext mappingContext, Class domainType, PartTree tree, Parameters formalParameters, ParameterAccessor actualParameters @@ -103,6 +118,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { Statement statement = mappingContext .prepareMatchOf(nodeDescription, Optional.of(condition)) .returning(Cypher.asterisk()) + .orderBy(sortItems.toArray(new SortItem[sortItems.size()])) .build(); return CypherRenderer.create().render(statement); @@ -174,6 +190,8 @@ final class CypherQueryCreator extends AbstractQueryCreator { .matches(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); case TRUE: return property(persistentProperty).isTrue(); + case WITHIN: + return createWithinCondition(persistentProperty, actualParameters); default: throw new IllegalArgumentException("Unsupported part type: " + part.getType()); } @@ -202,17 +220,17 @@ final class CypherQueryCreator extends AbstractQueryCreator { Iterator actualParameters) { Parameter p1 = nextRequiredParameter(actualParameters); - Parameter p2 = nextRequiredParameter(actualParameters); + Optional p2 = nextOptionalParameter(actualParameters); Expression referencePoint; - Parameter other; + Optional other; if (p1.value instanceof Point) { referencePoint = parameter(p1.nameOrIndex); other = p2; - } else if (p2.value instanceof Point) { - referencePoint = parameter(p2.nameOrIndex); - other = p1; + } else if (p2.isPresent() && p2.get().value instanceof Point) { + referencePoint = parameter(p2.get().nameOrIndex); + other = Optional.of(p1); } else { throw new IllegalArgumentException( String.format("The NEAR operation requires a reference point of type %s", Point.class)); @@ -220,13 +238,36 @@ final class CypherQueryCreator extends AbstractQueryCreator { Expression distanceFunction = Functions.distance(property(persistentProperty), referencePoint); - if (other.value instanceof Distance) { - return distanceFunction.lte(parameter(other.nameOrIndex)); - } else if (other.value instanceof Range) { - return createRangeConditionForProperty(distanceFunction, other); + if (other.filter(p -> p.hasValueOfType(Distance.class)).isPresent()) { + return distanceFunction.lte(parameter(other.get().nameOrIndex)); + } else if (other.filter(p -> p.hasValueOfType(Range.class)).isPresent()) { + return createRangeConditionForProperty(distanceFunction, other.get()); + } else { + // We only have a point parameter, that's ok, but we have to put back the last parameter when it wasn't null + other.ifPresent(this.lastParameter::offer); + + // Also, we cannot filter, but need to sort in the end. + this.sortItems.add(distanceFunction.ascending()); + return Conditions.noCondition(); + } + } + + private Condition createWithinCondition(Neo4jPersistentProperty persistentProperty, + Iterator actualParameters) { + + Parameter area = nextRequiredParameter(actualParameters); + if (area.hasValueOfType(Circle.class)) { + // We don't know the CRS of the point, so we assume the same as the reference property + Expression referencePoint = point(mapOf( + "x", parameter(area.nameOrIndex + ".x"), + "y", parameter(area.nameOrIndex + ".y"), + "srid", Cypher.property(property(persistentProperty), "srid")) + ); + Expression distanceFunction = Functions.distance(property(persistentProperty), referencePoint); + return distanceFunction.lte(parameter(area.nameOrIndex + ".radius")); } else { throw new IllegalArgumentException( - String.format("The NEAR operation requires a distance or a range of distances.")); + String.format("The WITHIN operation requires an area of type %s or %s.", Circle.class)); } } @@ -259,12 +300,31 @@ final class CypherQueryCreator extends AbstractQueryCreator { return Cypher.property(NodeDescription.NAME_OF_ROOT_NODE, persistentProperty.getPropertyName()); } - private Parameter nextRequiredParameter(Iterator actualParameters) { - if (!formalParameters.hasNext()) { - throw new IllegalStateException("Not enough formal, bindable parameters for parts"); + private Optional nextOptionalParameter(Iterator actualParameters) { + + Parameter nextRequiredParameter = lastParameter.poll(); + if (nextRequiredParameter != null) { + return Optional.of(nextRequiredParameter); + } else if (formalParameters.hasNext()) { + final Neo4jParameter parameter = formalParameters.next(); + return Optional.of(new Parameter(parameter.getNameOrIndex(), actualParameters.next())); + } else { + return Optional.empty(); + } + } + + private Parameter nextRequiredParameter(Iterator actualParameters) { + + Parameter nextRequiredParameter = lastParameter.poll(); + if (nextRequiredParameter != null) { + return nextRequiredParameter; + } else { + if (!formalParameters.hasNext()) { + throw new IllegalStateException("Not enough formal, bindable parameters for parts"); + } + final Neo4jParameter parameter = formalParameters.next(); + return new Parameter(parameter.getNameOrIndex(), actualParameters.next()); } - final Neo4jParameter parameter = formalParameters.next(); - return new Parameter(parameter.getNameOrIndex(), actualParameters.next()); } @RequiredArgsConstructor(access = PACKAGE) @@ -274,5 +334,9 @@ final class CypherQueryCreator extends AbstractQueryCreator { final String nameOrIndex; final Object value; + + boolean hasValueOfType(Class type) { + return type.isInstance(value); + } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index c78c665d9..bb50aa20d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -32,17 +32,16 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; +import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; -import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; @@ -51,7 +50,6 @@ import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree.OrPart; import org.springframework.data.util.ClassTypeInformation; -import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** @@ -87,9 +85,8 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { this.processor = queryMethod.getResultProcessor(); this.tree = new PartTree(queryMethod.getName(), domainType); - AtomicInteger parameterCounter = new AtomicInteger(); this.tree.flatMap(OrPart::stream) - .forEach(part -> validatePart(parameterCounter, part)); + .forEach(part -> validatePart(part)); } @Override @@ -113,26 +110,20 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { .build(); } - void validatePart(AtomicInteger index, Part part) { + void validatePart(Part part) { - int currentParameterIndex = index.getAndAdd(part.getNumberOfArguments()); switch (part.getType()) { case AFTER: case BEFORE: validateTemporalProperty(part); break; - case IN: - case NOT_IN: - Parameter parameter = this.queryMethod.getParameters() - .getBindableParameter(currentParameterIndex); - validateCollectionParameter(part, parameter); - break; case IS_EMPTY: case IS_NOT_EMPTY: validateCollectionProperty(part); break; + case NEAR: case WITHIN: - validatePointPropertyAndParameters(part); + validatePointProperty(part); break; } } @@ -144,28 +135,24 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { COMPARABLE_TEMPORAL_TYPES)); } - void validateCollectionParameter(Part part, Parameter parameter) { - - TypeInformation information = ClassTypeInformation.from(parameter.getType()); - Assert.state(information.isCollectionLike(), - () -> String.format("%s works only with collection parameters", part.getType())); - } - void validateCollectionProperty(Part part) { Assert.state(part.getProperty().getLeafProperty().isCollection(), () -> String .format("%s works only with collection properties", part.getType())); } - void validatePointPropertyAndParameters(Part part) { + void validatePointProperty(Part part) { Assert.state(ClassTypeInformation.from(Point.class) .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), () -> String .format("%s works only with spatial properties", part.getType())); - // TODO Check if 2 or 3 parameters are always bound correctly. - // Assert.state(part.getNumberOfArguments() == 2,"The distance operation needs two arguments: The reference point and the distance!"); } - // TODO Have fun with a bunch of conversion services + /** + * Converts parameter as needed by the query generated, which is not covered by standard conversion services + * + * @param parameter The parameter to fit into the generated query. + * @return A parameter that fits the place holders of a generated query + */ static Object convertParameter(Object parameter) { if (parameter instanceof Range) { Range range = (Range) parameter; @@ -175,6 +162,13 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { return map; } else if (parameter instanceof Distance) { return calculateDistanceInMeter((Distance) parameter); + } else if (parameter instanceof Circle) { + Circle circle = (Circle) parameter; + Map map = new HashMap<>(); + map.put("x", convertParameter(circle.getCenter().getX())); + map.put("y", convertParameter(circle.getCenter().getY())); + map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); + return map; } return parameter; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 6e9d6f7cb..be88f3830 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -24,6 +24,7 @@ import java.util.Optional; import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; +import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; @@ -132,12 +133,16 @@ public interface PersonRepository extends Neo4jRepository findAllByNullableExists(); + List findAllByPlaceNear(Point p); + List findAllByPlaceNear(Point p, Distance max); - List findAllByPlaceNear(Point p, Distance min, Distance max); - List findAllByPlaceNear(Point p, Range between); + List findAllByPlaceNearAndFirstNameIn(Point p, List haystack); + + List findAllByPlaceWithin(Circle circle); + // TODO Integration tests for failed validations // List findAllByBornOnAfter(String date); // List findAllByNameOrPersonNumberIsBetweenAndFirstNameNotInAndFirstNameEquals(String name, Long low, Long high, String wrong, List haystack); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 98c331df6..cef353d19 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -46,6 +46,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.NodeManagerFactory; @@ -660,6 +661,14 @@ class RepositoryIT { void findByNear() { List persons; + persons = repository.findAllByPlaceNear(SFO); + assertThat(persons) + .containsExactly(person2, person1); + + persons = repository.findAllByPlaceNearAndFirstNameIn(SFO, Collections.singletonList(TEST_PERSON1_FIRST_NAME)); + assertThat(persons) + .containsExactly(person1); + persons = repository.findAllByPlaceNear(MINC, new Distance(200.0 / 1000.0, Metrics.KILOMETERS)); assertThat(persons) .hasSize(1) @@ -683,6 +692,15 @@ class RepositoryIT { assertThat(persons) .hasSize(1) .contains(person2); + + persons = repository.findAllByPlaceWithin(new Circle(new org.springframework.data.geo.Point(MINC.x(), MINC.y()), new Distance(200.0 / 1000.0, Metrics.KILOMETERS))); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByPlaceNear(CLARION, new Distance(200.0 / 1000.0, Metrics.KILOMETERS)); + assertThat(persons).isEmpty(); + } @Configuration From 48bde65f9eb1675f0f8c98a508166eaf02fb945c Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 17 May 2019 15:52:18 +0200 Subject: [PATCH 088/342] Add support for ignoring case on derived finder methods. --- .../repository/query/CypherQueryCreator.java | 164 ++++++++++++------ .../repository/query/PartTreeNeo4jQuery.java | 37 +++- .../neo4j/integration/PersonRepository.java | 24 +++ .../data/neo4j/integration/RepositoryIT.java | 96 ++++++++-- 4 files changed, 251 insertions(+), 70 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 3ff1f6579..0b62cab81 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.repository.query; import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; import static org.springframework.data.neo4j.core.cypher.Functions.*; +import static org.springframework.data.neo4j.repository.query.PartTreeNeo4jQuery.*; import lombok.RequiredArgsConstructor; import lombok.ToString; @@ -43,7 +44,6 @@ import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.Property; import org.springframework.data.neo4j.core.cypher.SortItem; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; @@ -128,68 +128,77 @@ final class CypherQueryCreator extends AbstractQueryCreator { PersistentPropertyPath path = mappingContext .getPersistentPropertyPath(part.getProperty()); - Neo4jPersistentProperty persistentProperty = path.getLeafProperty(); - + Neo4jPersistentProperty persistentProperty = path.getRequiredLeafProperty(); // TODO case insensitive (like, notlike, simpleProperty, negatedSimpleProperty) + + boolean ignoreCase = ignoreCase(part); switch (part.getType()) { case AFTER: case GREATER_THAN: - return property(persistentProperty).gt(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .gt(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case BEFORE: case LESS_THAN: - return property(persistentProperty).lt(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .lt(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case BETWEEN: - return betweenCondition(persistentProperty, actualParameters); + return betweenCondition(persistentProperty, actualParameters, ignoreCase); case CONTAINING: - return property(persistentProperty) - .contains(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .contains(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case ENDING_WITH: - return property(persistentProperty) - .endsWith(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .endsWith(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case EXISTS: - return Conditions.exists(property(persistentProperty)); + return Conditions.exists(toCypherProperty(persistentProperty, ignoreCase)); case FALSE: - return property(persistentProperty).isFalse(); + return toCypherProperty(persistentProperty, ignoreCase).isFalse(); case GREATER_THAN_EQUAL: - return property(persistentProperty).gte(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .gte(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case IN: - return property(persistentProperty).in(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .in(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case IS_EMPTY: - return property(persistentProperty).isEmpty(); + return toCypherProperty(persistentProperty, ignoreCase).isEmpty(); case IS_NOT_EMPTY: - return property(persistentProperty).isEmpty().not(); + return toCypherProperty(persistentProperty, ignoreCase).isEmpty().not(); case IS_NOT_NULL: - return property(persistentProperty).isNotNull(); + return toCypherProperty(persistentProperty, ignoreCase).isNotNull(); case IS_NULL: - return property(persistentProperty).isNull(); + return toCypherProperty(persistentProperty, ignoreCase).isNull(); case LESS_THAN_EQUAL: - return property(persistentProperty).lte(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .lte(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case LIKE: - return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex); + return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex, + ignoreCase); case NEAR: return createNearCondition(persistentProperty, actualParameters); case NEGATING_SIMPLE_PROPERTY: - return property(persistentProperty) - .isNotEqualTo(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .isNotEqualTo(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case NOT_CONTAINING: - return property(persistentProperty) - .contains(parameter(nextRequiredParameter(actualParameters).nameOrIndex)).not(); + return toCypherProperty(persistentProperty, ignoreCase) + .contains(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)).not(); case NOT_IN: - return property(persistentProperty).in(parameter(nextRequiredParameter(actualParameters).nameOrIndex)) + return toCypherProperty(persistentProperty, ignoreCase) + .in(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)) .not(); case NOT_LIKE: - return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex).not(); + return likeCondition(persistentProperty, nextRequiredParameter(actualParameters).nameOrIndex, + ignoreCase).not(); case SIMPLE_PROPERTY: - return property(persistentProperty) - .isEqualTo(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .isEqualTo(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case STARTING_WITH: - return property(persistentProperty) - .startsWith(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .startsWith(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case REGEX: - return property(persistentProperty) - .matches(parameter(nextRequiredParameter(actualParameters).nameOrIndex)); + return toCypherProperty(persistentProperty, ignoreCase) + .matches(toCypherParameter(nextRequiredParameter(actualParameters), ignoreCase)); case TRUE: - return property(persistentProperty).isTrue(); + return toCypherProperty(persistentProperty, ignoreCase).isTrue(); case WITHIN: return createWithinCondition(persistentProperty, actualParameters); default: @@ -197,22 +206,46 @@ final class CypherQueryCreator extends AbstractQueryCreator { } } - private Condition likeCondition(Neo4jPersistentProperty persistentProperty, String parameterName) { - return property(persistentProperty) - .matches(literalOf(".*").plus(parameter(parameterName)).plus(literalOf(".*"))); + /** + * Checks whether or not to ignore the case for some operations. {@link PartTreeNeo4jQuery} will already have validated + * which properties can be made case insenstive given a certain keyword. + * + * @param p + * @return + */ + boolean ignoreCase(Part p) { + + switch (p.shouldIgnoreCase()) { + case ALWAYS: + return true; + case WHEN_POSSIBLE: + return canIgnoreCase(p); + case NEVER: + return false; + default: + throw new IllegalArgumentException("Unsupported option for ignoring case: " + p.shouldIgnoreCase()); + } } - private Condition betweenCondition(Neo4jPersistentProperty persistentProperty, Iterator actualParameters) { + private Condition likeCondition(Neo4jPersistentProperty persistentProperty, String parameterName, + boolean ignoreCase) { + String regexOptions = ignoreCase ? "(?i)" : ""; + return toCypherProperty(persistentProperty, false) + .matches(literalOf(regexOptions + ".*").plus(Cypher.parameter(parameterName)).plus(literalOf(".*"))); + } + + private Condition betweenCondition(Neo4jPersistentProperty persistentProperty, Iterator actualParameters, + boolean ignoreCase) { Parameter lowerBoundOrRange = nextRequiredParameter(actualParameters); - Property property = property(persistentProperty); + Expression property = toCypherProperty(persistentProperty, ignoreCase); if (lowerBoundOrRange.value instanceof Range) { return createRangeConditionForProperty(property, lowerBoundOrRange); } else { Parameter upperBound = nextRequiredParameter(actualParameters); - return property.gte(parameter(lowerBoundOrRange.nameOrIndex)) - .and(property.lte(parameter(upperBound.nameOrIndex))); + return property.gte(toCypherParameter(lowerBoundOrRange, ignoreCase)) + .and(property.lte(toCypherParameter(upperBound, ignoreCase))); } } @@ -226,24 +259,24 @@ final class CypherQueryCreator extends AbstractQueryCreator { Optional other; if (p1.value instanceof Point) { - referencePoint = parameter(p1.nameOrIndex); + referencePoint = toCypherParameter(p1, false); other = p2; } else if (p2.isPresent() && p2.get().value instanceof Point) { - referencePoint = parameter(p2.get().nameOrIndex); + referencePoint = toCypherParameter(p2.get(), false); other = Optional.of(p1); } else { throw new IllegalArgumentException( String.format("The NEAR operation requires a reference point of type %s", Point.class)); } - Expression distanceFunction = Functions.distance(property(persistentProperty), referencePoint); + Expression distanceFunction = Functions.distance(toCypherProperty(persistentProperty, false), referencePoint); if (other.filter(p -> p.hasValueOfType(Distance.class)).isPresent()) { - return distanceFunction.lte(parameter(other.get().nameOrIndex)); + return distanceFunction.lte(toCypherParameter(other.get(), false)); } else if (other.filter(p -> p.hasValueOfType(Range.class)).isPresent()) { return createRangeConditionForProperty(distanceFunction, other.get()); } else { - // We only have a point parameter, that's ok, but we have to put back the last parameter when it wasn't null + // We only have a point toCypherParameter, that's ok, but we have to put back the last toCypherParameter when it wasn't null other.ifPresent(this.lastParameter::offer); // Also, we cannot filter, but need to sort in the end. @@ -257,14 +290,15 @@ final class CypherQueryCreator extends AbstractQueryCreator { Parameter area = nextRequiredParameter(actualParameters); if (area.hasValueOfType(Circle.class)) { - // We don't know the CRS of the point, so we assume the same as the reference property + // We don't know the CRS of the point, so we assume the same as the reference toCypherProperty Expression referencePoint = point(mapOf( - "x", parameter(area.nameOrIndex + ".x"), - "y", parameter(area.nameOrIndex + ".y"), - "srid", Cypher.property(property(persistentProperty), "srid")) + "x", createCypherParameter(area.nameOrIndex + ".x", false), + "y", createCypherParameter(area.nameOrIndex + ".y", false), + "srid", Cypher.property(toCypherProperty(persistentProperty, false), "srid")) ); - Expression distanceFunction = Functions.distance(property(persistentProperty), referencePoint); - return distanceFunction.lte(parameter(area.nameOrIndex + ".radius")); + Expression distanceFunction = Functions + .distance(toCypherProperty(persistentProperty, false), referencePoint); + return distanceFunction.lte(createCypherParameter(area.nameOrIndex + ".radius", false)); } else { throw new IllegalArgumentException( String.format("The WITHIN operation requires an area of type %s or %s.", Circle.class)); @@ -281,14 +315,14 @@ final class CypherQueryCreator extends AbstractQueryCreator { Range range = (Range) rangeParameter.value; Condition betweenCondition = Conditions.noCondition(); if (range.getLowerBound().isBounded()) { - Expression parameterPlaceholder = parameter(rangeParameter.nameOrIndex + ".lb"); + Expression parameterPlaceholder = createCypherParameter(rangeParameter.nameOrIndex + ".lb", false); betweenCondition = betweenCondition.and(range.getLowerBound().isInclusive() ? property.gte(parameterPlaceholder) : property.gt(parameterPlaceholder)); } if (range.getUpperBound().isBounded()) { - Expression parameterPlaceholder = parameter(rangeParameter.nameOrIndex + ".ub"); + Expression parameterPlaceholder = createCypherParameter(rangeParameter.nameOrIndex + ".ub", false); betweenCondition = betweenCondition.and(range.getUpperBound().isInclusive() ? property.lte(parameterPlaceholder) : property.lt(parameterPlaceholder)); @@ -296,8 +330,28 @@ final class CypherQueryCreator extends AbstractQueryCreator { return betweenCondition; } - private static Property property(Neo4jPersistentProperty persistentProperty) { - return Cypher.property(NodeDescription.NAME_OF_ROOT_NODE, persistentProperty.getPropertyName()); + private static Expression toCypherProperty(Neo4jPersistentProperty persistentProperty, boolean addToLower) { + + Expression expression = Cypher + .property(NodeDescription.NAME_OF_ROOT_NODE, persistentProperty.getPropertyName()); + if (addToLower) { + expression = Functions.toLower(expression); + } + return expression; + } + + private static Expression toCypherParameter(Parameter parameter, boolean addToLower) { + + return createCypherParameter(parameter.nameOrIndex, addToLower); + } + + private static Expression createCypherParameter(String name, boolean addToLower) { + + Expression expression = Cypher.parameter(name); + if (addToLower) { + expression = Functions.toLower(expression); + } + return expression; } private Optional nextOptionalParameter(Iterator actualParameters) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index bb50aa20d..f1504fd58 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -27,7 +27,9 @@ import java.time.LocalDateTime; import java.time.OffsetTime; import java.time.ZonedDateTime; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -72,6 +74,11 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { .unmodifiableSet(new HashSet<>(Arrays.asList(LocalDate.class, OffsetTime.class, ZonedDateTime.class, LocalDateTime.class))); + private static final EnumSet TYPES_SUPPORTING_CASE_INSENSITIVITY = EnumSet + .of(Part.Type.CONTAINING, Part.Type.ENDING_WITH, Part.Type.LIKE, Part.Type.NEGATING_SIMPLE_PROPERTY, + Part.Type.NOT_CONTAINING, + Part.Type.NOT_LIKE, Part.Type.SIMPLE_PROPERTY, Part.Type.STARTING_WITH); + private final ResultProcessor processor; private final PartTree tree; @@ -112,6 +119,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { void validatePart(Part part) { + validateIgnoreCase(part); switch (part.getType()) { case AFTER: case BEFORE: @@ -128,23 +136,46 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { } } + static boolean canIgnoreCase(Part part) { + return part.getProperty().getLeafType() == String.class && TYPES_SUPPORTING_CASE_INSENSITIVITY + .contains(part.getType()); + } + + private String formatTypes(Collection types) { + return types.stream().flatMap(t -> t.getKeywords().stream()).collect(joining(", ", "[", "]")); + } + + void validateIgnoreCase(Part part) { + + Assert.state(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), + () -> String.format( + "Can not derive query for '%s': Only the case of String based properties can be ignored within the following keywords: %s", + super.queryMethod, + formatTypes(TYPES_SUPPORTING_CASE_INSENSITIVITY))); + } + void validateTemporalProperty(Part part) { Assert.state(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String - .format("%s works only with properties with one of the following types: %s", part.getType(), + .format( + "Can not derive query for '%s': The keywords %s work only with properties with one of the following types: %s", + super.queryMethod, formatTypes(Collections.singletonList(part.getType())), COMPARABLE_TEMPORAL_TYPES)); } void validateCollectionProperty(Part part) { Assert.state(part.getProperty().getLeafProperty().isCollection(), () -> String - .format("%s works only with collection properties", part.getType())); + .format("Can not derive query for '%s': The keywords %s work only with collection properties", + super.queryMethod, + formatTypes(Collections.singletonList(part.getType())))); } void validatePointProperty(Part part) { Assert.state(ClassTypeInformation.from(Point.class) .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), () -> String - .format("%s works only with spatial properties", part.getType())); + .format("Can not derive query for '%s': %s works only with spatial properties", super.queryMethod, + part.getType())); } /** diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index be88f3830..1ec95cf91 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -79,30 +79,48 @@ public interface PersonRepository extends Neo4jRepository findOneByNameAndFirstName(String name, String firstName); + Optional findOneByNameAndFirstNameAllIgnoreCase(String name, String firstName); + List findAllByNameOrName(String aName, String anotherName); List findAllBySameValue(String sameValue); + List findAllBySameValueIgnoreCase(String sameValue); + List findAllByNameNot(String name); + List findAllByNameNotIgnoreCase(String name); + List findAllByFirstNameLike(String name); + List findAllByFirstNameLikeIgnoreCase(String name); + List findAllByFirstNameMatches(String name); List findAllByFirstNameNotLike(String name); + List findAllByFirstNameNotLikeIgnoreCase(String name); + List findAllByCoolTrue(); List findAllByCoolFalse(); List findAllByFirstNameStartingWith(String name); + List findAllByFirstNameStartingWithIgnoreCase(String name); + List findAllByFirstNameContaining(String name); + List findAllByFirstNameContainingIgnoreCase(String name); + List findAllByFirstNameNotContaining(String name); + List findAllByFirstNameNotContainingIgnoreCase(String name); + List findAllByFirstNameEndingWith(String name); + List findAllByFirstNameEndingWithIgnoreCase(String name); + List findAllByPersonNumberIsLessThan(Long number); List findAllByPersonNumberIsLessThanEqual(Long number); @@ -115,6 +133,8 @@ public interface PersonRepository extends Neo4jRepository findAllByPersonNumberIsBetween(Long low, Long high); + List findAllByBornOn(LocalDate date); + List findAllByBornOnAfter(LocalDate date); List findAllByBornOnBefore(LocalDate date); @@ -139,8 +159,12 @@ public interface PersonRepository extends Neo4jRepository findAllByPlaceNear(Point p, Range between); + List findAllByPlaceNear(Range between, Point p); + List findAllByPlaceNearAndFirstNameIn(Point p, List haystack); + List findAllByPlaceNearAndFirstNameAllIgnoreCase(Point p, String firstName); + List findAllByPlaceWithin(Circle circle); // TODO Integration tests for failed validations diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index cef353d19..ac5caff1e 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -400,15 +400,29 @@ class RepositoryIT { @Test void findBySimpleProperty() { - List persons = repository.findAllBySameValue(TEST_PERSON_SAMEVALUE); + List persons; + + persons = repository.findAllBySameValue(TEST_PERSON_SAMEVALUE); assertThat(persons).containsExactlyInAnyOrder(person1, person2); + + persons = repository.findAllBySameValueIgnoreCase(TEST_PERSON_SAMEVALUE.toUpperCase()); + assertThat(persons).containsExactlyInAnyOrder(person1, person2); + + persons = repository.findAllByBornOn(TEST_PERSON1_BORN_ON); + assertThat(persons) + .hasSize(1) + .contains(person1); } @Test void findBySimplePropertiesAnded() { - Optional optionalPerson = repository - .findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME); + Optional optionalPerson; + + optionalPerson = repository.findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME); + assertThat(optionalPerson).isPresent().contains(person1); + + optionalPerson = repository.findOneByNameAndFirstNameAllIgnoreCase(TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON1_FIRST_NAME.toUpperCase()); assertThat(optionalPerson).isPresent().contains(person1); } @@ -422,7 +436,12 @@ class RepositoryIT { @Test void findByNegatedSimpleProperty() { - List persons = repository.findAllByNameNot(TEST_PERSON1_NAME); + List persons; + + persons = repository.findAllByNameNot(TEST_PERSON1_NAME); + assertThat(persons).doesNotContain(person1); + + persons = repository.findAllByNameNotIgnoreCase(TEST_PERSON1_NAME.toUpperCase()); assertThat(persons).doesNotContain(person1); } @@ -438,7 +457,14 @@ class RepositoryIT { @Test void findByLike() { - List persons = repository.findAllByFirstNameLike("Ern"); + List persons; + + persons = repository.findAllByFirstNameLike("Ern"); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByFirstNameLikeIgnoreCase("eRN"); assertThat(persons) .hasSize(1) .contains(person1); @@ -456,14 +482,26 @@ class RepositoryIT { @Test void findByNotLike() { - List persons = repository.findAllByFirstNameNotLike("Ern"); + List persons; + + persons = repository.findAllByFirstNameNotLike("Ern"); + assertThat(persons).doesNotContain(person1); + + persons = repository.findAllByFirstNameNotLikeIgnoreCase("eRN"); assertThat(persons).doesNotContain(person1); } @Test void findByStartingWith() { - List persons = repository.findAllByFirstNameStartingWith("Er"); + List persons; + + persons = repository.findAllByFirstNameStartingWith("Er"); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByFirstNameStartingWithIgnoreCase("eRN"); assertThat(persons) .hasSize(1) .contains(person1); @@ -472,7 +510,14 @@ class RepositoryIT { @Test void findByContaining() { - List persons = repository.findAllByFirstNameContaining("ni"); + List persons; + + persons = repository.findAllByFirstNameContaining("ni"); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByFirstNameContainingIgnoreCase("NI"); assertThat(persons) .hasSize(1) .contains(person1); @@ -481,7 +526,14 @@ class RepositoryIT { @Test void findByNotContaining() { - List persons = repository.findAllByFirstNameNotContaining("ni"); + List persons; + + persons = repository.findAllByFirstNameNotContaining("ni"); + assertThat(persons) + .hasSize(1) + .contains(person2); + + persons = repository.findAllByFirstNameNotContainingIgnoreCase("NI"); assertThat(persons) .hasSize(1) .contains(person2); @@ -490,7 +542,14 @@ class RepositoryIT { @Test void findByEndingWith() { - List persons = repository.findAllByFirstNameEndingWith("nie"); + List persons; + + persons = repository.findAllByFirstNameEndingWith("nie"); + assertThat(persons) + .hasSize(1) + .contains(person1); + + persons = repository.findAllByFirstNameEndingWithIgnoreCase("NIE"); assertThat(persons) .hasSize(1) .contains(person1); @@ -687,8 +746,13 @@ class RepositoryIT { Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)); assertThat(persons).isEmpty(); - persons = repository.findAllByPlaceNear(MINC, - Range.of(Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), Bound.unbounded())); + final Range distanceRange = Range.of(inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), unbounded()); + persons = repository.findAllByPlaceNear(MINC, distanceRange); + assertThat(persons) + .hasSize(1) + .contains(person2); + + persons = repository.findAllByPlaceNear(distanceRange, MINC); assertThat(persons) .hasSize(1) .contains(person2); @@ -700,7 +764,15 @@ class RepositoryIT { persons = repository.findAllByPlaceNear(CLARION, new Distance(200.0 / 1000.0, Metrics.KILOMETERS)); assertThat(persons).isEmpty(); + } + @Test + void findBySomeCaseInsensitiveProperties() { + + List persons; + persons = repository.findAllByPlaceNearAndFirstNameAllIgnoreCase(SFO, TEST_PERSON1_FIRST_NAME.toUpperCase()); + assertThat(persons) + .containsExactly(person1); } @Configuration From 49113379e26d6e7fafdfc59d37853f720e746627 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 17 May 2019 16:59:40 +0200 Subject: [PATCH 089/342] Add support for sort on derived queries. --- .../repository/query/CypherAdapterUtils.java | 83 +++++++++++++++++++ .../repository/query/CypherQueryCreator.java | 16 ++-- .../repository/query/PartTreeNeo4jQuery.java | 4 +- .../support/SimpleNeo4jRepository.java | 24 +----- .../neo4j/integration/PersonRepository.java | 2 + .../data/neo4j/integration/RepositoryIT.java | 9 ++ 6 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java new file mode 100644 index 000000000..74f19e4d5 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import static org.springframework.data.neo4j.core.cypher.Cypher.*; + +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.SortItem; +import org.springframework.data.neo4j.core.cypher.SymbolicName; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; +import org.springframework.data.neo4j.core.schema.NodeDescription; + +/** + * A set of shared utils to adapt {@code org.springframework.data.neo4j.core.cypher} functionality with Spring Data + * and vice versa. + * + * @author Michael J. Simons + * @soundtrack Rammstein - Herzeleid + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class CypherAdapterUtils { + + /** + * Maps Spring Datas {@link Sort.Order} to a {@link SortItem}. + * See {@link #toSortItems(NodeDescription, Sort)}. + * + * @param nodeDescription + * @return A stream if sort items. Will be empty when sort is unsorted. + */ + public static Function sortAdapterFor(NodeDescription nodeDescription) { + SymbolicName rootNode = Cypher.symbolicName(NodeDescription.NAME_OF_ROOT_NODE); + + return order -> { + String property = nodeDescription.getGraphProperty(order.getProperty()) + .map(GraphPropertyDescription::getPropertyName) + .orElseThrow(() -> new IllegalStateException( + String.format("Cannot order by the unknown graph property: '%s'", order.getProperty()))); + SortItem sortItem = Cypher.sort(property(rootNode, property)); + + // Spring's Sort.Order defaults to ascending, so we just need to change this if we have descending order. + if (order.isDescending()) { + sortItem = sortItem.descending(); + } + return sortItem; + }; + } + + /** + * Converts a Spring Data sort to an equivalent list of {@link SortItem sort items}. + * + * @param nodeDescription The node description to map the properties + * @param sort The sort object to convert + * @return An of sort items. It will be empty when sort is unsorted. + */ + public static SortItem[] toSortItems(NodeDescription nodeDescription, Sort sort) { + + return sort.stream().map(sortAdapterFor(nodeDescription)).toArray(SortItem[]::new); + } + + private CypherAdapterUtils() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 0b62cab81..8e3a74f7a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -21,6 +21,8 @@ package org.springframework.data.neo4j.repository.query; import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; import static org.springframework.data.neo4j.core.cypher.Functions.*; +import static org.springframework.data.neo4j.core.schema.NodeDescription.*; +import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; import static org.springframework.data.neo4j.repository.query.PartTreeNeo4jQuery.*; import lombok.RequiredArgsConstructor; @@ -32,6 +34,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Queue; +import java.util.stream.Stream; import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; @@ -116,9 +119,14 @@ final class CypherQueryCreator extends AbstractQueryCreator { protected String complete(Condition condition, Sort sort) { Statement statement = mappingContext - .prepareMatchOf(nodeDescription, Optional.of(condition)) + .prepareMatchOf(nodeDescription, Optional.ofNullable(condition)) .returning(Cypher.asterisk()) - .orderBy(sortItems.toArray(new SortItem[sortItems.size()])) + .orderBy( + Stream.concat( + sortItems.stream(), + sort.stream().map(sortAdapterFor(nodeDescription)) + ).toArray(SortItem[]::new) + ) .build(); return CypherRenderer.create().render(statement); @@ -129,7 +137,6 @@ final class CypherQueryCreator extends AbstractQueryCreator { PersistentPropertyPath path = mappingContext .getPersistentPropertyPath(part.getProperty()); Neo4jPersistentProperty persistentProperty = path.getRequiredLeafProperty(); - // TODO case insensitive (like, notlike, simpleProperty, negatedSimpleProperty) boolean ignoreCase = ignoreCase(part); switch (part.getType()) { @@ -332,8 +339,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { private static Expression toCypherProperty(Neo4jPersistentProperty persistentProperty, boolean addToLower) { - Expression expression = Cypher - .property(NodeDescription.NAME_OF_ROOT_NODE, persistentProperty.getPropertyName()); + Expression expression = Cypher.property(NAME_OF_ROOT_NODE, persistentProperty.getPropertyName()); if (addToLower) { expression = Functions.toLower(expression); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index f1504fd58..425d10782 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -92,8 +92,8 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { this.processor = queryMethod.getResultProcessor(); this.tree = new PartTree(queryMethod.getName(), domainType); - this.tree.flatMap(OrPart::stream) - .forEach(part -> validatePart(part)); + // Validate parts. Sort properties will be validated by Spring Data already. + this.tree.flatMap(OrPart::stream).forEach(part -> validatePart(part)); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 7942cc8b1..c5044578f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -22,6 +22,7 @@ import static java.util.Collections.*; import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; import static org.springframework.data.neo4j.core.schema.NodeDescription.*; +import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -51,7 +52,6 @@ import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.SortItem; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; import org.springframework.data.neo4j.core.cypher.SymbolicName; @@ -121,7 +121,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) .returning(asterisk()) - .orderBy(createSort(sort)) + .orderBy(toSortItems(nodeDescription, sort)) .build(); return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); @@ -167,7 +167,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { int pageSize = pageable.getPageSize(); - return returning.orderBy(createSort(sort)).skip(skip).limit(pageSize); + return returning.orderBy(toSortItems(nodeDescription, sort)).skip(skip).limit(pageSize); } @Override @@ -286,7 +286,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { Statement statement = mappingContext .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(asterisk()) - .orderBy(createSort(sort)).build(); + .orderBy(toSortItems(nodeDescription, sort)).build(); PreparedQuery preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters); return nodeManager.toExecutableQuery(preparedQuery).getResults(); @@ -419,22 +419,6 @@ class SimpleNeo4jRepository implements Neo4jRepository { return predicate; } - private SortItem[] createSort(Sort sort) { - - SymbolicName rootNode = Cypher.symbolicName("n"); - - return sort.stream().map(order -> { - String property = order.getProperty(); - SortItem sortItem = Cypher.sort(property(rootNode, property)); - - // Spring's Sort.Order defaults to ascending, so we just need to change this if we have descending order. - if (order.isDescending()) { - sortItem = sortItem.descending(); - } - return sortItem; - }).toArray(SortItem[]::new); - } - private PreparedQuery prepareQuery(Statement statement) { return prepareQuery(nodeClass, statement, Collections.emptyMap()); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 1ec95cf91..9fc1d58fd 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -167,6 +167,8 @@ public interface PersonRepository extends Neo4jRepository findAllByPlaceWithin(Circle circle); + List findAllByOrderByFirstNameAscBornOnDesc(); + // TODO Integration tests for failed validations // List findAllByBornOnAfter(String date); // List findAllByNameOrPersonNumberIsBetweenAndFirstNameNotInAndFirstNameEquals(String name, Long low, Long high, String wrong, List haystack); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index ac5caff1e..000155533 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -716,6 +716,15 @@ class RepositoryIT { .contains(person1); } + @Test + void shouldSupportSort() { + List persons; + + persons = repository.findAllByOrderByFirstNameAscBornOnDesc(); + assertThat(persons) + .containsExactly(person2, person1); + } + @Test void findByNear() { List persons; From ed962790aebc7bb9599504debed2751595ab5f11 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 20 May 2019 11:02:00 +0200 Subject: [PATCH 090/342] Add support for limiting keywords in derived query methods. --- .../core/cypher/DefaultStatementBuilder.java | 15 +++++++--- .../data/neo4j/core/cypher/Limit.java | 7 ++++- .../data/neo4j/core/cypher/Skip.java | 7 ++++- .../neo4j/core/cypher/StatementBuilder.java | 13 +++++---- .../repository/query/CypherQueryCreator.java | 7 +++++ .../repository/query/PartTreeNeo4jQuery.java | 1 + .../data/neo4j/core/cypher/CypherTest.java | 28 +++++++++++++++++++ .../data/neo4j/integration/RepositoryIT.java | 24 +++++++++++++++- .../neo4j/integration/ThingRepository.java | 5 ++++ 9 files changed, 94 insertions(+), 13 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index 5362fb301..fc2331324 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -27,6 +27,7 @@ import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithWhere; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -225,14 +226,20 @@ class DefaultStatementBuilder } @Override - public final OngoingMatchAndReturn skip(Number number) { - skip = Skip.of(number); + public final OngoingMatchAndReturn skip(@Nullable Number number) { + + if (number != null) { + skip = Skip.create(number); + } return this; } @Override - public final OngoingMatchAndReturn limit(Number number) { - limit = Limit.of(number); + public final OngoingMatchAndReturn limit(@Nullable Number number) { + + if (number != null) { + limit = Limit.create(number); + } return this; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java index 5fc7aa7f8..8fed0dc97 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; /** * @author Gerrit Meier @@ -30,7 +31,10 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; @API(status = API.Status.INTERNAL, since = "1.0") public final class Limit implements Visitable { - public static Limit of(Number value) { + static Limit create(Number value) { + + Assert.notNull(value, "A limit cannot have a null value."); + return new Limit(new NumberLiteral(value)); } @@ -44,5 +48,6 @@ public final class Limit implements Visitable { public void accept(Visitor visitor) { visitor.enter(this); limitAmount.accept(visitor); + visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java index 9e3d6946f..0f71fd6d3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; /** * @author Gerrit Meier @@ -30,7 +31,10 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; @API(status = API.Status.INTERNAL, since = "1.0") public final class Skip implements Visitable { - public static Skip of(Number value) { + static Skip create(Number value) { + + Assert.notNull(value, "Cannot skip a value of null."); + return new Skip(new NumberLiteral(value)); } @@ -44,5 +48,6 @@ public final class Skip implements Visitable { public void accept(Visitor visitor) { visitor.enter(this); skipAmount.accept(visitor); + visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index 4b0b8bf64..6051e95c9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; +import org.springframework.lang.Nullable; /** * @author Michael J. Simons @@ -236,10 +237,10 @@ public interface StatementBuilder { /** * Adds a skip clause, skipping the given number of records. * - * @param number How many records to skip - * @return A step that only allows the limit of records to be specified + * @param number How many records to skip. If this is null, then no records are skipped. + * @return A step that only allows the limit of records to be specified. */ - ExposesLimit skip(Number number); + ExposesLimit skip(@Nullable Number number); } @@ -250,10 +251,10 @@ public interface StatementBuilder { /** * Limits the number of returned records. - * @param number How many records to return - * @return A buildable match statement + * @param number How many records to return. If this is null, all the records are returned. + * @return A buildable match statement. */ - BuildableMatch limit(Number number); + BuildableMatch limit(@Nullable Number number); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 8e3a74f7a..2efbbc50a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -79,6 +79,11 @@ final class CypherQueryCreator extends AbstractQueryCreator { private Iterator formalParameters; private final Queue lastParameter = new LinkedList<>(); + /** + * Stores the number of max results, if the {@link PartTree tree} is limiting. + */ + private final Number maxResults; + /** * Sort items may already be needed for some parts, i.e. of type NEAR. */ @@ -93,6 +98,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { this.domainType = domainType; this.nodeDescription = this.mappingContext.getRequiredNodeDescription(this.domainType); this.formalParameters = formalParameters.iterator(); + this.maxResults = tree.isLimiting() ? tree.getMaxResults() : null; } @Override @@ -127,6 +133,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { sort.stream().map(sortAdapterFor(nodeDescription)) ).toArray(SortItem[]::new) ) + .limit(maxResults) .build(); return CypherRenderer.create().render(statement); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 425d10782..8e84f68ce 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -105,6 +105,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { mappingContext, domainType, tree, formalParameters, actualParameters ); + String cypherQuery = queryCreator.createQuery(); Map boundedParameters = formalParameters .getBindableParameters().stream() diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 9c6de7f23..6df72815f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -241,6 +241,15 @@ public class CypherTest { "MATCH (u:`User`) RETURN u SKIP 1"); } + @Test + void nullSkip() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + @Test void limit() { Statement statement = Cypher.match(userNode).returning(userNode).limit(1).build(); @@ -250,6 +259,15 @@ public class CypherTest { "MATCH (u:`User`) RETURN u LIMIT 1"); } + @Test + void nullLimit() { + Statement statement = Cypher.match(userNode).returning(userNode).limit(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + @Test void skipAndLimit() { Statement statement = Cypher.match(userNode).returning(userNode).skip(1).limit(1).build(); @@ -259,6 +277,16 @@ public class CypherTest { "MATCH (u:`User`) RETURN u SKIP 1 LIMIT 1"); } + @Test + void nullskipAndLimit() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(null).limit(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + + @Test void distinct() { Statement statement = Cypher.match(userNode).returningDistinct(userNode).skip(1).limit(1).build(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 000155533..5acf5a402 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.IntStream; import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.BeforeEach; @@ -125,6 +126,10 @@ class RepositoryIT { transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.run("CREATE (n:KotlinPerson) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})"); + + IntStream.rangeClosed(1, 20).forEach(i -> + transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", Values.parameters("i", String.format("%02d", i)))); + transaction.success(); transaction.close(); @@ -784,13 +789,30 @@ class RepositoryIT { .containsExactly(person1); } + @Test + void limitClauseShouldWork() { + + List things; + + things = thingRepository.findTop5ByOrderByNameDesc(); + assertThat(things) + .hasSize(5) + .extracting(ThingWithAssignedId::getName) + .containsExactlyInAnyOrder("name20", "name19", "name18", "name17", "name16"); + + things = thingRepository.findFirstByOrderByNameDesc(); + assertThat(things) + .extracting(ThingWithAssignedId::getName) + .containsExactlyInAnyOrder("name20"); + } + @Configuration @EnableNeo4jRepositories @EnableTransactionManagement static class Config { @Bean - public Driver driver() { + public static Driver driver() { return neo4jConnectionSupport.openConnection(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java index 76496dec0..57343e84b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java @@ -18,10 +18,15 @@ */ package org.springframework.data.neo4j.integration; +import java.util.List; + import org.springframework.data.repository.CrudRepository; /** * @author Michael J. Simons */ public interface ThingRepository extends CrudRepository { + List findFirstByOrderByNameDesc(); + + List findTop5ByOrderByNameDesc(); } From 8018d5d1334e410aba5ba3b2a0f8a7aa4c253ec3 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 20 May 2019 23:01:59 +0200 Subject: [PATCH 091/342] Add support for parameters in String based queries. --- .../repository/query/AbstractNeo4jQuery.java | 47 ++++ .../query/Neo4jQueryLookupStrategy.java | 22 +- .../repository/query/Neo4jQueryMethod.java | 24 ++ .../repository/query/PartTreeNeo4jQuery.java | 176 ++++++------ .../query/StringBasedNeo4jQuery.java | 172 ++++++++++-- .../neo4j/integration/PersonRepository.java | 8 + .../data/neo4j/integration/RepositoryIT.java | 14 + .../query/Neo4jQueryLookupStrategyTest.java | 83 ------ .../repository/query/RepositoryQueryTest.java | 253 ++++++++++++++++++ 9 files changed, 578 insertions(+), 221 deletions(-) delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategyTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java index a38f6e9a3..be27b423b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -18,6 +18,13 @@ */ package org.springframework.data.neo4j.repository.query; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.domain.Range; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -86,4 +93,44 @@ abstract class AbstractNeo4jQuery implements RepositoryQuery { * @return True if the query has an explicit limit set. */ protected abstract boolean isLimiting(); + + /** + * Converts parameter as needed by the query generated, which is not covered by standard conversion services. + * + * @param parameter The parameter to fit into the generated query. + * @return A parameter that fits the place holders of a generated query + */ + final Object convertParameter(Object parameter) { + if (parameter instanceof Range) { + Range range = (Range) parameter; + Map map = new HashMap<>(); + range.getLowerBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("lb", v)); + range.getUpperBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("ub", v)); + return map; + } else if (parameter instanceof Distance) { + return calculateDistanceInMeter((Distance) parameter); + } else if (parameter instanceof Circle) { + Circle circle = (Circle) parameter; + Map map = new HashMap<>(); + map.put("x", convertParameter(circle.getCenter().getX())); + map.put("y", convertParameter(circle.getCenter().getY())); + map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); + return map; + } + + // Good hook to check the NodeManager whether the thing is an entity and we replace the value with a known id. + + return parameter; + } + + private static double calculateDistanceInMeter(Distance distance) { + + if (distance.getMetric() == Metrics.KILOMETERS) { + return distance.getValue() / 0.001d; + } else if (distance.getMetric() == Metrics.MILES) { + return distance.getValue() / 0.00062137d; + } else { + return distance.getValue(); + } + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java index e1da8cf3c..079ced55d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java @@ -21,11 +21,8 @@ package org.springframework.data.neo4j.repository.query; import lombok.RequiredArgsConstructor; import java.lang.reflect.Method; -import java.util.Optional; import org.apiguardian.api.API; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.projection.ProjectionFactory; @@ -59,24 +56,11 @@ public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { Neo4jQueryMethod queryMethod = new Neo4jQueryMethod(method, metadata, factory); - Optional optionalQueryAnnotation = getQueryAnnotationOf(method); - if (optionalQueryAnnotation.isPresent()) { - return new StringBasedNeo4jQuery(nodeManager, mappingContext, queryMethod, - getCypherQuery(optionalQueryAnnotation), optionalQueryAnnotation); + if (queryMethod.hasQueryAnnotation()) { + return StringBasedNeo4jQuery + .create(nodeManager, mappingContext, evaluationContextProvider, queryMethod); } return new PartTreeNeo4jQuery(nodeManager, mappingContext, queryMethod); } - - /** - * @return the {@link Query} annotation that is applied to the method or an empty {@link Optional} if none available. - */ - static Optional getQueryAnnotationOf(Method method) { - return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)); - } - - static String getCypherQuery(Optional optionalQueryAnnotation) { - return optionalQueryAnnotation.map(Query::value).filter(s -> !s.isEmpty()) - .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not.")); - } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java index c038a6a46..d333dff47 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java @@ -22,13 +22,16 @@ import static java.lang.String.*; import java.lang.reflect.Method; import java.util.List; +import java.util.Optional; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.Nullable; /** * Neo4j specific implementation of {@link QueryMethod}. It contains a custom implementation of {@link Parameter} which @@ -40,6 +43,11 @@ import org.springframework.data.repository.query.QueryMethod; */ final class Neo4jQueryMethod extends QueryMethod { + /** + * Optional query annotation of the method. + */ + private @Nullable final Query queryAnnotation; + /** * Creates a new {@link Neo4jQueryMethod} from the given parameters. Looks up the correct query to use for following * invocations of the method given. @@ -50,6 +58,22 @@ final class Neo4jQueryMethod extends QueryMethod { */ Neo4jQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { super(method, metadata, factory); + + this.queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); + } + + /** + * @return True if the underlying method has been annotated with {@code @Query}. + */ + boolean hasQueryAnnotation() { + return this.queryAnnotation != null; + } + + /** + * @return the {@link Query} annotation that is applied to the method or an empty {@link Optional} if none available. + */ + Optional getQueryAnnotation() { + return Optional.ofNullable(this.queryAnnotation); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 8e84f68ce..692600748 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -30,16 +30,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.neo4j.driver.types.Point; -import org.springframework.data.domain.Range; -import org.springframework.data.geo.Circle; -import org.springframework.data.geo.Distance; -import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -64,7 +59,6 @@ import org.springframework.util.Assert; @Slf4j final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { - // /** * A set of the temporal types that are directly passable to the driver and support a meaningful comparision in a * temporal sense (after, before). @@ -93,7 +87,8 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { this.tree = new PartTree(queryMethod.getName(), domainType); // Validate parts. Sort properties will be validated by Spring Data already. - this.tree.flatMap(OrPart::stream).forEach(part -> validatePart(part)); + PartValidator validator = new PartValidator(queryMethod); + this.tree.flatMap(OrPart::stream).forEach(validator::validatePart); } @Override @@ -118,105 +113,17 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { .build(); } - void validatePart(Part part) { - - validateIgnoreCase(part); - switch (part.getType()) { - case AFTER: - case BEFORE: - validateTemporalProperty(part); - break; - case IS_EMPTY: - case IS_NOT_EMPTY: - validateCollectionProperty(part); - break; - case NEAR: - case WITHIN: - validatePointProperty(part); - break; - } - } - + /** + * Checks whether the given part can be queried without case sensitivity. + * + * @param part + * @return True when {@code part} can be queried case insensitive. + */ static boolean canIgnoreCase(Part part) { return part.getProperty().getLeafType() == String.class && TYPES_SUPPORTING_CASE_INSENSITIVITY .contains(part.getType()); } - private String formatTypes(Collection types) { - return types.stream().flatMap(t -> t.getKeywords().stream()).collect(joining(", ", "[", "]")); - } - - void validateIgnoreCase(Part part) { - - Assert.state(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), - () -> String.format( - "Can not derive query for '%s': Only the case of String based properties can be ignored within the following keywords: %s", - super.queryMethod, - formatTypes(TYPES_SUPPORTING_CASE_INSENSITIVITY))); - } - - void validateTemporalProperty(Part part) { - - Assert.state(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String - .format( - "Can not derive query for '%s': The keywords %s work only with properties with one of the following types: %s", - super.queryMethod, formatTypes(Collections.singletonList(part.getType())), - COMPARABLE_TEMPORAL_TYPES)); - } - - void validateCollectionProperty(Part part) { - Assert.state(part.getProperty().getLeafProperty().isCollection(), () -> String - .format("Can not derive query for '%s': The keywords %s work only with collection properties", - super.queryMethod, - formatTypes(Collections.singletonList(part.getType())))); - } - - void validatePointProperty(Part part) { - - Assert.state(ClassTypeInformation.from(Point.class) - .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), () -> String - .format("Can not derive query for '%s': %s works only with spatial properties", super.queryMethod, - part.getType())); - } - - /** - * Converts parameter as needed by the query generated, which is not covered by standard conversion services - * - * @param parameter The parameter to fit into the generated query. - * @return A parameter that fits the place holders of a generated query - */ - static Object convertParameter(Object parameter) { - if (parameter instanceof Range) { - Range range = (Range) parameter; - Map map = new HashMap<>(); - range.getLowerBound().getValue().map(PartTreeNeo4jQuery::convertParameter).ifPresent(v -> map.put("lb", v)); - range.getUpperBound().getValue().map(PartTreeNeo4jQuery::convertParameter).ifPresent(v -> map.put("ub", v)); - return map; - } else if (parameter instanceof Distance) { - return calculateDistanceInMeter((Distance) parameter); - } else if (parameter instanceof Circle) { - Circle circle = (Circle) parameter; - Map map = new HashMap<>(); - map.put("x", convertParameter(circle.getCenter().getX())); - map.put("y", convertParameter(circle.getCenter().getY())); - map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); - return map; - } - return parameter; - } - - private static double calculateDistanceInMeter(Distance distance) { - - if (distance.getMetric() == Metrics.KILOMETERS) { - return distance.getValue() / 0.001d; - } else if (distance.getMetric() == Metrics.MILES) { - return distance.getValue() / 0.00062137d; - } else { - return distance.getValue(); - } - } - - @Override protected boolean isCountQuery() { return tree.isCountProjection(); @@ -236,4 +143,71 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { protected boolean isLimiting() { return tree.isLimiting(); } + + static class PartValidator { + + private final Neo4jQueryMethod queryMethod; + + PartValidator(Neo4jQueryMethod queryMethod) { + this.queryMethod = queryMethod; + } + + void validatePart(Part part) { + + validateIgnoreCase(part); + switch (part.getType()) { + case AFTER: + case BEFORE: + validateTemporalProperty(part); + break; + case IS_EMPTY: + case IS_NOT_EMPTY: + validateCollectionProperty(part); + break; + case NEAR: + case WITHIN: + validatePointProperty(part); + break; + } + } + + private void validateIgnoreCase(Part part) { + + Assert.state(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), + () -> String.format( + "Can not derive query for '%s': Only the case of String based properties can be ignored within the following keywords: %s", + queryMethod, + formatTypes(TYPES_SUPPORTING_CASE_INSENSITIVITY))); + } + + private void validateTemporalProperty(Part part) { + + Assert.state(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String + .format( + "Can not derive query for '%s': The keywords %s work only with properties with one of the following types: %s", + queryMethod, formatTypes(Collections.singletonList(part.getType())), + COMPARABLE_TEMPORAL_TYPES)); + } + + private void validateCollectionProperty(Part part) { + + Assert.state(part.getProperty().getLeafProperty().isCollection(), () -> String + .format("Can not derive query for '%s': The keywords %s work only with collection properties", + queryMethod, + formatTypes(Collections.singletonList(part.getType())))); + } + + private void validatePointProperty(Part part) { + + Assert.state(ClassTypeInformation.from(Point.class) + .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), () -> String + .format("Can not derive query for '%s': %s works only with spatial properties", queryMethod, + part.getType())); + } + + private static String formatTypes(Collection types) { + return types.stream().flatMap(t -> t.getKeywords().stream()).collect(joining(", ", "[", "]")); + } + + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index d81875be1..010a09d30 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -18,16 +18,36 @@ */ package org.springframework.data.neo4j.repository.query; -import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; +import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.SpelEvaluator; +import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** - * Implementation of {@link RepositoryQuery} for String based custom Cypher query. + * Implementation of {@link RepositoryQuery} for query methods annotated with {@link Query @Query}. + * + * + * The flow to handle queries with SpEL parameters is as follows + *
    + *
  1. Parse template as something that has SpEL-expressions in it
  2. + *
  3. Replace the SpEL-expressions with Neo4j Statement template parameters
  4. + *
  5. The parameters passed here _and_ the values that might have been computed during SpEL-parsing
  6. + *
+ * The main ingredient is a SpelEvaluator, that parses a template and replaces SpEL expressions + * with real Neo4j parameters. * * @author Gerrit Meier * @author Michael J. Simons @@ -35,30 +55,105 @@ import org.springframework.data.repository.query.RepositoryQuery; */ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { - private final String cypherQuery; + /** + * Used for extracting SpEL expressions inside Cypher query templates. + */ + static final SpelQueryContext SPEL_QUERY_CONTEXT = SpelQueryContext + .of(StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource); + /** + * Is this a count projection? + */ private final boolean countQuery; + + /** + * Is this an exists projection? + */ private final boolean existsQuery; + + /** + * Is this a modifying delete query? + */ private final boolean deleteQuery; - StringBasedNeo4jQuery(NodeManager nodeManager, Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod, - String cypherQuery, - Optional optionalQueryAnnotation) { + /** + * Used to evaluate the expression found while parsing the cypher template of this query against the actual parameters + * with the help of the formal parameters during the building of the {@link PreparedQuery}. + */ + private final SpelEvaluator spelEvaluator; + + /** + * The Cypher string used for this query. The cypher query will not be changed after parsed via {@link #SPEL_QUERY_CONTEXT}. + * All SpEL expressions will be substituted via "native" parameter placeholders. This will be done via the {@link #spelEvaluator}. + */ + private final String cypherQuery; + + /** + * Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The annotation + * is expected to have a value. + * + * @param nodeManager + * @param mappingContext + * @param evaluationContextProvider + * @param queryMethod + * @return A new instance of a String based Neo4j query. + */ + static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext, + QueryMethodEvaluationContextProvider evaluationContextProvider, + Neo4jQueryMethod queryMethod) { + + Query queryAnnotation = queryMethod.getQueryAnnotation() + .orElseThrow(() -> new MappingException("Expected @Query annotation on the query method!")); + + String cypherTemplate = Optional.ofNullable(queryAnnotation.value()) + .filter(StringUtils::hasText) + .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not.")); + + return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod, + cypherTemplate, queryAnnotation.count(), queryAnnotation.exists(), queryAnnotation.delete()); + } + + /** + * Create a {@link StringBasedNeo4jQuery} based on an explicit Cypher template. + * + * @param nodeManager + * @param mappingContext + * @param evaluationContextProvider + * @param queryMethod + * @param cypherTemplate The template to use. + * @return A new instance of a String based Neo4j query. + */ + static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext, + QueryMethodEvaluationContextProvider evaluationContextProvider, + Neo4jQueryMethod queryMethod, String cypherTemplate) { + + Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template."); + + return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod, + cypherTemplate, false, false, false); + } + + private StringBasedNeo4jQuery(NodeManager nodeManager, + Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider, + Neo4jQueryMethod queryMethod, String cypherTemplate, boolean countQuery, + boolean existsQuery, boolean deleteQuery) { super(nodeManager, mappingContext, queryMethod); - this.cypherQuery = cypherQuery; + this.countQuery = countQuery; + this.existsQuery = existsQuery; + this.deleteQuery = deleteQuery; - if (optionalQueryAnnotation.isPresent()) { - Query queryAnnotation = optionalQueryAnnotation.get(); - countQuery = queryAnnotation.count(); - existsQuery = queryAnnotation.exists(); - deleteQuery = queryAnnotation.delete(); - } else { - countQuery = false; - existsQuery = false; - deleteQuery = false; - } + SpelExtractor spelExtractor = SPEL_QUERY_CONTEXT.parse(cypherTemplate); + this.spelEvaluator = new SpelEvaluator(evaluationContextProvider, queryMethod.getParameters(), spelExtractor); + this.cypherQuery = spelExtractor.getQueryString(); + } + + static String getQueryTemplate(Query queryAnnotation) { + + return Optional.ofNullable(queryAnnotation.value()) + .filter(StringUtils::hasText) + .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not.")); } @Override @@ -66,7 +161,7 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { return PreparedQuery.queryFor(super.domainType) .withCypherQuery(cypherQuery) - .withParameters(Collections.emptyMap()) // TODO Map parameters. + .withParameters(bindParameters(parameters)) .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) // Null is fine .build(); } @@ -90,4 +185,45 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { protected boolean isLimiting() { return false; } + + Map bindParameters(Object[] actualParameters) { + + final Parameters formalParameters = queryMethod.getParameters(); + + Map resolvedParameters = new HashMap<>(spelEvaluator.evaluate(actualParameters)); + formalParameters.stream() + .filter(Parameter::isBindable) + .forEach(parameter -> { + + int parameterIndex = parameter.getIndex(); + Object parameterValue = super.convertParameter(actualParameters[parameterIndex]); + + // Add the parameter under it's name when possible + parameter.getName() + .ifPresent(parameterName -> resolvedParameters.put(parameterName, parameterValue)); + // Always add under its index. + resolvedParameters.put(Integer.toString(parameterIndex), parameterValue); + }); + + return resolvedParameters; + } + + /** + * @param index + * @param originalSpelExpression Not used for configuring parameter names atm. + * @return + */ + private static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { + return "__SpEL__" + index; + } + + /** + * @param originalPrefix The prefix passed to the replacement source is either ':' or '?', so that isn't usable for + * Cypher templates and therefore ignored. + * @param parameterName + * @return + */ + private static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { + return "$" + parameterName; + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 9fc1d58fd..2e7c46c2c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -28,6 +28,7 @@ import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; /** @@ -49,6 +50,13 @@ public interface PersonRepository extends Neo4jRepository getOptionalPersonViaQuery(); + @Query("MATCH (n:PersonWithAllConstructor{name:$name}) return n") + Optional getOptionalPersonViaQuery(@Param("name") String name); + + @Query("MATCH (n:PersonWithAllConstructor{name::#{#part1 + #part2}}) return n") + Optional getOptionalPersonViaQuery(@Param("part1") String part1, + @Param("part2") String part2); + @Query("MATCH (n:PersonWithNoConstructor) return n") List getAllPersonsWithNoConstructorViaQuery(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 5acf5a402..c0b420a13 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -331,6 +331,20 @@ class RepositoryIT { assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } + @Test + void loadOptionalPersonWithAllConstructorWithParameter() { + Optional person = repository.getOptionalPersonViaQuery(TEST_PERSON1_NAME); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); + } + + @Test + void loadOptionalPersonWithAllConstructorWithSpelParameters() { + Optional person = repository.getOptionalPersonViaQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); + } + @Test void loadAllPersonsWithNoConstructor() { List persons = repository.getAllPersonsWithNoConstructorViaQuery(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategyTest.java deleted file mode 100644 index c272ad706..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategyTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.query; - -import static org.assertj.core.api.Assertions.*; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.springframework.data.mapping.MappingException; -import org.springframework.data.repository.Repository; - -/** - * @author Gerrit Meier - * @author Michael J. Simons - */ -class Neo4jQueryLookupStrategyTest { - - private static final String CUSTOM_CYPHER_QUERY = "MATCH (n) return n"; - - @Test - void shouldFindAnnotatedQuery() throws Exception { - - Method method = queryMethod("annotatedQuery"); - Optional optionalQueryAnnotation = Neo4jQueryLookupStrategy.getQueryAnnotationOf(method); - assertThat(Neo4jQueryLookupStrategy.getCypherQuery(optionalQueryAnnotation)).isEqualTo(CUSTOM_CYPHER_QUERY); - } - - @Test - void shouldDetectInvalidAnnotation() throws Exception { - - Method method = queryMethod("invalidAnnotatedQuery"); - Optional optionalQueryAnnotation = Neo4jQueryLookupStrategy.getQueryAnnotationOf(method); - assertThatExceptionOfType(MappingException.class) - .isThrownBy(() -> Neo4jQueryLookupStrategy.getCypherQuery(optionalQueryAnnotation)) - .withMessage("Expected @Query annotation to have a value, but it did not."); - } - - @Test - void findQueryAnnotation() throws Exception { - - Method method = queryMethod("annotatedQuery"); - Optional optionalQueryAnnotation = Neo4jQueryLookupStrategy.getQueryAnnotationOf(method); - assertThat(optionalQueryAnnotation).isPresent(); - } - - private Method queryMethod(String name, Class... parameters) throws Exception { - Class repositoryClass = PersonRepository.class; - - return repositoryClass.getMethod(name, parameters); - } - - interface PersonRepository extends Repository { - - @Query(CUSTOM_CYPHER_QUERY) - List annotatedQuery(); - - @Query - List invalidAnnotatedQuery(); - } - - class Person { - } - -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java new file mode 100644 index 000000000..be8c885ef --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.driver.Values; +import org.neo4j.driver.types.Point; +import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.Param; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.util.ReflectionUtils; + +/** + * Unit tests for + *
    + *
  • {@link Neo4jQueryLookupStrategy}
  • + *
  • {@link Neo4jQueryMethod}
  • + *
  • {@link StringBasedNeo4jQuery}
  • + *
+ * + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +final class RepositoryQueryTest { + + private static final String CUSTOM_CYPHER_QUERY = "MATCH (n) return n"; + + private static final RepositoryMetadata TEST_REPOSITORY_METADATA = new DefaultRepositoryMetadata( + TestRepository.class); + + private static final ProjectionFactory PROJECTION_FACTORY = new SpelAwareProxyProjectionFactory(); + + @Mock + NodeManager nodeManager; + + @Mock + Neo4jMappingContext schema; + + @Mock + NamedQueries namedQueries; + + @Nested + class Neo4jQueryMethodTest { + + @Test + void findQueryAnnotation() { + + Neo4jQueryMethod neo4jQueryMethod = neo4jQueryMethod("annotatedQueryWithValidTemplate"); + + Optional optionalQueryAnnotation = neo4jQueryMethod.getQueryAnnotation(); + assertThat(optionalQueryAnnotation).isPresent(); + } + } + + @Nested + class Neo4jQueryLookupStrategyTest { + + @Test + void shouldSelectPartTreeNeo4jQuery() { + + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock( + Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT); + + RepositoryQuery query = lookupStrategy + .resolveQuery(queryMethod("findById", Object.class), TEST_REPOSITORY_METADATA, PROJECTION_FACTORY, + namedQueries); + assertThat(query).isInstanceOf(PartTreeNeo4jQuery.class); + } + + @Test + void shouldSelectStringBasedNeo4jQuery() { + + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock( + Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT); + + RepositoryQuery query = lookupStrategy + .resolveQuery(queryMethod("annotatedQueryWithValidTemplate"), TEST_REPOSITORY_METADATA, + PROJECTION_FACTORY, namedQueries); + assertThat(query).isInstanceOf(StringBasedNeo4jQuery.class); + } + } + + @Nested + class StringBasedNeo4jQueryTest { + + @Test + void spelQueryContextShouldBeConfiguredCorrectly() { + + SpelQueryContext spelQueryContext = StringBasedNeo4jQuery.SPEL_QUERY_CONTEXT; + + String template; + String query; + SpelQueryContext.SpelExtractor spelExtractor; + + template = "MATCH (user:User) WHERE user.name = :#{#searchUser.name} and user.middleName = ?#{#searchUser.middleName} RETURN user"; + + spelExtractor = spelQueryContext.parse(template); + query = spelExtractor.getQueryString(); + + assertThat(query) + .isEqualTo( + "MATCH (user:User) WHERE user.name = $__SpEL__0 and user.middleName = $__SpEL__1 RETURN user"); + + template = "MATCH (user:User) WHERE user.name=?#{[0]} and user.name=:#{[0]} RETURN user"; + spelExtractor = spelQueryContext.parse(template); + query = spelExtractor.getQueryString(); + + assertThat(query) + .isEqualTo("MATCH (user:User) WHERE user.name=$__SpEL__0 and user.name=$__SpEL__1 RETURN user"); + } + + @Test + void shouldExtractQueryTemplate() { + + Neo4jQueryMethod method = neo4jQueryMethod("annotatedQueryWithValidTemplate"); + + assertThat(StringBasedNeo4jQuery.getQueryTemplate(method.getQueryAnnotation().get())) + .isEqualTo(CUSTOM_CYPHER_QUERY); + } + + @Test + void shouldDetectInvalidAnnotation() { + + Neo4jQueryMethod method = neo4jQueryMethod("annotatedQueryWithoutTemplate"); + + assertThatExceptionOfType(MappingException.class) + .isThrownBy( + () -> StringBasedNeo4jQuery.create(mock(NodeManager.class), mock(Neo4jMappingContext.class), + QueryMethodEvaluationContextProvider.DEFAULT, method)) + .withMessage("Expected @Query annotation to have a value, but it did not."); + } + + @Test + void shouldBindParameters() { + + Neo4jQueryMethod method = RepositoryQueryTest + .neo4jQueryMethod("annotatedQueryWithValidTemplate", String.class, String.class); + + StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(NodeManager.class), + mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT, + method); + + Map resolveParameters = repositoryQuery + .bindParameters(new Object[] { "A String", "Another String" }); + + assertThat(resolveParameters) + .containsEntry("0", "A String") + .containsEntry("1", "Another String"); + } + + @Test + void shouldResolveNamedParameters() { + + Neo4jQueryMethod method = RepositoryQueryTest + .neo4jQueryMethod("findByDontDoThisInRealLiveNamed", org.neo4j.driver.types.Point.class, String.class, + String.class); + + StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(NodeManager.class), + mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT, + method); + + Point thePoint = Values.point(4223, 1, 2).asPoint(); + Map resolveParameters = repositoryQuery.bindParameters( + new Object[] { thePoint, "TheName", "TheFirstName" }); + + assertThat(resolveParameters) + .hasSize(8) + .containsEntry("0", thePoint) + .containsEntry("location", thePoint) + .containsEntry("1", "TheName") + .containsEntry("name", "TheName") + .containsEntry("2", "TheFirstName") + .containsEntry("firstName", "TheFirstName") + .containsEntry("__SpEL__0", "TheFirstName") + .containsEntry("__SpEL__1", "TheNameTheFirstName"); + } + } + + static Method queryMethod(String name, Class... parameters) { + + return ReflectionUtils.findMethod(TestRepository.class, name, parameters); + } + + static Neo4jQueryMethod neo4jQueryMethod(String name, Class... parameters) { + + return new Neo4jQueryMethod(ReflectionUtils.findMethod(TestRepository.class, name, parameters), + TEST_REPOSITORY_METADATA, PROJECTION_FACTORY); + } + + static class TestEntity { + @Id + private Long id; + } + + interface TestRepository extends CrudRepository { + + @Query("MATCH (n:Test) WHERE n.name = $name AND n.firstName = :#{#firstName} AND n.fullName = ?#{#name + #firstName} AND p.location = $location return n") + Optional findByDontDoThisInRealLiveNamed(@Param("location") org.neo4j.driver.types.Point location, + @Param("name") String name, + @Param("firstName") String aFirstName); + + @Query("MATCH (n:Test) WHERE n.name = $0 OR n.name = $1") + List annotatedQueryWithValidTemplate(String name, String anotherName); + + @Query(CUSTOM_CYPHER_QUERY) + List annotatedQueryWithValidTemplate(); + + @Query + List annotatedQueryWithoutTemplate(); + } + + private RepositoryQueryTest() { + } +} From ab8a187607642d94da44e8c7b510396b5dfa2c04 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 21 May 2019 11:39:57 +0200 Subject: [PATCH 092/342] Polishing - extract methods - remove unused code --- .../repository/query/AbstractNeo4jQuery.java | 36 ++++++++++++------- .../repository/query/PartTreeNeo4jQuery.java | 3 -- .../query/StringBasedNeo4jQuery.java | 23 +----------- 3 files changed, 24 insertions(+), 38 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java index be27b423b..b058db391 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -102,20 +102,11 @@ abstract class AbstractNeo4jQuery implements RepositoryQuery { */ final Object convertParameter(Object parameter) { if (parameter instanceof Range) { - Range range = (Range) parameter; - Map map = new HashMap<>(); - range.getLowerBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("lb", v)); - range.getUpperBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("ub", v)); - return map; + return convertRange((Range) parameter); } else if (parameter instanceof Distance) { return calculateDistanceInMeter((Distance) parameter); } else if (parameter instanceof Circle) { - Circle circle = (Circle) parameter; - Map map = new HashMap<>(); - map.put("x", convertParameter(circle.getCenter().getX())); - map.put("y", convertParameter(circle.getCenter().getY())); - map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); - return map; + return convertCircle((Circle) parameter); } // Good hook to check the NodeManager whether the thing is an entity and we replace the value with a known id. @@ -123,12 +114,31 @@ abstract class AbstractNeo4jQuery implements RepositoryQuery { return parameter; } + private Map convertRange(Range range) { + Map map = new HashMap<>(); + range.getLowerBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("lb", v)); + range.getUpperBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("ub", v)); + return map; + } + + private Map convertCircle(Circle circle) { + Map map = new HashMap<>(); + map.put("x", convertParameter(circle.getCenter().getX())); + map.put("y", convertParameter(circle.getCenter().getY())); + map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); + return map; + } + private static double calculateDistanceInMeter(Distance distance) { if (distance.getMetric() == Metrics.KILOMETERS) { - return distance.getValue() / 0.001d; + double kilometersDivisor = 0.001d; + return distance.getValue() / kilometersDivisor; + } else if (distance.getMetric() == Metrics.MILES) { - return distance.getValue() / 0.00062137d; + double milesDivisor = 0.00062137d; + return distance.getValue() / milesDivisor; + } else { return distance.getValue(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 692600748..9c0565418 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -42,7 +42,6 @@ import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jPar import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree.OrPart; @@ -73,7 +72,6 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { Part.Type.NOT_CONTAINING, Part.Type.NOT_LIKE, Part.Type.SIMPLE_PROPERTY, Part.Type.STARTING_WITH); - private final ResultProcessor processor; private final PartTree tree; PartTreeNeo4jQuery( @@ -83,7 +81,6 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { ) { super(nodeManager, mappingContext, queryMethod); - this.processor = queryMethod.getResultProcessor(); this.tree = new PartTree(queryMethod.getName(), domainType); // Validate parts. Sort properties will be validated by Spring Data already. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index 010a09d30..3d3665a9a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -33,7 +33,6 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.SpelEvaluator; import org.springframework.data.repository.query.SpelQueryContext; import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -113,26 +112,6 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { cypherTemplate, queryAnnotation.count(), queryAnnotation.exists(), queryAnnotation.delete()); } - /** - * Create a {@link StringBasedNeo4jQuery} based on an explicit Cypher template. - * - * @param nodeManager - * @param mappingContext - * @param evaluationContextProvider - * @param queryMethod - * @param cypherTemplate The template to use. - * @return A new instance of a String based Neo4j query. - */ - static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext, - QueryMethodEvaluationContextProvider evaluationContextProvider, - Neo4jQueryMethod queryMethod, String cypherTemplate) { - - Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template."); - - return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod, - cypherTemplate, false, false, false); - } - private StringBasedNeo4jQuery(NodeManager nodeManager, Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod, String cypherTemplate, boolean countQuery, @@ -198,7 +177,7 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { int parameterIndex = parameter.getIndex(); Object parameterValue = super.convertParameter(actualParameters[parameterIndex]); - // Add the parameter under it's name when possible + // Add the parameter under its name when possible parameter.getName() .ifPresent(parameterName -> resolvedParameters.put(parameterName, parameterValue)); // Always add under its index. From 6c0c5e509e5f0107ab8998676d782fe7fa0883ca Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 21 May 2019 12:26:12 +0200 Subject: [PATCH 093/342] Add support for named queries. --- .../query/Neo4jQueryLookupStrategy.java | 13 +++++++----- .../query/StringBasedNeo4jQuery.java | 21 +++++++++++++++++++ .../neo4j/integration/PersonRepository.java | 3 +++ .../data/neo4j/integration/RepositoryIT.java | 7 +++++++ .../repository/query/RepositoryQueryTest.java | 18 ++++++++++++++++ .../META-INF/neo4j-named-queries.properties | 19 +++++++++++++++++ 6 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 spring-data-neo4j/src/test/resources/META-INF/neo4j-named-queries.properties diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java index 079ced55d..1f44ab431 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java @@ -55,12 +55,15 @@ public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { NamedQueries namedQueries) { Neo4jQueryMethod queryMethod = new Neo4jQueryMethod(method, metadata, factory); + String namedQueryName = queryMethod.getNamedQueryName(); - if (queryMethod.hasQueryAnnotation()) { - return StringBasedNeo4jQuery - .create(nodeManager, mappingContext, evaluationContextProvider, queryMethod); + if (namedQueries.hasQuery(namedQueryName)) { + return StringBasedNeo4jQuery.create(nodeManager, mappingContext, evaluationContextProvider, queryMethod, + namedQueries.getQuery(namedQueryName)); + } else if (queryMethod.hasQueryAnnotation()) { + return StringBasedNeo4jQuery.create(nodeManager, mappingContext, evaluationContextProvider, queryMethod); + } else { + return new PartTreeNeo4jQuery(nodeManager, mappingContext, queryMethod); } - - return new PartTreeNeo4jQuery(nodeManager, mappingContext, queryMethod); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index 3d3665a9a..2de60721b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -33,6 +33,7 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.SpelEvaluator; import org.springframework.data.repository.query.SpelQueryContext; import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -112,6 +113,26 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { cypherTemplate, queryAnnotation.count(), queryAnnotation.exists(), queryAnnotation.delete()); } + /** + * Create a {@link StringBasedNeo4jQuery} based on an explicit Cypher template. + * + * @param nodeManager + * @param mappingContext + * @param evaluationContextProvider + * @param queryMethod + * @param cypherTemplate The template to use. + * @return A new instance of a String based Neo4j query. + */ + static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext, + QueryMethodEvaluationContextProvider evaluationContextProvider, + Neo4jQueryMethod queryMethod, String cypherTemplate) { + + Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template."); + + return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod, + cypherTemplate, false, false, false); + } + private StringBasedNeo4jQuery(NodeManager nodeManager, Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod, String cypherTemplate, boolean countQuery, diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java index 2e7c46c2c..138cd9204 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java @@ -57,6 +57,9 @@ public interface PersonRepository extends Neo4jRepository getOptionalPersonViaQuery(@Param("part1") String part1, @Param("part2") String part2); + Optional getOptionalPersonViaNamedQuery(@Param("part1") String part1, + @Param("part2") String part2); + @Query("MATCH (n:PersonWithNoConstructor) return n") List getAllPersonsWithNoConstructorViaQuery(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index c0b420a13..32c2f56c6 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -345,6 +345,13 @@ class RepositoryIT { assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } + @Test + void loadOptionalPersonWithAllConstructorWithSpelParametersAndNamedQuery() { + Optional person = repository.getOptionalPersonViaNamedQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2)); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); + } + @Test void loadAllPersonsWithNoConstructor() { List persons = repository.getAllPersonsWithNoConstructorViaQuery(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java index be8c885ef..e9f8b628b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java @@ -117,6 +117,22 @@ final class RepositoryQueryTest { PROJECTION_FACTORY, namedQueries); assertThat(query).isInstanceOf(StringBasedNeo4jQuery.class); } + + @Test + void shouldSelectStringBasedNeo4jQueryForNamedQuery() { + + final String namedQueryName = "TestEntity.findAllByANamedQuery"; + when(namedQueries.hasQuery(namedQueryName)).thenReturn(true); + when(namedQueries.getQuery(namedQueryName)).thenReturn("MATCH (n) RETURN n"); + + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock( + Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT); + + RepositoryQuery query = lookupStrategy + .resolveQuery(queryMethod("findAllByANamedQuery"), TEST_REPOSITORY_METADATA, + PROJECTION_FACTORY, namedQueries); + assertThat(query).isInstanceOf(StringBasedNeo4jQuery.class); + } } @Nested @@ -246,6 +262,8 @@ final class RepositoryQueryTest { @Query List annotatedQueryWithoutTemplate(); + + List findAllByANamedQuery(); } private RepositoryQueryTest() { diff --git a/spring-data-neo4j/src/test/resources/META-INF/neo4j-named-queries.properties b/spring-data-neo4j/src/test/resources/META-INF/neo4j-named-queries.properties new file mode 100644 index 000000000..6ad8ce6dd --- /dev/null +++ b/spring-data-neo4j/src/test/resources/META-INF/neo4j-named-queries.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2019 "Neo4j," +# Neo4j Sweden AB [https://neo4j.com] +# +# This file is part of Neo4j. +# +# Licensed 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 +# +# https://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. +# +PersonWithAllConstructor.getOptionalPersonViaNamedQuery = MATCH (n:PersonWithAllConstructor{name::#{#part1 + #part2}}) return n From 808eb875076d2db793c95acf7f22a88b7e0fc0c8 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 21 May 2019 18:54:30 +0200 Subject: [PATCH 094/342] Drop the notion of the NodeManager. This removes the NodeManager and related infrastructure and introduces some configuration support. See more about the reasoning in ADR-004. Co-authored-by: Gerrit Meier --- README.adoc | 48 +---- etc/adr/adr-004.adoc | 22 ++ .../AbstractNeo4jConfig.java} | 26 ++- .../config/Neo4jConfigurationSupport.java | 146 +++++++++++++ .../data/neo4j/core/DefaultNodeManager.java | 136 ------------ .../data/neo4j/core/NodeManager.java | 66 ------ .../data/neo4j/core/NodeManagerFactory.java | 123 ----------- .../data/neo4j/core/PersistenceException.java | 68 ------ .../context/DefaultPersistenceContext.java | 115 ---------- .../core/context/PersistenceContext.java | 65 ------ .../data/neo4j/core/context/package-info.java | 9 - .../context/tracking/EntityChangeEvent.java | 39 ---- .../tracking/EntityComparisonStrategy.java | 170 --------------- .../tracking/EntityTrackingStrategy.java | 74 ------- .../core/mapping/Neo4jMappingContext.java | 7 - .../data/neo4j/core/schema/Schema.java | 11 +- .../transaction/Neo4jTransactionManager.java | 50 +---- .../transaction/Neo4jTransactionUtils.java | 39 +--- .../core/transaction/NodeManagerHolder.java | 47 ----- .../NodeManagerSynchronization.java | 42 ---- .../NoResultException.java | 17 +- .../config/EnableNeo4jRepositories.java | 11 +- ...Neo4jRepositoryConfigurationExtension.java | 77 +------ .../repository/query/AbstractNeo4jQuery.java | 14 +- .../repository/query/Neo4jQueryExecution.java | 12 +- .../query/Neo4jQueryLookupStrategy.java | 10 +- .../repository/query/PartTreeNeo4jQuery.java | 9 +- .../query/StringBasedNeo4jQuery.java | 20 +- .../support/DefaultExecutableQuery.java | 61 ++++++ .../support}/DefaultPreparedQuery.java | 6 +- .../support}/ExecutableQuery.java | 25 ++- .../support/Neo4jRepositoryFactory.java | 12 +- .../support/Neo4jRepositoryFactoryBean.java | 10 +- .../support/NodeManagerFactoryBean.java | 62 ------ .../support}/PreparedQuery.java | 11 +- .../support/SharedNodeManagerCreator.java | 127 ----------- .../support/SimpleNeo4jRepository.java | 55 +++-- .../DefaultPersistenceContextTest.java | 140 ------------ .../EntityComparisonStrategyTest.java | 199 ------------------ .../data/neo4j/integration/RepositoryIT.java | 21 +- .../repository/query/RepositoryQueryTest.java | 16 +- .../support/SimpleNeo4jRepositoryTest.java | 96 --------- 42 files changed, 406 insertions(+), 1908 deletions(-) create mode 100644 etc/adr/adr-004.adoc rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/{core/NonUniqueResultException.java => config/AbstractNeo4jConfig.java} (50%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PersistenceException.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityChangeEvent.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerSynchronization.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/{core => repository}/NoResultException.java (70%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/{core => repository/support}/DefaultPreparedQuery.java (92%) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/{core => repository/support}/ExecutableQuery.java (55%) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/{core => repository/support}/PreparedQuery.java (82%) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SharedNodeManagerCreator.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java diff --git a/README.adoc b/README.adoc index 9f21575ab..50e64fd18 100644 --- a/README.adoc +++ b/README.adoc @@ -49,14 +49,7 @@ Each property of a class that is not identified as a simple type by `org.springf NOTE: Context in this sections refers especially to dirty tracking and dealing with state of entities. -It is unlikely that we can do completely without context. -While it's easy enough to update on entity as is with the <>, it's unlikely that we can deal this way with collections of relationships. -Spring Data JDBC's approach is as follow -____ -If the aggregate root is not new, all referenced entities get deleted, the aggregate root gets updated, and all referenced entities get inserted again. -____ - -That sounds good in theory, but the Graph itself being more optimized for reads than ongoing large chunks of writes probably won't like that. +We decided against a context for tracking changes, much like Spring Data JDBC did. === Other principles @@ -182,7 +175,8 @@ It is only meant to be a basic for discussions. note "Implementation of Spring Data Commons SPI" as SDC_note package "org.springframework.data.neo4j" { package "core" { - interface Neo4jTemplate + interface Neo4jClient + interface ReactiveNeo4jClient package "schema" { package "internal" { note "Schema description" as schemaDescription @@ -194,8 +188,8 @@ package "core" { interface Neo4jPersistentEntity interface Neo4jPersistentProperty } - package "session" { - interface Neo4jSessionFactory + package "transaction" { + class Neo4jTransactionManager } package "convert" { note "conversion support" as conversionNote @@ -234,7 +228,6 @@ core-[hidden]--->repository |`Neo4jTemplate` and related classes. |core.schema |Annotations for marking classes as nodes to be saved as well as internal schema description. -|context |Infrastructure for dirty tracking etc. |core.mapping |Spring mapping information. @@ -291,10 +284,10 @@ The other execution paths are only drafts right now and marked with a `*`. From our previous experience and handling in other Spring Data stores this would branch off in two (technical three) directions: . `StringBasedNeo4jQuery` for custom Cypher queries that are provided with the `@Query` annotation. -. `*` `StringBasedNeo4jQuery` for named queries that are outsourced in property files. -. `*` `PartTreeNeo4jQuery` for derived finder methods. +. `StringBasedNeo4jQuery` for named queries that are outsourced in property files. +. `PartTreeNeo4jQuery` for derived finder methods. -All three of them will get a custom `Neo4jQueryMethod` besides `Neo4jOperations` and `QueryMethodEvaluationContextProvider` (not used yet) provided. +All three of them will get a custom `Neo4jQueryMethod` besides `Neo4jClient` and `QueryMethodEvaluationContextProvider` (not used yet) provided. This is a wrapper around the `java.lang.reflect.Method` passed into the `resolveQuery` method of the `Neo4jQueryLookupStrategy` to provide additional metadata. ==== `StringBasedNeo4jQuery` execution @@ -314,30 +307,7 @@ We considered several approaches of dirty tracking in SDN-RX: . Shallow copy of objects to get compared on save. _A full copy of the objects will occupy twice the memory._ -Picking the third option for now (_using some kind of event / listener_) would allow us to track single field / relationship changes per object. -An entity will get tracked after it gets initially saved into or loaded from the database. - -[NOTE] -.Architecture evaluation fragment (not current anymore but should get archived) -==== -As the technical solution we decided to use proxies to listen for the changes. -It will then be required for the user to work with the returned (proxy) object after calling `save` to set additional fields etc. -The event tracking and event triggering through the proxies should communicate via an API to have the option of using another tracking approach in the future. - -Moved back to evaluation: It is not possible to simply intercept arbitrary property changes in Java. -We would need some kind of byte code enhancement/maven plugin to achieve this. -==== - -The information about object changes will be based on an object comparison. -We will store the saved or loaded object state and compare it with the actual version that should get saved. -To keep the eventing idea up this will create an one-shot event containing multiple changed properties. -This will also be needed if we implement the support for property changes through the byte code enhancement, -because a change / domain interaction could affect more than one property at a time. -The events will then get used to construct the matching cypher for the next save interaction. - -==== Side-effect on "flush mode" -We can also track repository interactions like `save` and defer them depending on the flush mode. -This would make it possible to remove the need for calling `save` explicitly but be more JPA-ish. +We have settled with option 1 (See ADR-004), analogue to Spring Data JDBC. [[starter]] == Spring Boot Starter diff --git a/etc/adr/adr-004.adoc b/etc/adr/adr-004.adoc new file mode 100644 index 000000000..38bc47ee2 --- /dev/null +++ b/etc/adr/adr-004.adoc @@ -0,0 +1,22 @@ +== ADR 4: Drop the notion of the `NodeManager` + +=== Status + +accepted + +=== Context + +We introduced the `NodeManager` as a pendan to Hibernates `EntityManager` and with it, a concept of a persistence context, tracking changes. +This setup is required for updating only changed properties and also having implicit saves. + +=== Decision + +The previous versions of SDN and OGM all copied the concept of having a tracking of entities. +We decided against it this time to remove complexity. +We will update all properties each time a node is save, relying on the database to do this in an efficient way. + +Relationships will be updated via smart queries. + +=== Consequences + +The biggest impact will probably more network traffic with models having a huge number of properties on a single domain object. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NonUniqueResultException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java similarity index 50% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NonUniqueResultException.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java index 3bd6174d8..b9640bf56 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NonUniqueResultException.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java @@ -16,22 +16,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.springframework.data.neo4j.config; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSizeException; +import org.neo4j.driver.Driver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.Neo4jClient; /** - * Throw when a query doesn't return a unique result. + * Base class for imperative SDN-RX configuration using JavaConfig. + * This can be included in all scenarios in which Spring Boot is not an option. * * @author Michael J. Simons - * @soundtrack Deichkind - Niveau weshalb warum + * @author Gerrit Meier * @since 1.0 */ +@Configuration @API(status = API.Status.STABLE, since = "1.0") -public class NonUniqueResultException extends IllegalResultSizeException { +public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport { - public NonUniqueResultException(String query, long actualNumberOfResults) { - super(1L, actualNumberOfResults, query); + /** + * The driver used here should be the driver resulting from {@link #driver()}, which is the default. + * + * @param driver The driver to connect with. + * @return A imperative Neo4j client. + */ + @Bean + public Neo4jClient neo4jClient(Driver driver) { + return Neo4jClient.create(driver); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java new file mode 100644 index 000000000..716208906 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.config; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.apiguardian.api.API; +import org.neo4j.driver.Driver; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Internal support class for basic configuration. The support infrastructure here is basically all around finding out about + * which classes are to be mapped and which not. The driver is part of the configuration support, as Neo4js Java driver + * contains both imperative and reactive components. + * + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public abstract class Neo4jConfigurationSupport { + + /** + * The driver to be used for interacting with Neo4j. + * + * @return + */ + public abstract Driver driver(); + + /** + * Creates a {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} equipped with entity classes + * scanned from the mapping base package. + * + * @return + * @see #getMappingBasePackages() + */ + @Bean + public Neo4jMappingContext neo4jMappingContext() throws ClassNotFoundException { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + mappingContext.setInitialEntitySet(getInitialEntitySet()); + + return mappingContext; + } + + /** + * Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}. + * + * @param driver The driver to synchronize against + * @return A platform transaction manager + */ + @Bean + public PlatformTransactionManager transactionManager(Driver driver) { + + return new Neo4jTransactionManager(driver); + } + + /** + * Returns the base packages to scan for Neo4j mapped entities at startup. Will return the package name of the + * configuration class' (the concrete class, not this one here) by default. So if you have a + * {@code com.acme.AppConfig} extending {@link Neo4jConfigurationSupport} the base package will be considered + * {@code com.acme} unless the method is overridden to implement alternate behavior. + * + * @return the base packages to scan for mapped {@link Node} classes + * or an empty collection to not enable scanning for entities. + */ + protected final Collection getMappingBasePackages() { + + Package mappingBasePackage = getClass().getPackage(); + return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName()); + } + + /** + * Scans the mapping base package for classes annotated with {@link Node}. + * By default, it scans for entities in all packages returned by {@link #getMappingBasePackages()}. + * + * @return + * @throws ClassNotFoundException + * @see #getMappingBasePackages() + */ + protected final Set> getInitialEntitySet() throws ClassNotFoundException { + + Set> initialEntitySet = new HashSet>(); + + for (String basePackage : getMappingBasePackages()) { + initialEntitySet.addAll(scanForEntities(basePackage)); + } + + return initialEntitySet; + } + + /** + * Scans the given base package for entities, i.e. Neo4j specific types annotated with {@link Node}. + * + * @param basePackage must not be {@literal null}. + * @return + * @throws ClassNotFoundException + */ + protected final Set> scanForEntities(String basePackage) throws ClassNotFoundException { + + if (!StringUtils.hasText(basePackage)) { + return Collections.emptySet(); + } + + Set> initialEntitySet = new HashSet>(); + + ClassPathScanningCandidateComponentProvider componentProvider = + new ClassPathScanningCandidateComponentProvider(false); + componentProvider.addIncludeFilter(new AnnotationTypeFilter(Node.class)); + + ClassLoader classLoader = Neo4jConfigurationSupport.class.getClassLoader(); + for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) { + initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), classLoader)); + } + + return initialEntitySet; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java deleted file mode 100644 index 0792accaf..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNodeManager.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core; - -import static java.util.stream.Collectors.*; - -import lombok.RequiredArgsConstructor; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -import org.apiguardian.api.API; -import org.neo4j.driver.Transaction; -import org.neo4j.driver.exceptions.NoSuchRecordException; -import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; -import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; -import org.springframework.data.neo4j.core.context.DefaultPersistenceContext; -import org.springframework.data.neo4j.core.context.PersistenceContext; -import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.Schema; -import org.springframework.lang.Nullable; - -/** - * @author Michael J. Simons - * @author Gerrit Meier - * @since 1.0 - */ -@API(status = API.Status.INTERNAL, since = "1.0") -class DefaultNodeManager implements NodeManager { - - private final Schema schema; - - private final Neo4jClient neo4jClient; - - private final Transaction transaction; - - private final PersistenceContext persistenceContext; - - DefaultNodeManager(Schema schema, Neo4jClient neo4jClient, @Nullable Transaction transaction) { - - this.schema = schema; - this.neo4jClient = neo4jClient; - this.transaction = transaction; - - this.persistenceContext = new DefaultPersistenceContext(); - } - - @Override - @Nullable - public Transaction getTransaction() { - return transaction; - } - - @Override - public ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery) { - - Class resultType = preparedQuery.getResultType(); - MappingSpec, Collection, T> mappingSpec = neo4jClient.newQuery(preparedQuery.getCypherQuery()) - .bindAll(preparedQuery.getParameters()) - .fetchAs(resultType); - RecordFetchSpec, Collection, T> fetchSpec = preparedQuery.getOptionalMappingFunction() - .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) - .orElse(mappingSpec); - - return new DefaultExecutableQuery(preparedQuery, schema.getNodeDescription(resultType), fetchSpec); - } - - @Override - public T save(T entityWithUnknownState) { - - // TODO if already registered, here or in the context? - this.persistenceContext - .register(entityWithUnknownState, schema.getRequiredNodeDescription(entityWithUnknownState.getClass())); - - throw new UnsupportedOperationException("Not there yet."); - } - - @Override - public void delete(Object managedEntity) { - - this.persistenceContext.deregister(managedEntity); - - throw new UnsupportedOperationException("Not there yet."); - } - - @RequiredArgsConstructor - class DefaultExecutableQuery implements ExecutableQuery { - - private final PreparedQuery preparedQuery; - private final Optional> optionalNodeDescription; - private final RecordFetchSpec, Collection, T> fetchSpec; - - @Override - public List getResults() { - return fetchSpec.all().stream().map(this::register).collect(toList()); - } - - @Override - public Optional getSingleResult() { - try { - return fetchSpec.one().map(this::register); - } catch (NoSuchRecordException e) { - return Optional.empty(); - } - } - - @Override - public T getRequiredSingleResult() { - return fetchSpec.one().map(this::register) - .orElseThrow(() -> new NoResultException(1L, preparedQuery.getCypherQuery())); - } - - private T register(T entity) { - this.optionalNodeDescription.ifPresent( - nodeDescription -> DefaultNodeManager.this.persistenceContext.register(entity, nodeDescription)); - return entity; - } - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java deleted file mode 100644 index 38c30e96b..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManager.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core; - -import org.apiguardian.api.API; -import org.neo4j.driver.Transaction; -import org.springframework.lang.Nullable; - -/** - * Entry point for creating queries that return managed Nodes. The node manager is not supposed to be kept around - * for longer than necessary. Try to keep your transactions short to avoid memory pressure due to keeping track of - * managed nodes. - * - * @author Michael J. Simons - */ -@API(status = API.Status.STABLE, since = "1.0") -public interface NodeManager { - - /** - * Clears all managed entities and flushes any open state to the underlying storage. - * TODO reflect if we really, really want to have this. - * If an user flushes the node manager, we will end up in a state where we do not have any information about - * the nodes / entities currently processed in the ongoing transaction. - */ - default void flush() { - } - - @Nullable - Transaction getTransaction(); - - ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery); - - /** - * Saves an entity. When the entity is not yet managed in this instance of the NodeManager, and will be registered as - * a managed instance. In either way, the state of the entity will be written to the underlying store afterwards. - * It is recommended to use the returned, managed instance of the object. This is especially true when dealing with - * immutable entities, where SDN RX has to return new instances to fill in generated keys and the like. - * - * @param entityWithUnknownState An entity that is either managed or unmanaged - * @return A managed object - */ - T save(T entityWithUnknownState); - - /** - * Delete an object from the persistence context and the underlying store. - * - * @param managedEntity Object to be removed - */ - void delete(Object managedEntity); -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java deleted file mode 100644 index 5fb402d6f..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeManagerFactory.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core; - -import lombok.extern.slf4j.Slf4j; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.apiguardian.api.API; -import org.neo4j.driver.Driver; -import org.springframework.data.neo4j.core.schema.Schema; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; -import org.springframework.lang.Nullable; - -/** - * Creates ready to use instances of {@link NodeManager}. - * - * @author Michael J. Simons - */ -@API(status = API.Status.STABLE, since = "1.0") -@Slf4j -public final class NodeManagerFactory { - - private final AtomicBoolean initialized = new AtomicBoolean(false); - - /** - * Driver that is used to create new sessions, either by directly invoking it or through Springs transactional utils. - */ - private final Driver driver; - - /** - * The initial set of classes that will be registered with the schema {@link #initialize()} to build the schema for node managers - * belonging to this factory. - */ - private final Set> initialPersistentClasses; - - @Nullable - private Schema schema; - - /** - * Creates a new instance of a factory producing {@link NodeManager node managers}. When used in a transactional setup, - * i.e. with the {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}, make sure to use - * the same {@link Driver driver instance} for both the node and the transaction manager. - *

- * Spring Boots autoconfiguration for SDN RX will make sure that the same driver is used for both concerns. - * - * @param driver The driver used to obtain statement runners from when creating instances of node managers. - * @param initialPersistentClasses The set of classes that should be initially scanned - */ - public NodeManagerFactory(Driver driver, Class... initialPersistentClasses) { - this(driver, Arrays.asList(initialPersistentClasses)); - } - - /** - * @see NodeManagerFactory#NodeManagerFactory(Driver, Class...) - */ - public NodeManagerFactory(Driver driver, Collection> initialPersistentClasses) { - this.driver = driver; - this.initialPersistentClasses = new HashSet<>(initialPersistentClasses); - } - - /** - * Creates a new node manager. The returned manager is supposed to have a short lifetime. When used in a Spring setup, - * this method should not called directly by client code. Instead the client code should use an injected instance of - * {@link NodeManager}, which will participate in Springs application based transaction or in JTA transactions. - * - * @return A new node manager - */ - public NodeManager createNodeManager() { - - if (!initialized.get()) { - throw new IllegalStateException( - "This factory has not been correctly initialized. Please provider a schema and register your persistent classes."); - } - // The call here to our Spring transaction shim has to be rethought in case we move this out of a Spring scope. - // I dropped all the methods to configure that in an effort to make the setup more simple. ^mjs - return new DefaultNodeManager(schema, Neo4jClient.create(driver), - Neo4jTransactionUtils.retrieveTransaction(driver, null).orElse(null)); - } - - /** - * Provides the schema for this node manager factory. The schemas has to be set before a node manager is retrieved from this factory. - * - * @param schema - */ - public void setSchema(@Nullable Schema schema) { - this.schema = schema; - } - - /** - * This initializes this factory and is usually called by Springs infrastructure and only useful as standalone call - * when the node manager factory is used without Spring. - */ - public void initialize() { - - if (this.initialized.compareAndSet(false, true)) { - Objects.requireNonNull(schema, "A schema is required. Did you provide one with #setSchema() beforehand?"); - log.info("Initializing schema with {} persistent classes", this.initialPersistentClasses.size()); - this.schema.register(this.initialPersistentClasses); - } - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PersistenceException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PersistenceException.java deleted file mode 100644 index 2a192b794..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PersistenceException.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core; - -import org.apiguardian.api.API; - -/** - * Shared base class for exceptions during persistence operations of a {@link NodeManager}. - * - * @author Michael J. Simons - * @soundtrack Deichkind - Niveau weshalb warum - * @since 1.0 - */ -@API(status = API.Status.STABLE, since = "1.0") -public abstract class PersistenceException extends RuntimeException { - - protected PersistenceException(String message) { - super(message); - } - - protected PersistenceException(String message, Throwable cause) { - super(message, cause); - } - - abstract static class IllegalResultSizeException extends PersistenceException { - - private final long expectedNumberOfResults; - private final long actualNumberOfResults; - private final String query; - - IllegalResultSizeException(long expectedNumberOfResults, long actualNumberOfResults, String query) { - - super(String.format("Expected %d results, got %d", expectedNumberOfResults, actualNumberOfResults)); - - this.query = query; - this.expectedNumberOfResults = expectedNumberOfResults; - this.actualNumberOfResults = actualNumberOfResults; - } - - public long getExpectedNumberOfResults() { - return expectedNumberOfResults; - } - - public long getActualNumberOfResults() { - return actualNumberOfResults; - } - - public String getQuery() { - return query; - } - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java deleted file mode 100644 index 0034ff0a9..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContext.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.context; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -import org.springframework.data.neo4j.core.context.tracking.EntityChangeEvent; -import org.springframework.data.neo4j.core.context.tracking.EntityComparisonStrategy; -import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy; -import org.springframework.data.neo4j.core.schema.NodeDescription; - -/** - * TODO explain what the persistence context should do other than being the plural of EntityTrackingStrategy :D - * - * @author Michael J. Simons - * @author Gerrit Meier - */ -@Slf4j -public class DefaultPersistenceContext implements PersistenceContext { - - private final EntityTrackingStrategy entityTrackingStrategy; - - private final Set registeredObjectIds = new HashSet<>(); - - public DefaultPersistenceContext() { - this.entityTrackingStrategy = getEntityTrackingStrategy(); - } - - @Override - public void register(Object entity, NodeDescription description) { - - Objects.requireNonNull(entity, "Cannot track null-entity!"); - - int identityOfEntity = getIdentityOf(entity); - - if (registeredObjectIds.contains(identityOfEntity)) { - log.debug("Object " + entity + " was already registered"); - return; - } - - registeredObjectIds.add(identityOfEntity); - entityTrackingStrategy.track(entity, description); - } - - @Override - public void deregister(Object managedEntity) { - - int identityOfEntity = getIdentityOf(managedEntity); - if (!registeredObjectIds.contains(identityOfEntity)) { - log.info("Cannot deregister " + managedEntity + " because it were never registered"); - return; - } - - entityTrackingStrategy.untrack(managedEntity); - } - - @Override - public Collection getEntityChanges(Object... objects) { - - List entityChanges = new ArrayList<>(); - - for (Object object : objects) { - Collection changeEvents = entityTrackingStrategy.getAggregatedEntityChangeEvents(object); - entityChanges.add(new EntityChanges(object, changeEvents)); - } - - return entityChanges; - } - - EntityTrackingStrategy getEntityTrackingStrategy() { - // todo choose the right / fitting implementation - return new EntityComparisonStrategy(); - } - - private int getIdentityOf(Object entity) { - return entityTrackingStrategy.getObjectIdentifier(entity); - } - - @Getter - static class EntityChanges { - private final Object entity; - - private final Collection changeEvents; - - EntityChanges(Object entity, Collection changeEvents) { - this.entity = entity; - this.changeEvents = changeEvents; - } - } - -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java deleted file mode 100644 index e5c42fe5e..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/PersistenceContext.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.context; - -import java.util.Collection; - -import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.schema.NodeDescription; - -/** - * Represents the state of varies instances being tracked. Those include: - *

    - *
  • All nodes
  • - *
  • All relationships
  • - *
- * including properties of them. - * - * @author Michael J. Simons - * @author Gerrit Meier - */ -@API(status = API.Status.INTERNAL, since = "1.0") -public interface PersistenceContext { - - /** - * Registers an entity within the context and starts tracking the entities state. - * - * @param entity The entity to register - * @param description The schema description of the entities class - */ - void register(Object entity, NodeDescription description); - - /** - * Removes an entity from this context and stops tracking the entitys state. - * - * @param managedEntity The entity to remove - */ - void deregister(Object managedEntity); - - /** - * Calculates the deltas for each registered object and returns them grouped by the object identifying function - * defined in - * {@link org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy#getObjectIdentifier(Object)} or - * one of its implementations. - * - * @param objects to calculate and return changes for - * @return All change events that got registered since registration. - */ - Collection getEntityChanges(Object... objects); -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java deleted file mode 100644 index 0673137de..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Contains all necessary infrastructure for the context of a session, i.e. dirty tracking and related. - * - * @author Michael J. Simons - */ -@NonNullApi -package org.springframework.data.neo4j.core.context; - -import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityChangeEvent.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityChangeEvent.java deleted file mode 100644 index 4cce80dea..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityChangeEvent.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.context.tracking; - -import lombok.Getter; - -import org.apiguardian.api.API; - -/** - * @author Gerrit Meier - * @author Michael J. Simons - */ -@API(status = API.Status.INTERNAL, since = "1.0") -@Getter -public final class EntityChangeEvent { - private final String propertyField; - private final Object value; - - EntityChangeEvent(String propertyField, Object value) { - this.propertyField = propertyField; - this.value = value; - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java deleted file mode 100644 index 160af229d..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategy.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.context.tracking; - -import static org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy.*; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; -import org.springframework.data.neo4j.core.schema.NodeDescription; - -/** - * Dirty tracking strategy based on property comparison. The strategy can compare two states of the same instance on - * attribute level but uses hashes for collection-like fields. Only fields that are considered to be node properties - * will get compared. - * - * @author Gerrit Meier - * @author Michael J. Simons - */ -public class EntityComparisonStrategy implements EntityTrackingStrategy { - - private final Map statesOfEntities = new HashMap<>(); - - @Override - public void track(Object entity, NodeDescription nodeDescription) { - statesOfEntities.put(getObjectIdentifier(entity), - new EntityState(nodeDescription, entity, getDefaultObjectIdentifier())); - } - - @Override - public void untrack(Object entity) { - statesOfEntities.remove(getObjectIdentifier(entity)); - } - - @Override - public Collection getAggregatedEntityChangeEvents(Object entity) { - int objectIdentifier = getObjectIdentifier(entity); - return statesOfEntities.get(objectIdentifier).aggregateChanges(entity); - } - - /** - * Compares two entities of the same instance and creates a delta {@link EntityChangeEvent}. - */ - private static class EntityState { - - private final Map oldState; - private final List objectFields; - private final Object identifier; - private final Function objectIdentifyingFuction; - - EntityState(NodeDescription nodeDescription, Object entity, Function objectIdentifyingFuction) { - - Collection properties = nodeDescription.getGraphProperties(); - this.objectFields = getFieldsFromProperties(entity, properties); - - this.objectIdentifyingFuction = objectIdentifyingFuction; - this.identifier = this.objectIdentifyingFuction.apply(entity); - this.oldState = retrieveStateFrom(entity); - } - - Set aggregateChanges(Object newObject) { - - if (!sameObject(objectIdentifyingFuction.apply(newObject))) { - throw new IllegalArgumentException("The objects to compare are not the same."); - } - Set changes = new HashSet<>(); - - for (Field field : objectFields) { - Object newValue = getFieldValueOrHashCode(field, newObject); - Object oldValue = oldState.get(field.getName()); - - // check oldValue null first to avoid NPE in `equals` calls - if (oldValue == null && newValue == null) { - continue; - } - if (oldValue == null || !oldValue.equals(newValue)) { - changes.add(new EntityChangeEvent(field.getName(), newValue)); - } - } - - return changes; - } - - private List getFieldsFromProperties(Object entity, Collection properties) { - - List fields = new ArrayList<>(); - - Class entityClass = entity.getClass(); - Map classFields = retrieveAllFields(entityClass); - for (GraphPropertyDescription property : properties) { - fields.add(classFields.get(property.getFieldName())); - } - - return fields; - } - - private Map retrieveAllFields(Class entityClass) { - - Map classFields = new HashMap<>(); - - Class classToScan = entityClass; - - do { - for (Field field : classToScan.getDeclaredFields()) { - classFields.put(field.getName(), field); - } - classToScan = classToScan.getSuperclass(); - } while (classToScan != null); - - return classFields; - } - - private boolean sameObject(Integer objectIdentifier) { - return this.identifier.equals(objectIdentifier); - } - - private Map retrieveStateFrom(Object entity) { - - Map state = new HashMap<>(); - - for (Field field : objectFields) { - state.put(field.getName(), getFieldValueOrHashCode(field, entity)); - } - - return Collections.unmodifiableMap(state); - } - - private Object getFieldValueOrHashCode(Field field, Object entity) { - try { - field.setAccessible(true); - Object value = field.get(entity); - field.setAccessible(false); - if (value == null) { - return null; - } else if (!Collection.class.isAssignableFrom(value.getClass())) { - return value; - } else { - return value.hashCode(); - } - } catch (IllegalAccessException e) { - throw new IllegalStateException("Cannot determine field value", e); - } - } - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java deleted file mode 100644 index a569cf65d..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/context/tracking/EntityTrackingStrategy.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.context.tracking; - -import java.util.Collection; -import java.util.function.Function; - -import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.schema.NodeDescription; - -/** - * A tracking strategy is used to determine if an entity has changed it state over time in a transaction. It is also - * responsible to generate and return a complete list of all these changes on an entity's attribute level. - * - * @author Gerrit Meier - */ -@API(status = API.Status.INTERNAL, since = "1.0") -public interface EntityTrackingStrategy { - - static Function getDefaultObjectIdentifier() { - return System::identityHashCode; - } - - /** - * Register an entity in the entity tracking strategy. {@link NodeDescription} is needed to determine the fields that - * are considered mapping relevant. - * - * @param entity the object that should get tracked. - * @param nodeDescription the "rich" entity's class description. - */ - void track(Object entity, NodeDescription nodeDescription); - - /** - * Remove an entity from tracking. This method should get called after an entity was deleted to have a clean change - * history state. - * - * @param entity to get removed from entity tracking - */ - void untrack(Object entity); - - /** - * Aggregates all changes that were registered for this entity since the start of tracking. - * - * @param entity for which a summarized collection of {@EntityChangeEvent}s should get created. - * @return all events that occurred since tracking started. - */ - Collection getAggregatedEntityChangeEvents(Object entity); - - /** - * Returns an object identifier for a given entity. - * - * @param entity the entity to get an unique identifier for. - * @return hash or something similar that represents the entity. - */ - default int getObjectIdentifier(Object entity) { - return getDefaultObjectIdentifier().apply(entity); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 838ff7061..5b956047b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -138,13 +138,6 @@ public class Neo4jMappingContext return new DefaultNeo4jPersistentProperty(property, neo4jPersistentProperties, simpleTypeHolder); } - @Override - public synchronized void register(Set> entityClasses) { - - this.setInitialEntitySet(entityClasses); - this.initialize(); - } - @Override public Optional> getNodeDescription(String primaryLabel) { return Optional.ofNullable(this.nodeDescriptionsByPrimaryLabel.get(primaryLabel)); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java index 7eb9f469c..bbd8777c9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -40,11 +40,16 @@ import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchA public interface Schema { /** - * Registers and scans the given set of classes to be available as Neo4j domain entities. + * Registers the given set of classes to be available as Neo4j domain entities. * - * @param entityClasses The additional set of classes to register with this schema + * @param initialEntitySet The set of classes to register with this schema */ - void register(Set> entityClasses); + void setInitialEntitySet(Set> initialEntitySet); + + /** + * Triggers the scanning of the registered, initial entity set. + */ + void initialize(); /** * Retrieves a nodes description by its primary label. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index facd2a9cb..f6459e6ec 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -23,18 +23,12 @@ import lombok.extern.slf4j.Slf4j; import java.time.Duration; import java.util.Collections; import java.util.List; -import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.TransactionConfig; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.lang.Nullable; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.InvalidIsolationLevelException; @@ -57,32 +51,14 @@ import org.springframework.util.Assert; */ @API(status = API.Status.STABLE, since = "1.0") @Slf4j -public class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements BeanFactoryAware { +public class Neo4jTransactionManager extends AbstractPlatformTransactionManager { private final Driver driver; - @Nullable - private NodeManagerFactory nodeManagerFactory; - public Neo4jTransactionManager(Driver driver) { this.driver = driver; } - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - try { - this.setNodeManagerFactory(beanFactory.getBean(NodeManagerFactory.class)); - } catch (NoSuchBeanDefinitionException ex) { - log.warn("Found no instance of {}", NodeManagerFactory.class); - } - } - - public void setNodeManagerFactory(@Nullable NodeManagerFactory nodeManagerFactory) { - - // TODO Check if the NodeManager uses the same datasource aka driver as we do - this.nodeManagerFactory = nodeManagerFactory; - } - @Override protected Object doGetTransaction() throws TransactionException { @@ -118,13 +94,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager connectionHolder.setSynchronizedWithTransaction(true); transactionObject.setResourceHolder(connectionHolder); TransactionSynchronizationManager.bindResource(this.driver, connectionHolder); - - if (this.nodeManagerFactory != null) { - NodeManagerHolder nodeManagerHolder = new NodeManagerHolder(this.nodeManagerFactory.createNodeManager()); - nodeManagerHolder.setSynchronizedWithTransaction(true); - transactionObject.setNodeManagerHolder(nodeManagerHolder); - TransactionSynchronizationManager.bindResource(this.nodeManagerFactory, nodeManagerHolder); - } } catch (Exception ex) { ex.printStackTrace(); throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage())); @@ -173,14 +142,8 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); transactionObject.getRequiredResourceHolder().close(); - transactionObject.getNodeManagerHolder() - .map(NodeManagerHolder::getNodeManager) - .ifPresent(nodeManager -> nodeManager.flush()); TransactionSynchronizationManager.unbindResource(driver); - if (this.nodeManagerFactory != null) { - TransactionSynchronizationManager.unbindResource(this.nodeManagerFactory); - } } private static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) { @@ -227,9 +190,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager @Nullable private Neo4jConnectionHolder resourceHolder; - @Nullable - private NodeManagerHolder nodeManagerHolder; - Neo4jTransactionObject(@Nullable Neo4jConnectionHolder resourceHolder) { this.resourceHolder = resourceHolder; } @@ -244,10 +204,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager this.resourceHolder = resourceHolder; } - void setNodeManagerHolder(@Nullable NodeManagerHolder nodeManagerHolder) { - this.nodeManagerHolder = nodeManagerHolder; - } - /** * @return {@literal true} if a {@link Neo4jConnectionHolder} is set. */ @@ -261,10 +217,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager return resourceHolder; } - Optional getNodeManagerHolder() { - return Optional.ofNullable(nodeManagerHolder); - } - void setRollbackOnly() { getRequiredResourceHolder().setRollbackOnly(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index fff533aa4..237c2abc9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -29,8 +29,6 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.SessionParametersTemplate; import org.neo4j.driver.Transaction; import org.neo4j.driver.reactive.RxTransaction; -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.NodeManagerFactory; import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -48,12 +46,10 @@ public final class Neo4jTransactionUtils { * @return Session parameters to configure the default session used */ public static Consumer defaultSessionParameters(@Nullable String databaseName) { - return t -> { - t - .withDefaultAccessMode(AccessMode.WRITE) - .withBookmarks(Collections.EMPTY_LIST) - .withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME)); - }; + return t -> t + .withDefaultAccessMode(AccessMode.WRITE) + .withBookmarks(Collections.EMPTY_LIST) + .withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME)); } /** @@ -105,33 +101,6 @@ public final class Neo4jTransactionUtils { return Mono.empty(); } - public static NodeManager retrieveTransactionalNodeManager(NodeManagerFactory nodeManagerFactory) { - - if (!TransactionSynchronizationManager.isSynchronizationActive()) { - return nodeManagerFactory.createNodeManager(); - } - - // Try existing transaction - NodeManagerHolder nodeManagerHolder = (NodeManagerHolder) TransactionSynchronizationManager - .getResource(nodeManagerFactory); - - if (nodeManagerHolder != null) { - - return nodeManagerHolder.getNodeManager(); - } - - // Manually create a new synchronization - nodeManagerHolder = new NodeManagerHolder(nodeManagerFactory.createNodeManager()); - nodeManagerHolder.setSynchronizedWithTransaction(true); - - TransactionSynchronizationManager.registerSynchronization( - new NodeManagerSynchronization(nodeManagerHolder, nodeManagerFactory)); - - TransactionSynchronizationManager.bindResource(nodeManagerFactory, nodeManagerHolder); - - return nodeManagerHolder.getNodeManager(); - } - private Neo4jTransactionUtils() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java deleted file mode 100644 index d077d77b3..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerHolder.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.transaction; - -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.lang.Nullable; -import org.springframework.transaction.support.ResourceHolderSupport; -import org.springframework.util.Assert; - -/** - * Dedicated holder for storing a NodeManager inside a transaction. - *

- * Note: Intended for internal usage only. - * - * @author Michael J. Simons - */ -class NodeManagerHolder extends ResourceHolderSupport { - - @Nullable - private final NodeManager nodeManager; - - NodeManagerHolder(@Nullable NodeManager nodeManager) { - this.nodeManager = nodeManager; - } - - public NodeManager getNodeManager() { - Assert.state(this.nodeManager != null, "No NodeManager available"); - return this.nodeManager; - } - -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerSynchronization.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerSynchronization.java deleted file mode 100644 index d2a81bd6c..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/NodeManagerSynchronization.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.transaction; - -import org.springframework.data.neo4j.core.NodeManagerFactory; -import org.springframework.transaction.support.ResourceHolderSynchronization; - -/** - * @author Michael J. Simons - */ -public class NodeManagerSynchronization - extends ResourceHolderSynchronization { - - private final NodeManagerHolder localNodeManagerHolder; - - NodeManagerSynchronization(NodeManagerHolder nodeManagerHolder, NodeManagerFactory nodeManagerFactory) { - - super(nodeManagerHolder, nodeManagerFactory); - this.localNodeManagerHolder = nodeManagerHolder; - } - - @Override - protected void flushResource(NodeManagerHolder resourceHolder) { - resourceHolder.getNodeManager().flush(); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NoResultException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java similarity index 70% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NoResultException.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java index f26205a98..c26f63500 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NoResultException.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/NoResultException.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.springframework.data.neo4j.repository; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSizeException; +import org.springframework.dao.EmptyResultDataAccessException; /** * Throw when a query doesn't return a required result. @@ -29,9 +29,16 @@ import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSiz * @since 1.0 */ @API(status = API.Status.STABLE, since = "1.0") -public class NoResultException extends IllegalResultSizeException { +public class NoResultException extends EmptyResultDataAccessException { - NoResultException(long expectedNumberOfResults, String query) { - super(expectedNumberOfResults, 0L, query); + private final String query; + + public NoResultException(int expectedNumberOfResults, String query) { + super(expectedNumberOfResults); + this.query = query; + } + + public String getQuery() { + return query; } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java index 0e358a883..17bedd3c1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java @@ -31,9 +31,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.data.neo4j.core.NodeManager; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; -import org.springframework.transaction.PlatformTransactionManager; /** * Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()}, @@ -78,15 +76,14 @@ public @interface EnableNeo4jRepositories { Class repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class; /** - * Configures the name of the {@link NodeManager} bean to be used with the repositories detected. + * Configures the name of the {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected. */ - String nodeManagerFactoryRef() default DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME; + String neo4jMappingContextRef() default DEFAULT_MAPPING_CONTEXT_BEAN_NAME; /** - * Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories - * discovered through this annotation. Defaults to {@code transactionManager}. + * Configures the name of the {@link org.springframework.data.neo4j.core.Neo4jClient} bean to be used with the repositories detected. */ - String transactionManagerRef() default DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; + String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_NAME; /** * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index 34a6aa0a7..bcd9bc16c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -24,12 +24,9 @@ import java.util.Collections; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; -import org.springframework.data.neo4j.repository.support.NodeManagerFactoryBean; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; @@ -49,20 +46,12 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati /** * See {@link AbstractBeanDefinition#INFER_METHOD}. */ - static final String GENERATE_BEAN_NAME = "(generated)"; - - static final String DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME = "nodeManagerFactory"; - static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; + static final String DEFAULT_NEO4J_CLIENT_NAME = "neo4jClient"; /** - * Holds the name of the shared NodeManagerBean created from the factory with the configured name. + * See {@link AbstractBeanDefinition#INFER_METHOD}. */ - private String generatedNodeManagerBeanName; - - /** - * Holds the name of the shared Neo4j mapping context.. - */ - private String generatedMappingContextBeanName; + static final String DEFAULT_MAPPING_CONTEXT_BEAN_NAME = "neo4jMappingContext"; /* * (non-Javadoc) @@ -99,61 +88,9 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati @Override public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { - builder.addPropertyValue("transactionManager", - source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); - - builder.addPropertyReference("nodeManager", this.generatedNodeManagerBeanName); - builder.addPropertyReference("neo4jMappingContext", this.generatedMappingContextBeanName); - } - - @Override - public void registerBeansForRoot(BeanDefinitionRegistry registry, - RepositoryConfigurationSource config) { - - // Mapping context - AbstractBeanDefinition neo4jMappingContextBeanDefinition = BeanDefinitionBuilder - .rootBeanDefinition(Neo4jMappingContext.class) - .getBeanDefinition(); - this.generatedMappingContextBeanName = registerWithSourceAndGeneratedBeanName( - neo4jMappingContextBeanDefinition, registry, config); - - // Augmented node manager factory (creating injectable, shared instances of NodeManager that are aware of the mapping context). - String nameOfNodeManagerFactory = config.getAttribute("nodeManagerFactoryRef") - .orElse(DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME); - AbstractBeanDefinition sharedSessionCreatorBeanDefinition = BeanDefinitionBuilder - .rootBeanDefinition(NodeManagerFactoryBean.class) - .addConstructorArgReference(nameOfNodeManagerFactory) - .addConstructorArgReference(generatedMappingContextBeanName) - .getBeanDefinition(); - this.generatedNodeManagerBeanName = registerWithSourceAndGeneratedBeanName( - sharedSessionCreatorBeanDefinition, registry, config); - } - - /** - * Uses a generated bean name if {@code configuredBeanName} is equal to {@link #GENERATE_BEAN_NAME}, otherwise uses - * the configured bean name to register the new bean. Does not check if there's already a bean under the configured - * name but throws a {@link org.springframework.beans.factory.BeanDefinitionStoreException}. Checks whether a bean is - * already registered under {@code configuredBeanName} in the given {@link BeanDefinitionRegistry} and uses a - * generated name for registering the bean instead. If not, the suggested bean name is used. - * - * @param bean must not be {@literal null}. - * @param registry must not be {@literal null}. - * @param configuredBeanName must not be {@literal null} or empty. - * @param source must not be {@literal null}. - * @return the bean name used for registering the given {@link AbstractBeanDefinition} - * @throws org.springframework.beans.factory.BeanDefinitionStoreException if the BeanDefinition is invalid or if there - * is already a BeanDefinition for the specified bean name * (and we are not allowed to override it) - */ - private static String registerWithGeneratedNameOrUseConfigured(AbstractBeanDefinition bean, - BeanDefinitionRegistry registry, String configuredBeanName, Object source) { - - String registeredBeanName = configuredBeanName; - if (GENERATE_BEAN_NAME.equals(configuredBeanName)) { - registeredBeanName = registerWithSourceAndGeneratedBeanName(bean, registry, source); - } else { - bean.setSource(source); - registry.registerBeanDefinition(configuredBeanName, bean); - } - return registeredBeanName; + builder.addPropertyReference("neo4jClient", + source.getAttribute("neo4jClientRef").orElse(DEFAULT_NEO4J_CLIENT_NAME)); + builder.addPropertyReference("neo4jMappingContext", + source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java index b058db391..e76807dd2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -25,9 +25,9 @@ import org.springframework.data.domain.Range; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.support.PreparedQuery; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; @@ -41,19 +41,19 @@ import org.springframework.util.Assert; */ abstract class AbstractNeo4jQuery implements RepositoryQuery { - protected final NodeManager nodeManager; + protected final Neo4jClient neo4jClient; protected final Neo4jMappingContext mappingContext; protected final Neo4jQueryMethod queryMethod; protected final Class domainType; - AbstractNeo4jQuery(NodeManager nodeManager, + AbstractNeo4jQuery(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod) { - Assert.notNull(nodeManager, "The node manager is required."); + Assert.notNull(neo4jClient, "The Neo4j client is required."); Assert.notNull(mappingContext, "The mapping context is required."); Assert.notNull(queryMethod, "Query method must not be null!"); - this.nodeManager = nodeManager; + this.neo4jClient = neo4jClient; this.mappingContext = mappingContext; this.queryMethod = queryMethod; this.domainType = queryMethod.getReturnedObjectType(); @@ -66,7 +66,7 @@ abstract class AbstractNeo4jQuery implements RepositoryQuery { @Override public final Object execute(Object[] parameters) { - return new Neo4jQueryExecution.DefaultQueryExecution(nodeManager) + return new Neo4jQueryExecution.DefaultQueryExecution(neo4jClient) .execute(prepareQuery(parameters), queryMethod.isCollectionQuery()); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java index b291f3af9..5eb5d1215 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java @@ -20,9 +20,9 @@ package org.springframework.data.neo4j.repository.query; import lombok.RequiredArgsConstructor; -import org.springframework.data.neo4j.core.ExecutableQuery; -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.repository.support.ExecutableQuery; +import org.springframework.data.neo4j.repository.support.PreparedQuery; /** * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a @@ -40,12 +40,12 @@ interface Neo4jQueryExecution { @RequiredArgsConstructor class DefaultQueryExecution implements Neo4jQueryExecution { - private final NodeManager nodeManager; + private final Neo4jClient neo4jClient; @Override - public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { + public Object execute(PreparedQuery description, boolean asCollectionQuery) { - ExecutableQuery executableQuery = nodeManager.toExecutableQuery(preparedQuery); + ExecutableQuery executableQuery = ExecutableQuery.create(description, neo4jClient); if (asCollectionQuery) { return executableQuery.getResults(); } else { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java index 1f44ab431..f3300d364 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java @@ -23,7 +23,7 @@ import lombok.RequiredArgsConstructor; import java.lang.reflect.Method; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; @@ -43,7 +43,7 @@ import org.springframework.data.repository.query.RepositoryQuery; @RequiredArgsConstructor public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { - private final NodeManager nodeManager; + private final Neo4jClient neo4jClient; private final Neo4jMappingContext mappingContext; private final QueryMethodEvaluationContextProvider evaluationContextProvider; @@ -58,12 +58,12 @@ public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { String namedQueryName = queryMethod.getNamedQueryName(); if (namedQueries.hasQuery(namedQueryName)) { - return StringBasedNeo4jQuery.create(nodeManager, mappingContext, evaluationContextProvider, queryMethod, + return StringBasedNeo4jQuery.create(neo4jClient, mappingContext, evaluationContextProvider, queryMethod, namedQueries.getQuery(namedQueryName)); } else if (queryMethod.hasQueryAnnotation()) { - return StringBasedNeo4jQuery.create(nodeManager, mappingContext, evaluationContextProvider, queryMethod); + return StringBasedNeo4jQuery.create(neo4jClient, mappingContext, evaluationContextProvider, queryMethod); } else { - return new PartTreeNeo4jQuery(nodeManager, mappingContext, queryMethod); + return new PartTreeNeo4jQuery(neo4jClient, mappingContext, queryMethod); } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 9c0565418..394ce588c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -35,10 +35,10 @@ import java.util.Map; import java.util.Set; import org.neo4j.driver.types.Point; -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.springframework.data.neo4j.repository.support.PreparedQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; @@ -75,11 +75,11 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { private final PartTree tree; PartTreeNeo4jQuery( - NodeManager nodeManager, + Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod ) { - super(nodeManager, mappingContext, queryMethod); + super(neo4jClient, mappingContext, queryMethod); this.tree = new PartTree(queryMethod.getName(), domainType); @@ -97,7 +97,6 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { mappingContext, domainType, tree, formalParameters, actualParameters ); - String cypherQuery = queryCreator.createQuery(); Map boundedParameters = formalParameters .getBindableParameters().stream() diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index 2de60721b..a6742e60f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -23,9 +23,9 @@ import java.util.Map; import java.util.Optional; import org.springframework.data.mapping.MappingException; -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.support.PreparedQuery; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -92,13 +92,13 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { * Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The annotation * is expected to have a value. * - * @param nodeManager + * @param neo4jClient * @param mappingContext * @param evaluationContextProvider * @param queryMethod * @return A new instance of a String based Neo4j query. */ - static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext, + static StringBasedNeo4jQuery create(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod) { @@ -109,36 +109,36 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { .filter(StringUtils::hasText) .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not.")); - return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod, + return new StringBasedNeo4jQuery(neo4jClient, mappingContext, evaluationContextProvider, queryMethod, cypherTemplate, queryAnnotation.count(), queryAnnotation.exists(), queryAnnotation.delete()); } /** * Create a {@link StringBasedNeo4jQuery} based on an explicit Cypher template. * - * @param nodeManager + * @param neo4jClient * @param mappingContext * @param evaluationContextProvider * @param queryMethod * @param cypherTemplate The template to use. * @return A new instance of a String based Neo4j query. */ - static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext, + static StringBasedNeo4jQuery create(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod, String cypherTemplate) { Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template."); - return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod, + return new StringBasedNeo4jQuery(neo4jClient, mappingContext, evaluationContextProvider, queryMethod, cypherTemplate, false, false, false); } - private StringBasedNeo4jQuery(NodeManager nodeManager, + private StringBasedNeo4jQuery(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod, String cypherTemplate, boolean countQuery, boolean existsQuery, boolean deleteQuery) { - super(nodeManager, mappingContext, queryMethod); + super(neo4jClient, mappingContext, queryMethod); this.countQuery = countQuery; this.existsQuery = existsQuery; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java new file mode 100644 index 000000000..3fd62c1e3 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import static java.util.stream.Collectors.*; + +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.neo4j.driver.exceptions.NoSuchRecordException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.repository.NoResultException; + +@RequiredArgsConstructor public +final class DefaultExecutableQuery implements ExecutableQuery { + + private final PreparedQuery preparedQuery; + private final Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec; + + @Override + public List getResults() { + return fetchSpec.all().stream().collect(toList()); + } + + @Override + public Optional getSingleResult() { + try { + return fetchSpec.one(); + } catch (NoSuchRecordException e) { + // This exception is thrown by the driver in both cases when there are 0 or 1+n records + // So there has been an incorrect result size, but not to few results but to many. + throw new IncorrectResultSizeDataAccessException(1); + } + } + + @Override + public T getRequiredSingleResult() { + return fetchSpec.one() + .orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery())); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultPreparedQuery.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultPreparedQuery.java index e473311ea..e2d254e25 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultPreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultPreparedQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.springframework.data.neo4j.repository.support; import java.util.Map; import java.util.Optional; @@ -27,9 +27,9 @@ import org.neo4j.driver.types.TypeSystem; import org.springframework.lang.Nullable; /** - * Typed preparation of a query that is used to create an {@link org.springframework.data.neo4j.core.ExecutableQuery} of the same type. + * Typed preparation of a query that is used to create an {@link ExecutableQuery} of the same type. *

- * When no mapping function is provided, the node manager will assume a simple type to be returned. Otherwise make sure + * When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure * that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected * by the mapping function to work correctly. * diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java similarity index 55% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ExecutableQuery.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java index 74a23c2ac..387411776 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ExecutableQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java @@ -16,12 +16,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.springframework.data.neo4j.repository.support; +import java.util.Collection; import java.util.List; import java.util.Optional; import org.apiguardian.api.API; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.repository.NoResultException; /** * Interface for controlling query execution. @@ -31,9 +35,23 @@ import org.apiguardian.api.API; * @soundtrack Deichkind - Niveau weshalb warum * @since 1.0 */ -@API(status = API.Status.STABLE, since = "1.0") +@API(status = API.Status.INTERNAL, since = "1.0") public interface ExecutableQuery { + static ExecutableQuery create(PreparedQuery description, Neo4jClient neo4jClient) { + + Class resultType = description.getResultType(); + Neo4jClient.MappingSpec, Collection, T> mappingSpec = neo4jClient + .newQuery(description.getCypherQuery()) + .bindAll(description.getParameters()) + .fetchAs(resultType); + Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec = description.getOptionalMappingFunction() + .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) + .orElse(mappingSpec); + + return new DefaultExecutableQuery<>(description, fetchSpec); + } + /** * @return All results returned by this query. */ @@ -41,13 +59,12 @@ public interface ExecutableQuery { /** * @return A single result - * @throws NonUniqueResultException + * @throws IncorrectResultSizeDataAccessException */ Optional getSingleResult(); /** * @return A single result - * @throws NonUniqueResultException * @throws NoResultException */ T getRequiredSingleResult(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 910a403ce..40f9447f2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -20,7 +20,7 @@ package org.springframework.data.neo4j.repository.support; import java.util.Optional; -import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Neo4jQueryLookupStrategy; @@ -41,12 +41,12 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro */ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { - private final NodeManager nodeManager; + private final Neo4jClient neo4jClient; private final Neo4jMappingContext mappingContext; - Neo4jRepositoryFactory(NodeManager nodeManager, Neo4jMappingContext mappingContext) { - this.nodeManager = nodeManager; + Neo4jRepositoryFactory(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext) { + this.neo4jClient = neo4jClient; this.mappingContext = mappingContext; } @@ -57,7 +57,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { @Override protected Object getTargetRepository(RepositoryInformation metadata) { - return getTargetRepositoryViaReflection(metadata, nodeManager, mappingContext, metadata.getDomainType()); + return getTargetRepositoryViaReflection(metadata, neo4jClient, mappingContext, metadata.getDomainType()); } @Override @@ -73,6 +73,6 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { protected Optional getQueryLookupStrategy(Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new Neo4jQueryLookupStrategy(nodeManager, mappingContext, evaluationContextProvider)); + return Optional.of(new Neo4jQueryLookupStrategy(neo4jClient, mappingContext, evaluationContextProvider)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java index 3ee4eb141..8b853649e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java @@ -20,7 +20,7 @@ package org.springframework.data.neo4j.repository.support; import java.io.Serializable; -import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -36,7 +36,7 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF public class Neo4jRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport { - private NodeManager nodeManager; + private Neo4jClient neo4jClient; private Neo4jMappingContext neo4jMappingContext; @@ -49,8 +49,8 @@ public class Neo4jRepositoryFactoryBean, S, ID exten super(repositoryInterface); } - public void setNodeManager(NodeManager nodeManager) { - this.nodeManager = nodeManager; + public void setNeo4jClient(Neo4jClient neo4jClient) { + this.neo4jClient = neo4jClient; } public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) { @@ -59,7 +59,7 @@ public class Neo4jRepositoryFactoryBean, S, ID exten @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { - return new Neo4jRepositoryFactory(nodeManager, neo4jMappingContext); + return new Neo4jRepositoryFactory(neo4jClient, neo4jMappingContext); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java deleted file mode 100644 index 5a265f7a2..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/NodeManagerFactoryBean.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import lombok.RequiredArgsConstructor; - -import org.apiguardian.api.API; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.NodeManagerFactory; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; - -/** - * This is a shim that integrates a {@link NodeManagerFactory} with Spring Datas infrastructure. It takes in the factory - * that needs to be provided by the user and the mapping context provided by our infrastructure. Springs mapping context - * replaces the default noop scanner. - * - * @author Gerrit Meier - * @author Michael J. Simons - */ -@API(status = API.Status.INTERNAL, since = "1.0") -@RequiredArgsConstructor -public final class NodeManagerFactoryBean implements InitializingBean, FactoryBean { - - private final NodeManagerFactory target; - - private final Neo4jMappingContext neo4jMappingContext; - - @Override - public NodeManager getObject() { - return SharedNodeManagerCreator.createSharedNodeManager(this.target); - } - - @Override - public Class getObjectType() { - return NodeManager.class; - } - - @Override - public void afterPropertiesSet() { - - target.setSchema(this.neo4jMappingContext); - target.initialize(); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java similarity index 82% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java index f9d1782ef..fcbe4864a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.springframework.data.neo4j.repository.support; import java.util.Collections; import java.util.HashMap; @@ -30,12 +30,19 @@ import org.neo4j.driver.types.TypeSystem; import org.springframework.lang.Nullable; /** + * Typed preparation of a query that is used to create an {@link ExecutableQuery} of the same type. + *

+ * When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure + * that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected + * by the mapping function to work correctly. + * * @param The type of the objects returned by this query. + * @see ExecutableQuery#create(PreparedQuery, org.springframework.data.neo4j.core.Neo4jClient) * @author Michael J. Simons * @soundtrack Deichkind - Arbeit nervt * @since 1.0 */ -@API(status = API.Status.STABLE, since = "1.0") +@API(status = API.Status.INTERNAL, since = "1.0") public interface PreparedQuery { static RequiredBuildStep queryFor(Class resultType) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SharedNodeManagerCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SharedNodeManagerCreator.java deleted file mode 100644 index afca68a4b..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SharedNodeManagerCreator.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import lombok.extern.slf4j.Slf4j; - -import java.io.Serializable; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Function; - -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.NodeManagerFactory; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils; -import org.springframework.lang.Nullable; -import org.springframework.transaction.support.TransactionSynchronizationManager; -import org.springframework.util.ReflectionUtils; - -/** - * @author Michael J. Simons - */ -@Slf4j -final class SharedNodeManagerCreator { - - private static final Set TRANSACTION_REQUIRING_METHODS; - - static { - Set tmp = new HashSet<>(); - - tmp.add("save"); - tmp.add("delete"); - - TRANSACTION_REQUIRING_METHODS = Collections.unmodifiableSet(tmp); - } - - public static NodeManager createSharedNodeManager(NodeManagerFactory nodeManagerFactory) { - - return (NodeManager) Proxy.newProxyInstance(SharedNodeManagerCreator.class.getClassLoader(), - new Class[] { NodeManager.class }, new SharedNodeManagerInvocationHandler(nodeManagerFactory)); - } - - private static class SharedNodeManagerInvocationHandler implements InvocationHandler, Serializable { - - private final NodeManagerFactory targetFactory; - - SharedNodeManagerInvocationHandler(NodeManagerFactory targetFactory) { - this.targetFactory = targetFactory; - } - - @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - - String methodName = method.getName(); - switch (methodName) { - case "equals": - // Only consider equal when proxies are identical. - return (proxy == args[0]); - case "hashCode": - // Use hashCode of proxy. - return hashCode(); - case "toString": - // Deliver toString without touching a target Session. - return "Shared Session proxy for target factory [" + this.targetFactory + "]"; - default: - Function methodCall = - targetSession -> ReflectionUtils.invokeMethod(method, targetSession, args); - return invokeInTransaction(methodName, methodCall); - } - } - - private Object invokeInTransaction(String methodName, Function methodCall) { - - // Determine current Session: either the transactional one - // managed by the factory or a temporary one for the given invocation. - NodeManager target = Neo4jTransactionUtils.retrieveTransactionalNodeManager(this.targetFactory); - - if (TRANSACTION_REQUIRING_METHODS.contains(methodName)) { - if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive() - && target.getTransaction() != null)) { - throw new IllegalStateException("No NodeManager with actual transaction available " - + "for current thread - cannot reliably process '" + methodName + "' call"); - } - } - - // Regular Session operations. - boolean isNewSession = false; - if (target == null) { - log.debug("Creating new Session for shared Session invocation"); - target = this.targetFactory.createNodeManager(); - isNewSession = true; - } - - // Invoke method on current Session. - try { - return methodCall.apply(target); - } finally { - if (isNewSession) { - target.flush(); - } - } - } - } - - private SharedNodeManagerCreator() { - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index c5044578f..1a3263468 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.repository.support; import static java.util.Collections.*; +import static java.util.stream.Collectors.*; import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; import static org.springframework.data.neo4j.core.schema.NodeDescription.*; @@ -45,8 +46,7 @@ import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; @@ -81,7 +81,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { private static final Renderer renderer = CypherRenderer.create(); - private final NodeManager nodeManager; + private final Neo4jClient neo4jClient; private final Neo4jMappingContext mappingContext; @@ -91,9 +91,9 @@ class SimpleNeo4jRepository implements Neo4jRepository { private final BiFunction mappingFunction; private Expression idExpression; - SimpleNeo4jRepository(NodeManager nodeManager, Neo4jMappingContext mappingContext, Class nodeClass) { + SimpleNeo4jRepository(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, Class nodeClass) { - this.nodeManager = nodeManager; + this.neo4jClient = neo4jClient; this.mappingContext = mappingContext; this.nodeClass = nodeClass; @@ -124,7 +124,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { .orderBy(toSortItems(nodeDescription, sort)) .build(); - return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); + return createExecutableQuery(statement).getResults(); } @Override @@ -137,7 +137,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { Statement statement = returningWithPaging.build(); - List allResult = nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); + List allResult = createExecutableQuery(statement).getResults(); LongSupplier totalCountSupplier = this::count; return PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier); } @@ -153,8 +153,8 @@ class SimpleNeo4jRepository implements Neo4jRepository { Statement statement = returningWithPaging.build(); - List page = nodeManager - .toExecutableQuery(prepareQuery(example.getProbeType(), statement, predicate.parameters)).getResults(); + List page = createExecutableQuery(example.getProbeType(), statement, predicate.parameters) + .getResults(); LongSupplier totalCountSupplier = this::count; return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); } @@ -173,7 +173,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override @Transactional public S save(S entity) { - return this.nodeManager.save(entity); + throw new UnsupportedOperationException("Not there yet."); } @Override @@ -189,7 +189,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { .prepareMatchOf(nodeDescription, Optional.of(idExpression.isEqualTo(literalOf(id)))) .returning(asterisk()) .build(); - return nodeManager.toExecutableQuery(prepareQuery(statement)).getSingleResult(); + return createExecutableQuery(statement).getSingleResult(); } @Override @@ -200,9 +200,9 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Iterable findAll() { - Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()).returning(asterisk()) - .build(); - return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults(); + Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) + .returning(asterisk()).build(); + return createExecutableQuery(statement).getResults(); } @Override @@ -213,7 +213,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { .returning(asterisk()) .build(); - return nodeManager.toExecutableQuery(prepareQuery(nodeClass, statement, singletonMap("ids", ids))).getResults(); + return createExecutableQuery(nodeClass, statement, singletonMap("ids", ids)).getResults(); } @Override @@ -222,8 +222,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) .returning(Functions.count(asterisk())).build(); - return nodeManager.toExecutableQuery(prepareQuery(Long.class, statement, Collections.emptyMap())) - .getRequiredSingleResult(); + return createExecutableQuery(Long.class, statement, Collections.emptyMap()).getRequiredSingleResult(); } @Override @@ -263,8 +262,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { .returning(asterisk()) .build(); - PreparedQuery preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters); - return nodeManager.toExecutableQuery(preparedQuery).getSingleResult(); + return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getSingleResult(); } @Override @@ -275,8 +273,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { .returning(asterisk()) .build(); - PreparedQuery preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters); - return nodeManager.toExecutableQuery(preparedQuery).getResults(); + return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults(); } @Override @@ -288,8 +285,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { .returning(asterisk()) .orderBy(toSortItems(nodeDescription, sort)).build(); - PreparedQuery preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters); - return nodeManager.toExecutableQuery(preparedQuery).getResults(); + return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults(); } @Override @@ -301,8 +297,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { .returning(Functions.count(asterisk())) .build(); - PreparedQuery preparedQuery = prepareQuery(Long.class, statement, predicate.parameters); - return nodeManager.toExecutableQuery(preparedQuery).getRequiredSingleResult(); + return createExecutableQuery(Long.class, statement, predicate.parameters).getRequiredSingleResult(); } @Override @@ -419,11 +414,11 @@ class SimpleNeo4jRepository implements Neo4jRepository { return predicate; } - private PreparedQuery prepareQuery(Statement statement) { - return prepareQuery(nodeClass, statement, Collections.emptyMap()); + private ExecutableQuery createExecutableQuery(Statement statement) { + return createExecutableQuery(nodeClass, statement, Collections.emptyMap()); } - private PreparedQuery prepareQuery(Class resultType, Statement statement, + private ExecutableQuery createExecutableQuery(Class resultType, Statement statement, Map parameters) { BiFunction mappingFunctionToUse = this.mappingFunction; @@ -431,10 +426,12 @@ class SimpleNeo4jRepository implements Neo4jRepository { mappingFunctionToUse = mappingContext.getMappingFunctionFor(resultType).orElse(null); } - return PreparedQuery.queryFor(resultType) + PreparedQuery queryDescription = PreparedQuery.queryFor(resultType) .withCypherQuery(renderer.render(statement)) .withParameters(parameters) .usingMappingFunction(mappingFunctionToUse) .build(); + + return ExecutableQuery.create(queryDescription, neo4jClient); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java deleted file mode 100644 index 1560594c9..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/DefaultPersistenceContextTest.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.context; - -import static org.mockito.Mockito.*; - -import java.util.Arrays; -import java.util.HashSet; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.Schema; - -/** - * @author Gerrit Meier - * @author Michael J. Simons - */ -class DefaultPersistenceContextTest { - - private Schema schema; - private PersistenceContext context; - private EntityTrackingStrategy entityTrackingStrategy; - - @BeforeEach - void setup() { - entityTrackingStrategy = mock(EntityTrackingStrategy.class); - when(entityTrackingStrategy.getObjectIdentifier(any())) - .thenAnswer(invocation -> System.identityHashCode(invocation.getArguments()[0])); - - Neo4jMappingContext mappingContext = new Neo4jMappingContext(); - mappingContext.setInitialEntitySet(new HashSet>(Arrays.asList(Something.class))); - mappingContext.initialize(); - schema = mappingContext; - - // override method to return a verifiable mock - context = new DefaultPersistenceContext() { - @Override - EntityTrackingStrategy getEntityTrackingStrategy() { - return entityTrackingStrategy; - } - }; - } - - @Test - void registerAddsEntityToTrackingStrategy() { - - NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); - - context.register(new Something(), nodeDescription); - - verify(entityTrackingStrategy).track(any(), any()); - } - - @Test - void registerTheSameObjectMultipleTimesCallsTrackJustOnce() { - - NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); - Something entity = new Something(); - - context.register(entity, nodeDescription); - context.register(entity, nodeDescription); - - verify(entityTrackingStrategy).track(any(), any()); - } - - @Test - void registerTwoObjectOfTheSameTypeCallsTrackTwice() { - - NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); - Something entity1 = new Something(); - Something entity2 = new Something(); - - context.register(entity1, nodeDescription); - context.register(entity2, nodeDescription); - - verify(entityTrackingStrategy, times(2)).track(any(), any()); - } - - @Test - void triggersDeltaCalculationOnDeltaCall() { - - NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); - Something entity = new Something(); - - context.register(entity, nodeDescription); - context.getEntityChanges(entity); - - verify(entityTrackingStrategy).getAggregatedEntityChangeEvents(entity); - } - - @Test - void deregisterRemovesEntityFromTracking() { - - NodeDescription nodeDescription = schema.getRequiredNodeDescription(Something.class); - Something entity = new Something(); - - context.register(entity, nodeDescription); - context.deregister(entity); - - verify(entityTrackingStrategy).untrack(entity); - } - - @Test - void deregisterUnknownEntityDoesNotCallUntrack() { - Something entity = new Something(); - - context.deregister(entity); - - verify(entityTrackingStrategy, never()).untrack(entity); - } - - class Something { - - @Id - private Long id; - - String value; - } - -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java deleted file mode 100644 index 588ecac9b..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/context/tracking/EntityComparisonStrategyTest.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.context.tracking; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; - -import org.junit.jupiter.api.Test; -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; -import org.springframework.data.neo4j.core.schema.NodeDescription; - -/** - * @author Gerrit Meier - */ -class EntityComparisonStrategyTest { - - private final GraphPropertyDescription valuePropertyDescription; - - private final GraphPropertyDescription informationPropertyDescription; - - private final GraphPropertyDescription parentPropertyDescription; - - private final NodeDescription description; - - EntityComparisonStrategyTest() { - this.valuePropertyDescription = mock(GraphPropertyDescription.class); - when(this.valuePropertyDescription.getFieldName()).thenReturn("value"); - when(this.valuePropertyDescription.getPropertyName()).thenReturn("value"); - - this.informationPropertyDescription = mock(GraphPropertyDescription.class); - when(this.informationPropertyDescription.getFieldName()).thenReturn("information"); - when(this.informationPropertyDescription.getPropertyName()).thenReturn("information"); - - this.parentPropertyDescription = mock(GraphPropertyDescription.class); - when(this.parentPropertyDescription.getFieldName()).thenReturn("parentValue"); - when(this.parentPropertyDescription.getPropertyName()).thenReturn("parentValue"); - - this.description = mock(NodeDescription.class); - when(this.description.getPrimaryLabel()).thenReturn("Something"); - when(this.description.getGraphProperties()).thenReturn(Arrays - .asList(this.valuePropertyDescription, this.informationPropertyDescription, - this.parentPropertyDescription)); - when(this.description.getUnderlyingClass()).thenReturn(Something.class); - } - - @Test - void trackSimplePropertyChange() { - EntityComparisonStrategy strategy = new EntityComparisonStrategy(); - Something something = new Something("oldValue"); - - strategy.track(something, description); - - String fieldName = "value"; - String newValue = "newValue"; - something.value = newValue; - - Collection changeEvents = strategy.getAggregatedEntityChangeEvents(something); - - EntityChangeEvent changeEvent = changeEvents.iterator().next(); - assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName); - assertThat(changeEvent.getValue()).isEqualTo(newValue); - } - - @Test - void trackCollectionPropertyChange() { - EntityComparisonStrategy strategy = new EntityComparisonStrategy(); - Something something = new Something("oldValue"); - - strategy.track(something, description); - - String fieldName = "information"; - something.information.add("additional entry"); - - Collection changeEvents = strategy.getAggregatedEntityChangeEvents(something); - - EntityChangeEvent changeEvent = changeEvents.iterator().next(); - assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName); - assertThat(changeEvent.getValue()).isInstanceOf(Integer.class); - } - - @Test - void trackCollectionPropertyReorderChange() { - EntityComparisonStrategy strategy = new EntityComparisonStrategy(); - Something something = new Something("blubb"); - something.information.add("entry 1"); - something.information.add("entry 2"); - - strategy.track(something, description); - - something.information.sort(Comparator.reverseOrder()); - - String fieldName = "information"; - - Collection changeEvents = strategy.getAggregatedEntityChangeEvents(something); - - EntityChangeEvent changeEvent = changeEvents.iterator().next(); - assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName); - assertThat(changeEvent.getValue()).isInstanceOf(Integer.class); - } - - @Test - void trackParentClassPropertyChange() { - EntityComparisonStrategy strategy = new EntityComparisonStrategy(); - Something something = new Something("oldValue"); - - strategy.track(something, description); - - String fieldName = "parentValue"; - String newValue = "newValue"; - something.parentValue = newValue; - - Collection changeEvents = strategy.getAggregatedEntityChangeEvents(something); - - EntityChangeEvent changeEvent = changeEvents.iterator().next(); - assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName); - assertThat(changeEvent.getValue()).isEqualTo(newValue); - } - - @Test - void trackMultipleObjects() { - EntityComparisonStrategy strategy = new EntityComparisonStrategy(); - Something something1 = new Something("oldValue"); - Something something2 = new Something("oldValue"); - - strategy.track(something1, description); - strategy.track(something2, description); - - String fieldName = "value"; - String newValue1 = "newValue1"; - String newValue2 = "newValue2"; - something1.value = newValue1; - something2.value = newValue2; - - Collection changeEvents1 = strategy.getAggregatedEntityChangeEvents(something1); - Collection changeEvents2 = strategy.getAggregatedEntityChangeEvents(something2); - - EntityChangeEvent changeEvent1 = changeEvents1.iterator().next(); - assertThat(changeEvent1.getPropertyField()).isEqualTo(fieldName); - assertThat(changeEvent1.getValue()).isEqualTo(newValue1); - - EntityChangeEvent changeEvent2 = changeEvents2.iterator().next(); - assertThat(changeEvent2.getPropertyField()).isEqualTo(fieldName); - assertThat(changeEvent2.getValue()).isEqualTo(newValue2); - } - - class ParentClass { - String parentValue; - } - - class Something extends ParentClass { - final List information = new ArrayList<>(); - String value; - - Something(String value) { - this.value = value; - } - - // create own equals and hashCode that should not get used in any technical parts of the dirty tracking - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Something something = (Something) o; - return value.equals(something.value) && information.equals(something.information); - } - - @Override - public int hashCode() { - return Objects.hash(value, information); - } - } -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 32c2f56c6..bf523c328 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -50,14 +50,12 @@ import org.springframework.data.domain.Sort; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; -import org.springframework.data.neo4j.core.NodeManagerFactory; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.data.neo4j.config.AbstractNeo4jConfig; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; /** @@ -830,25 +828,12 @@ class RepositoryIT { @Configuration @EnableNeo4jRepositories @EnableTransactionManagement - static class Config { + static class Config extends AbstractNeo4jConfig { @Bean - public static Driver driver() { + public Driver driver() { return neo4jConnectionSupport.openConnection(); } - - @Bean - public NodeManagerFactory nodeManagerFactory(Driver driver) { - - return new NodeManagerFactory(driver, PersonWithAllConstructor.class, PersonWithNoConstructor.class, - PersonWithWither.class, ThingWithAssignedId.class, KotlinPerson.class); - } - - @Bean - public PlatformTransactionManager transactionManager(Driver driver) { - - return new Neo4jTransactionManager(driver); - } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java index e9f8b628b..40b7b681c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java @@ -35,7 +35,7 @@ import org.neo4j.driver.Values; import org.neo4j.driver.types.Point; import org.springframework.data.annotation.Id; import org.springframework.data.mapping.MappingException; -import org.springframework.data.neo4j.core.NodeManager; +import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -70,7 +70,7 @@ final class RepositoryQueryTest { private static final ProjectionFactory PROJECTION_FACTORY = new SpelAwareProxyProjectionFactory(); @Mock - NodeManager nodeManager; + Neo4jClient neo4jClient; @Mock Neo4jMappingContext schema; @@ -97,7 +97,7 @@ final class RepositoryQueryTest { @Test void shouldSelectPartTreeNeo4jQuery() { - final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock( + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock( Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT); RepositoryQuery query = lookupStrategy @@ -109,7 +109,7 @@ final class RepositoryQueryTest { @Test void shouldSelectStringBasedNeo4jQuery() { - final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock( + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock( Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT); RepositoryQuery query = lookupStrategy @@ -125,7 +125,7 @@ final class RepositoryQueryTest { when(namedQueries.hasQuery(namedQueryName)).thenReturn(true); when(namedQueries.getQuery(namedQueryName)).thenReturn("MATCH (n) RETURN n"); - final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock( + final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock( Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT); RepositoryQuery query = lookupStrategy @@ -180,7 +180,7 @@ final class RepositoryQueryTest { assertThatExceptionOfType(MappingException.class) .isThrownBy( - () -> StringBasedNeo4jQuery.create(mock(NodeManager.class), mock(Neo4jMappingContext.class), + () -> StringBasedNeo4jQuery.create(mock(Neo4jClient.class), mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT, method)) .withMessage("Expected @Query annotation to have a value, but it did not."); } @@ -191,7 +191,7 @@ final class RepositoryQueryTest { Neo4jQueryMethod method = RepositoryQueryTest .neo4jQueryMethod("annotatedQueryWithValidTemplate", String.class, String.class); - StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(NodeManager.class), + StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(Neo4jClient.class), mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT, method); @@ -210,7 +210,7 @@ final class RepositoryQueryTest { .neo4jQueryMethod("findByDontDoThisInRealLiveNamed", org.neo4j.driver.types.Point.class, String.class, String.class); - StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(NodeManager.class), + StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(Neo4jClient.class), mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT, method); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java deleted file mode 100644 index 7b6abd1b2..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.util.Collections; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.data.neo4j.core.NodeManager; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.IdDescription; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.NodeDescription; - -/** - * @author Gerrit Meier - * @author Michael J. Simons - */ -class SimpleNeo4jRepositoryTest { - - private NodeManager nodeManager; - - private Neo4jMappingContext mappingContext; - - private NodeDescription nodeDescription; - - private SimpleNeo4jRepository repository; - - @BeforeEach - void setupMock() { - nodeManager = mock(NodeManager.class); - - nodeDescription = mock(NodeDescription.class); - when(nodeDescription.getPrimaryLabel()).thenReturn("TestNode"); - when(nodeDescription.getIdDescription()).thenReturn(new IdDescription()); - - mappingContext = mock(Neo4jMappingContext.class); - when(mappingContext.getRequiredNodeDescription(TestNode.class)).thenReturn(nodeDescription); - repository = new SimpleNeo4jRepository(this.nodeManager, this.mappingContext, TestNode.class); - } - - @Test - void saveNotImplemented() { - repository.save(null); // todo this should throw an exception upfront - verify(nodeManager).save(any()); - } - - @Test - void saveAllNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.saveAll(null)); - } - - @Test - void deleteByIdNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteById(null)); - } - - @Test - void deleteNotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.delete(null)); - } - - @Test - void deleteAll1NotImplemented() { - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> repository.deleteAll(Collections.emptyList())); - } - - @Node - class TestNode { - @Id - private Long id; - - private String value; - } -} From acea00e3dbe7abe72bddcbb3e76286381cc608d7 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 22 May 2019 09:47:57 +0200 Subject: [PATCH 095/342] Add support for delete, deleteAll and deleteById. We also decoupled Schema from Cypher again and added a common logging function to all statements ran through DefaultNeo4jClient. Co-authored-by: Gerrit Meier --- etc/jqassistant/structure.adoc | 4 +- .../data/neo4j/core/DefaultNeo4jClient.java | 86 ++++++++++++------- .../mapping/DefaultNeo4jPersistentEntity.java | 10 ++- .../core/mapping/Neo4jMappingContext.java | 25 ------ .../data/neo4j/core/schema/IdDescription.java | 15 +++- .../neo4j/core/schema/NodeDescription.java | 2 + .../data/neo4j/core/schema/Schema.java | 18 ---- .../repository/query/CypherAdapterUtils.java | 69 +++++++++++++++ .../repository/query/CypherQueryCreator.java | 8 +- .../support/SimpleNeo4jRepository.java | 79 ++++++++++++----- .../data/neo4j/integration/RepositoryIT.java | 34 ++++++++ 11 files changed, 245 insertions(+), 105 deletions(-) diff --git a/etc/jqassistant/structure.adoc b/etc/jqassistant/structure.adoc index cb113db04..495e34abc 100644 --- a/etc/jqassistant/structure.adoc +++ b/etc/jqassistant/structure.adoc @@ -5,12 +5,12 @@ Most of the time, the package structure under `org.springframework.data.neo4j` s [[structure:mapping]] [source,cypher,role=constraint,requiresConcepts="dependency:Package"] -.The mapping package must not depend on any other SDN-RX packages than `schema` and its transitive dependencies +.The mapping package must not depend on any other SDN-RX packages than `schema` ---- MATCH (a:Main:Artifact) MATCH (a) -[:CONTAINS]-> (s:Package) WHERE s.fqn = 'org.springframework.data.neo4j.core.schema' MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a) WHERE p1.fqn = 'org.springframework.data.neo4j.core.mapping' - AND NOT (p2 = s OR (s) -[:DEPENDS_ON]-> (p2) OR (p1) -[:CONTAINS]-> (p2)) + AND NOT (p2 = s OR (p1) -[:CONTAINS]-> (p2)) return p1,p2 ---- diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index 4e903448f..8bbfe1cfa 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -135,15 +135,51 @@ class DefaultNeo4jClient implements Neo4jClient { return new DefaultRunnableDelegation<>(callback); } - @RequiredArgsConstructor - class DefaultRunnableSpec implements RunnableSpec { + /** + * Basically a holder of a cypher template supplier and a set of named parameters. It's main purpose is to + * orchestrate the running of things with a bit of logging. + */ + class RunnableStatement { + + RunnableStatement(Supplier cypherSupplier) { + this(cypherSupplier, new NamedParameters()); + } + + RunnableStatement(Supplier cypherSupplier, NamedParameters parameters) { + this.cypherSupplier = cypherSupplier; + this.parameters = parameters; + } private final Supplier cypherSupplier; - private final NamedParameters parameters = new NamedParameters(); + private final NamedParameters parameters; + + protected final StatementResult runWith(AutoCloseableStatementRunner statementRunner) { + String statementTemplate = cypherSupplier.get(); + + if (cypherLog.isDebugEnabled()) { + cypherLog.debug("Executing:{}{}", System.lineSeparator(), statementTemplate); + + if (cypherLog.isTraceEnabled() && !parameters.isEmpty()) { + cypherLog.trace("with parameters:{}{}", System.lineSeparator(), parameters); + } + } + + StatementResult result = statementRunner.run(statementTemplate, parameters.get()); + return result; + } + } + + class DefaultRunnableSpec implements RunnableSpec { + + private RunnableStatement runnableStatement; private String targetDatabase; + DefaultRunnableSpec(Supplier cypherSupplier) { + this.runnableStatement = new RunnableStatement(cypherSupplier); + } + @Override public RunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) { this.targetDatabase = targetDatabase; @@ -158,7 +194,7 @@ class DefaultNeo4jClient implements Neo4jClient { @Override public RunnableSpecTightToDatabase to(String name) { - DefaultRunnableSpec.this.parameters.add(name, value); + DefaultRunnableSpec.this.runnableStatement.parameters.add(name, value); return DefaultRunnableSpec.this; } @@ -175,14 +211,14 @@ class DefaultNeo4jClient implements Neo4jClient { @Override public RunnableSpecTightToDatabase bindAll(Map newParameters) { - this.parameters.addAll(newParameters); + this.runnableStatement.parameters.addAll(newParameters); return this; } @Override public MappingSpec, Collection, T> fetchAs(Class targetClass) { - return new DefaultRecordFetchSpec(this.targetDatabase, this.cypherSupplier, this.parameters, + return new DefaultRecordFetchSpec(this.targetDatabase, this.runnableStatement, new SingleValueMappingFunction(targetClass)); } @@ -191,32 +227,35 @@ class DefaultNeo4jClient implements Neo4jClient { return new DefaultRecordFetchSpec<>( this.targetDatabase, - this.cypherSupplier, - this.parameters, (t, r) -> r.asMap()); + this.runnableStatement, (t, r) -> r.asMap()); } @Override public ResultSummary run() { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { - StatementResult result = statementRunner.run(cypherSupplier.get(), parameters.get()); + StatementResult result = runnableStatement.runWith(statementRunner); return result.consume(); } } } - @AllArgsConstructor class DefaultRecordFetchSpec implements RecordFetchSpec, Collection, T>, MappingSpec, Collection, T> { private final String targetDatabase; - private final Supplier cypherSupplier; - - private final NamedParameters parameters; + private final RunnableStatement runnableStatement; private BiFunction mappingFunction; + DefaultRecordFetchSpec(String targetDatabase, RunnableStatement runnableStatement, + BiFunction mappingFunction) { + this.targetDatabase = targetDatabase; + this.runnableStatement = runnableStatement; + this.mappingFunction = mappingFunction; + } + @Override public RecordFetchSpec, Collection, T> mappedBy( @SuppressWarnings("HiddenField") BiFunction mappingFunction) { @@ -229,7 +268,7 @@ class DefaultNeo4jClient implements Neo4jClient { public Optional one() { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { - StatementResult result = runWith(statementRunner); + StatementResult result = runnableStatement.runWith(statementRunner); return result.hasNext() ? Optional.of(mappingFunction.apply(typeSystem, result.single())) : Optional.empty(); @@ -240,7 +279,7 @@ class DefaultNeo4jClient implements Neo4jClient { public Optional first() { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { - StatementResult result = runWith(statementRunner); + StatementResult result = runnableStatement.runWith(statementRunner); return result.stream().map(partialMappingFunction(typeSystem)).findFirst(); } } @@ -249,26 +288,11 @@ class DefaultNeo4jClient implements Neo4jClient { public Collection all() { try (AutoCloseableStatementRunner statementRunner = getStatementRunner(this.targetDatabase)) { - StatementResult result = runWith(statementRunner); + StatementResult result = runnableStatement.runWith(statementRunner); return result.stream().map(partialMappingFunction(typeSystem)).collect(toList()); } } - private StatementResult runWith(AutoCloseableStatementRunner statementRunner) { - String statementTemplate = cypherSupplier.get(); - - if (cypherLog.isDebugEnabled()) { - cypherLog.debug("Executing:{}{}", System.lineSeparator(), statementTemplate); - - if (cypherLog.isTraceEnabled() && !parameters.isEmpty()) { - cypherLog.trace("with parameters:{}{}", System.lineSeparator(), parameters); - } - } - - StatementResult result = statementRunner.run(statementTemplate, parameters.get()); - return result; - } - /** * @param typeSystem The actual type system * @return The partially evaluated mapping function diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index 751a709ca..7acbed270 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Optional; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.Id; @@ -85,6 +86,12 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity propertyAccessor = this.getPropertyAccessor(instance); + return propertyAccessor.getProperty(this.getRequiredIdProperty()); + } + @Override public void verify() { @@ -117,7 +124,8 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity new IdDescription()); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 5b956047b..ea566c4ec 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -18,8 +18,6 @@ */ package org.springframework.data.neo4j.core.mapping; -import static org.springframework.data.neo4j.core.schema.NodeDescription.*; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -42,15 +40,6 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.neo4j.core.cypher.Condition; -import org.springframework.data.neo4j.core.cypher.Conditions; -import org.springframework.data.neo4j.core.cypher.Cypher; -import org.springframework.data.neo4j.core.cypher.Expression; -import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.Node; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Relationship; @@ -175,20 +164,6 @@ public class Neo4jMappingContext .map(neo4jPersistentEntity -> new DefaultNeo4jMappingFunction<>(instantiators, neo4jPersistentEntity)); } - @Override - public OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition) { - Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); - IdDescription idDescription = nodeDescription.getIdDescription(); - - List expressions = new ArrayList<>(); - expressions.add(rootNode); - if (idDescription.getIdStrategy() == Id.Strategy.INTERNAL) { - expressions.add(Functions.id(rootNode).as(NAME_OF_INTERNAL_ID)); - } - return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())) - .with(expressions.toArray(new Expression[expressions.size()])); - } - private Collection computeRelationshipsOf(String primaryLabel) { NodeDescription nodeDescription = getRequiredNodeDescription(primaryLabel); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java index f6954edbe..dac803d6b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java @@ -45,17 +45,17 @@ public final class IdDescription { /** * The property that stores the id if applicable. */ - private @Nullable final GraphPropertyDescription graphProperty; + private @Nullable final String graphPropertyName; public IdDescription() { this(Id.Strategy.INTERNAL, null, null); } public IdDescription(Id.Strategy idStrategy, - @Nullable Class idGeneratorClass, @Nullable GraphPropertyDescription graphProperty) { + @Nullable Class idGeneratorClass, @Nullable String graphPropertyName) { this.idStrategy = idStrategy; this.idGeneratorClass = idGeneratorClass; - this.graphProperty = graphProperty; + this.graphPropertyName = graphPropertyName; } public Id.Strategy getIdStrategy() { @@ -66,7 +66,14 @@ public final class IdDescription { return Optional.ofNullable(idGeneratorClass); } + /** + * An ID description has only a corresponding graph property name when it's based on either {@link Id.Strategy#ASSIGNED} + * or {@link Id.Strategy#GENERATED}. An internal id has no corresponding graph property and therefor this method + * will return an empty {@link Optional} in such cases. + * + * @return The name of an optional graph property. + */ public Optional getOptionalGraphPropertyName() { - return Optional.ofNullable(graphProperty).map(GraphPropertyDescription::getPropertyName); + return Optional.ofNullable(graphPropertyName); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java index 710e7993c..6b6d55d21 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -71,4 +71,6 @@ public interface NodeDescription { default boolean useInternalIds() { return this.getIdDescription().getIdStrategy() == Id.Strategy.INTERNAL; } + + Object extractId(T entity); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java index bbd8777c9..0e2875601 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -26,9 +26,6 @@ import java.util.function.BiFunction; import org.apiguardian.api.API; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.cypher.Condition; -import org.springframework.data.neo4j.core.cypher.Cypher; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; /** * Contains the descriptions of all nodes, their properties and relationships known to SDN-RX. @@ -112,19 +109,4 @@ public interface Schema { return getMappingFunctionFor(targetClass).orElseThrow(() -> new UnknownEntityException(targetClass)); } - - /** - * This will create a match statement that fits the given node description and may contains additional conditions. - * The {@code WITH} clause of this statement contains all nodes and relationships necessary to map a record to - * the given {@code nodeDescription}. - *

- * It is recommended to use {@link Cypher#asterisk()} to return everything from the query in the end. - *

- * The root node is guaranted to have the symbolic name {@code n}. - * - * @param nodeDescription The node description for which a match clause should be generated - * @param condition Optional conditions to add - * @return An ongoing match - */ - OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java index 74f19e4d5..91bab6def 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java @@ -19,16 +19,30 @@ package org.springframework.data.neo4j.repository.query; import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.core.schema.NodeDescription.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import java.util.function.Function; import org.apiguardian.api.API; import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.cypher.Condition; +import org.springframework.data.neo4j.core.cypher.Conditions; import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Expression; +import org.springframework.data.neo4j.core.cypher.Functions; +import org.springframework.data.neo4j.core.cypher.Node; import org.springframework.data.neo4j.core.cypher.SortItem; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndDelete; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; import org.springframework.data.neo4j.core.cypher.SymbolicName; import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.Schema; /** * A set of shared utils to adapt {@code org.springframework.data.neo4j.core.cypher} functionality with Spring Data @@ -78,6 +92,61 @@ public final class CypherAdapterUtils { return sort.stream().map(sortAdapterFor(nodeDescription)).toArray(SortItem[]::new); } + /** + * Provides a builder for statements based on the schema of entity classes. + * + * @param schema The schema used for creating queries. + * @return + */ + public static SchemaBasedStatementBuilder createSchemaBasedStatementBuilder(Schema schema) { + return new SchemaBasedStatementBuilder(schema); + } + + /** + * This is a adapter between the Schema and the Cypher module. + */ + public static class SchemaBasedStatementBuilder { + + private final Schema schema; + + private SchemaBasedStatementBuilder(Schema schema) { + this.schema = schema; + } + + /** + * This will create a match statement that fits the given node description and may contains additional conditions. + * The {@code WITH} clause of this statement contains all nodes and relationships necessary to map a record to + * the given {@code nodeDescription}. + *

+ * It is recommended to use {@link Cypher#asterisk()} to return everything from the query in the end. + *

+ * The root node is guaranted to have the symbolic name {@code n}. + * + * @param nodeDescription The node description for which a match clause should be generated + * @param condition Optional conditions to add + * @return An ongoing match + */ + public OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition) { + Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); + IdDescription idDescription = nodeDescription.getIdDescription(); + + List expressions = new ArrayList<>(); + expressions.add(rootNode); + if (idDescription.getIdStrategy() == Id.Strategy.INTERNAL) { + expressions.add(Functions.id(rootNode).as(NAME_OF_INTERNAL_ID)); + } + return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())) + .with(expressions.toArray(new Expression[expressions.size()])); + } + + public OngoingMatchAndDelete prepareDeleteOf(NodeDescription nodeDescription, + Optional condition) { + + Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); + return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())).delete(rootNode); + } + } + private CypherAdapterUtils() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 2efbbc50a..f71afe07e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -73,10 +73,11 @@ import org.springframework.data.repository.query.parser.PartTree; final class CypherQueryCreator extends AbstractQueryCreator { private final Neo4jMappingContext mappingContext; + private final Class domainType; private final NodeDescription nodeDescription; - private Iterator formalParameters; + private final Iterator formalParameters; private final Queue lastParameter = new LinkedList<>(); /** @@ -93,10 +94,11 @@ final class CypherQueryCreator extends AbstractQueryCreator { Parameters formalParameters, ParameterAccessor actualParameters ) { super(tree, actualParameters); - this.mappingContext = mappingContext; + this.domainType = domainType; this.nodeDescription = this.mappingContext.getRequiredNodeDescription(this.domainType); + this.formalParameters = formalParameters.iterator(); this.maxResults = tree.isLimiting() ? tree.getMaxResults() : null; } @@ -124,7 +126,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { @Override protected String complete(Condition condition, Sort sort) { - Statement statement = mappingContext + Statement statement = createSchemaBasedStatementBuilder(mappingContext) .prepareMatchOf(nodeDescription, Optional.ofNullable(condition)) .returning(Cypher.asterisk()) .orderBy( diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 1a3263468..0e7209320 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -19,7 +19,6 @@ package org.springframework.data.neo4j.repository.support; import static java.util.Collections.*; -import static java.util.stream.Collectors.*; import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; import static org.springframework.data.neo4j.core.schema.NodeDescription.*; @@ -36,8 +35,11 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; import java.util.function.LongSupplier; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.neo4j.driver.Record; +import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; @@ -87,9 +89,12 @@ class SimpleNeo4jRepository implements Neo4jRepository { private final Class nodeClass; - private final NodeDescription nodeDescription; + private final NodeDescription nodeDescription; private final BiFunction mappingFunction; - private Expression idExpression; + + private final SchemaBasedStatementBuilder statementBuilder; + + private final Expression idExpression; SimpleNeo4jRepository(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, Class nodeClass) { @@ -97,9 +102,11 @@ class SimpleNeo4jRepository implements Neo4jRepository { this.mappingContext = mappingContext; this.nodeClass = nodeClass; - this.nodeDescription = mappingContext.getRequiredNodeDescription(nodeClass); + this.nodeDescription = (NodeDescription) mappingContext.getRequiredNodeDescription(nodeClass); this.mappingFunction = mappingContext.getRequiredMappingFunctionFor(nodeClass); + this.statementBuilder = createSchemaBasedStatementBuilder(mappingContext); + final SymbolicName rootNode = Cypher.symbolicName("n"); final IdDescription idDescription = this.nodeDescription.getIdDescription(); switch (idDescription.getIdStrategy()) { @@ -119,7 +126,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Iterable findAll(Sort sort) { - Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) .returning(asterisk()) .orderBy(toSortItems(nodeDescription, sort)) .build(); @@ -130,7 +137,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Page findAll(Pageable pageable) { - StatementBuilder.OngoingMatchAndReturn returning = mappingContext + StatementBuilder.OngoingMatchAndReturn returning = statementBuilder .prepareMatchOf(nodeDescription, Optional.empty()).returning(asterisk()); StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); @@ -146,7 +153,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { public Page findAll(Example example, Pageable pageable) { Predicate predicate = createPredicate(example); - StatementBuilder.OngoingMatchAndReturn returning = mappingContext + StatementBuilder.OngoingMatchAndReturn returning = statementBuilder .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)).returning(asterisk()); StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); @@ -185,7 +192,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Optional findById(ID id) { - Statement statement = mappingContext + Statement statement = statementBuilder .prepareMatchOf(nodeDescription, Optional.of(idExpression.isEqualTo(literalOf(id)))) .returning(asterisk()) .build(); @@ -200,7 +207,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Iterable findAll() { - Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) .returning(asterisk()).build(); return createExecutableQuery(statement).getResults(); } @@ -208,7 +215,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Iterable findAllById(Iterable ids) { - Statement statement = mappingContext + Statement statement = statementBuilder .prepareMatchOf(nodeDescription, Optional.of(idExpression.in((parameter("ids"))))) .returning(asterisk()) .build(); @@ -219,7 +226,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public long count() { - Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) .returning(Functions.count(asterisk())).build(); return createExecutableQuery(Long.class, statement, Collections.emptyMap()).getRequiredSingleResult(); @@ -228,36 +235,65 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override @Transactional public void deleteById(ID id) { - throw new UnsupportedOperationException("Not there yet."); + + String nameOfParameter = "id"; + Condition condition = idExpression.isEqualTo(parameter(nameOfParameter)); + + log.debug("Deleting entity with id {} ", id); + + Statement statement = statementBuilder.prepareDeleteOf(nodeDescription, Optional.of(condition)).build(); + ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)) + .bind(id).to(nameOfParameter) + .run(); + + log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); } @Override @Transactional public void delete(T entity) { - throw new UnsupportedOperationException("Not there yet."); + + ID id = (ID) this.nodeDescription.extractId(entity); + this.deleteById(id); } @Override @Transactional public void deleteAll(Iterable entities) { - throw new UnsupportedOperationException("Not there yet."); + String nameOfParameter = "ids"; + Condition condition = idExpression.in(parameter(nameOfParameter)); + + List ids = StreamSupport.stream(entities.spliterator(), false) + .map(this.nodeDescription::extractId).collect(Collectors.toList()); + + log.debug("Deleting all entities with the following ids: {} ", ids); + + Statement statement = statementBuilder.prepareDeleteOf(nodeDescription, Optional.of(condition)).build(); + ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)) + .bind(ids).to(nameOfParameter) + .run(); + + log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); } @Override @Transactional public void deleteAll() { - for (T element : findAll()) { - delete(element); - } + log.debug("Deleting all nodes with primary label {}", nodeDescription.getPrimaryLabel()); + + Statement statement = statementBuilder.prepareDeleteOf(nodeDescription, Optional.empty()).build(); + ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)).run(); + + log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); } @Override public Optional findOne(Example example) { Predicate predicate = createPredicate(example); - Statement statement = mappingContext + Statement statement = statementBuilder .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(asterisk()) .build(); @@ -269,7 +305,8 @@ class SimpleNeo4jRepository implements Neo4jRepository { public Iterable findAll(Example example) { Predicate predicate = createPredicate(example); - Statement statement = mappingContext.prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) + Statement statement = statementBuilder + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(asterisk()) .build(); @@ -280,7 +317,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { public Iterable findAll(Example example, Sort sort) { Predicate predicate = createPredicate(example); - Statement statement = mappingContext + Statement statement = statementBuilder .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(asterisk()) .orderBy(toSortItems(nodeDescription, sort)).build(); @@ -292,7 +329,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { public long count(Example example) { Predicate predicate = createPredicate(example); - Statement statement = mappingContext + Statement statement = statementBuilder .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) .returning(Functions.count(asterisk())) .build(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index bf523c328..9411a3a20 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -157,6 +157,40 @@ class RepositoryIT { assertThat(exists).isTrue(); } + @Test + void delete() { + + repository.delete(person1); + + assertThat(repository.existsById(id1)).isFalse(); + assertThat(repository.existsById(id2)).isTrue(); + } + + @Test + void deleteById() { + + repository.deleteById(id1); + + assertThat(repository.existsById(id1)).isFalse(); + assertThat(repository.existsById(id2)).isTrue(); + } + + @Test + void deleteAllEntities() { + + repository.deleteAll(Arrays.asList(person1, person2)); + + assertThat(repository.existsById(id1)).isFalse(); + assertThat(repository.existsById(id2)).isFalse(); + } + + @Test + void deleteAll() { + + repository.deleteAll(); + assertThat(repository.count()).isEqualTo(0L); + } + @Test void findAllById() { Iterable persons = repository.findAllById(Arrays.asList(id1, id2)); From 521c69fc34c6e61f6024ebf8207502edd6f69087 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 22 May 2019 15:49:38 +0200 Subject: [PATCH 096/342] Enable reactive repository functionality. for - Derived finder methods - String based queries via Query annotation - SimpleReactiveNeo4jRepository --- .../config/AbstractReactiveNeo4jConfig.java | 48 +++ .../core/DefaultReactiveNeo4jClient.java | 2 +- .../EnableReactiveNeo4jRepositories.java | 111 +++++ .../ReactiveNeo4jRepositoriesRegistrar.java | 54 +++ ...Neo4jRepositoryConfigurationExtension.java | 102 +++++ .../query/AbstractReactiveNeo4jQuery.java | 145 +++++++ .../repository/query/Neo4jQueryExecution.java | 20 + .../ReactiveNeo4jQueryLookupStrategy.java | 68 +++ .../query/ReactivePartTreeNeo4jQuery.java | 208 +++++++++ .../query/ReactiveStringBasedNeo4jQuery.java | 229 ++++++++++ .../support/DefaultExecutableQuery.java | 4 +- .../support/ExecutableReactiveQuery.java | 66 +++ .../support/ReactiveExecutableQuery.java | 50 +++ .../ReactiveNeo4jRepositoryFactory.java | 77 ++++ .../ReactiveNeo4jRepositoryFactoryBean.java | 64 +++ .../SimpleReactiveNeo4jRepository.java | 401 ++++++++++++++++++ .../{ => blocking}/PersonRepository.java | 2 +- .../blocking/PersonWithAllConstructor.java | 68 +++ .../blocking/PersonWithNoConstructor.java | 41 ++ .../blocking/PersonWithWither.java | 50 +++ .../{ => blocking}/RepositoryIT.java | 2 +- .../integration/blocking/ThingRepository.java | 32 ++ .../blocking/ThingWithAssignedId.java | 54 +++ .../{ => rx}/PersonWithAllConstructor.java | 2 +- .../{ => rx}/PersonWithNoConstructor.java | 2 +- .../{ => rx}/PersonWithWither.java | 2 +- .../rx/ReactivePersonRepository.java | 50 +++ .../integration/rx/ReactiveRepositoryIT.java | 330 ++++++++++++++ .../integration/{ => rx}/ThingRepository.java | 2 +- .../{ => rx}/ThingWithAssignedId.java | 2 +- .../data/neo4j/test/Neo4jExtension.java | 4 +- .../{ => blocking}/KotlinPerson.kt | 2 +- 32 files changed, 2282 insertions(+), 12 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveExecutableQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{ => blocking}/PersonRepository.java (99%) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithAllConstructor.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithNoConstructor.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithWither.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{ => blocking}/RepositoryIT.java (99%) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingRepository.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingWithAssignedId.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{ => rx}/PersonWithAllConstructor.java (96%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{ => rx}/PersonWithNoConstructor.java (95%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{ => rx}/PersonWithWither.java (95%) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactivePersonRepository.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactiveRepositoryIT.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{ => rx}/ThingRepository.java (94%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{ => rx}/ThingWithAssignedId.java (95%) rename spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/{ => blocking}/KotlinPerson.kt (93%) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java new file mode 100644 index 000000000..2ab9b6ba0 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.config; + +import org.apiguardian.api.API; +import org.neo4j.driver.Driver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; + +/** + * Base class for reactive SDN-RX configuration using JavaConfig. + * This can be included in all scenarios in which Spring Boot is not an option. + * + * @author Gerrit Meier + * @since 1.0 + */ +@Configuration +@API(status = API.Status.STABLE, since = "1.0") +public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupport { + + /** + * The driver used here should be the driver resulting from {@link #driver()}, which is the default. + * + * @param driver The driver to connect with. + * @return A reactive Neo4j client. + */ + @Bean + public ReactiveNeo4jClient neo4jClient(Driver driver) { + return ReactiveNeo4jClient.create(driver); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 5fab5eb5a..4c83d9047 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -231,7 +231,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return Mono.usingWhen( getStatementRunner(targetDatabase), - runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).single(), + runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).singleOrEmpty(), DefaultReactiveNeo4jClient::asyncComplete, DefaultReactiveNeo4jClient::asyncError, DefaultReactiveNeo4jClient::asyncCancel diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java new file mode 100644 index 000000000..a51c85fd5 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.config; + +import static org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension.*; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.neo4j.repository.support.ReactiveNeo4jRepositoryFactoryBean; + +/** + * Annotation to activate reactive Neo4j repositories. If no base package is configured through either {@link #value()}, + * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated + * configuration class. + * + * @author Gerrit Meier + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(ReactiveNeo4jRepositoriesRegistrar.class) +public @interface EnableReactiveNeo4jRepositories { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: + * {@code @EnableNeo4jRepositories("org.my.pkg")} instead of + * {@code @EnableNeo4jRepositories(basePackages="org.my.pkg")}. + */ + @AliasFor("basePackages") + String[] value() default {}; + + /** + * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this + * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + */ + @AliasFor("value") + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The + * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in + * each package that serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + + /** + * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to + * {@link ReactiveNeo4jRepositoryFactoryBean}. + */ + Class repositoryFactoryBeanClass() default ReactiveNeo4jRepositoryFactoryBean.class; + + /** + * Configures the name of the {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected. + */ + String neo4jMappingContextRef() default DEFAULT_MAPPING_CONTEXT_BEAN_NAME; + + /** + * Configures the name of the {@link org.springframework.data.neo4j.core.Neo4jClient} bean to be used with the repositories detected. + */ + String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_NAME; + + /** + * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from + * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters. + */ + ComponentScan.Filter[] includeFilters() default {}; + + /** + * Specifies which types are not eligible for component scanning. + */ + ComponentScan.Filter[] excludeFilters() default {}; + + /** + * Configures the location of where to find the Spring Data named queries properties file. Will default to + * {@code META-INFO/neo4j-named-queries.properties}. + */ + String namedQueriesLocation() default ""; + + /** + * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So + * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning + * for {@code PersonRepositoryImpl}. + */ + String repositoryImplementationPostfix() default "Impl"; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java new file mode 100644 index 000000000..04a2cfb0f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.config; + +import java.lang.annotation.Annotation; + +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +/** + * {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableReactiveNeo4jRepositories} annotation. + * The {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of Spring's + * {@code org.springframework.context.annotation.ImportBeanDefinitionRegistrar}, a dedicated SPI to register beans + * during processing of configuration classes. + * + * @author Gerrit Meier + * @author Michael J. Simons + */ +class ReactiveNeo4jRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableReactiveNeo4jRepositories.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension() + */ + @Override + protected RepositoryConfigurationExtension getExtension() { + return new ReactiveNeo4jRepositoryConfigurationExtension(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java new file mode 100644 index 000000000..bfbb942f8 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.config; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.data.repository.core.RepositoryMetadata; + +/** + * This dedicated Neo4j repository extension will be registered via {@link Neo4jRepositoriesRegistrar} and then provide + * all necessary beans to be registered in the application's context before the user's "business" beans gets registered. + *

+ * While it is public, it is mainly used for internal API respectively for Spring Boots automatic configuration. + * + * @author Michael J. Simons + * @author Gerrit Meier + */ +public class ReactiveNeo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + private static final String MODULE_PREFIX = "neo4j"; + + /** + * See {@link AbstractBeanDefinition#INFER_METHOD}. + */ + static final String DEFAULT_NEO4J_CLIENT_NAME = "neo4jClient"; + + /** + * See {@link AbstractBeanDefinition#INFER_METHOD}. + */ + static final String DEFAULT_MAPPING_CONTEXT_BEAN_NAME = "neo4jMappingContext"; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config14.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() + */ + @Override + public String getRepositoryFactoryBeanClassName() { + return Neo4jRepositoryFactoryBean.class.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config14.RepositoryConfigurationExtensionSupport#getModulePrefix() + */ + @Override + protected String getModulePrefix() { + return MODULE_PREFIX; + } + + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.singleton(Node.class); + } + + @Override + protected Collection> getIdentifyingTypes() { + return Collections.singleton(Neo4jRepository.class); + } + + @Override + protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) + */ + @Override + public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { + + builder.addPropertyReference("neo4jClient", + source.getAttribute("neo4jClientRef").orElse(DEFAULT_NEO4J_CLIENT_NAME)); + builder.addPropertyReference("neo4jMappingContext", + source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java new file mode 100644 index 000000000..773de4f68 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.domain.Range; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.util.Assert; + +/** + * Base class for {@link RepositoryQuery} implementations for Neo4j. + * + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +abstract class AbstractReactiveNeo4jQuery implements RepositoryQuery { + + final ReactiveNeo4jClient neo4jClient; + final Neo4jMappingContext mappingContext; + final Neo4jQueryMethod queryMethod; + final Class domainType; + + AbstractReactiveNeo4jQuery(ReactiveNeo4jClient neo4jClient, + Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod) { + + Assert.notNull(neo4jClient, "The Neo4j client is required."); + Assert.notNull(mappingContext, "The mapping context is required."); + Assert.notNull(queryMethod, "Query method must not be null!"); + + this.neo4jClient = neo4jClient; + this.mappingContext = mappingContext; + this.queryMethod = queryMethod; + this.domainType = queryMethod.getReturnedObjectType(); + } + + @Override + public QueryMethod getQueryMethod() { + return this.queryMethod; + } + + @Override + public final Object execute(Object[] parameters) { + return new Neo4jQueryExecution.ReactiveQueryExecution(neo4jClient) + .execute(prepareQuery(parameters), queryMethod.isCollectionQuery()); + } + + protected abstract PreparedQuery prepareQuery(Object[] parameters); + + /** + * + * @return True if the query shout get a count projection applied. + */ + protected abstract boolean isCountQuery(); + + /** + * @return True if the query should get an exists projection applied. + */ + protected abstract boolean isExistsQuery(); + + /** + * @return True if the query should delete matching nodes. + */ + protected abstract boolean isDeleteQuery(); + + /** + * @return True if the query has an explicit limit set. + */ + protected abstract boolean isLimiting(); + + /** + * Converts parameter as needed by the query generated, which is not covered by standard conversion services. + * + * @param parameter The parameter to fit into the generated query. + * @return A parameter that fits the place holders of a generated query + */ + final Object convertParameter(Object parameter) { + if (parameter instanceof Range) { + return convertRange((Range) parameter); + } else if (parameter instanceof Distance) { + return calculateDistanceInMeter((Distance) parameter); + } else if (parameter instanceof Circle) { + return convertCircle((Circle) parameter); + } + + // Good hook to check the NodeManager whether the thing is an entity and we replace the value with a known id. + + return parameter; + } + + private Map convertRange(Range range) { + Map map = new HashMap<>(); + range.getLowerBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("lb", v)); + range.getUpperBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("ub", v)); + return map; + } + + private Map convertCircle(Circle circle) { + Map map = new HashMap<>(); + map.put("x", convertParameter(circle.getCenter().getX())); + map.put("y", convertParameter(circle.getCenter().getY())); + map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); + return map; + } + + private static double calculateDistanceInMeter(Distance distance) { + + if (distance.getMetric() == Metrics.KILOMETERS) { + double kilometersDivisor = 0.001d; + return distance.getValue() / kilometersDivisor; + + } else if (distance.getMetric() == Metrics.MILES) { + double milesDivisor = 0.00062137d; + return distance.getValue() / milesDivisor; + + } else { + return distance.getValue(); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java index 5eb5d1215..5c6dfb6d7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java @@ -21,7 +21,9 @@ package org.springframework.data.neo4j.repository.query; import lombok.RequiredArgsConstructor; import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.repository.support.ExecutableQuery; +import org.springframework.data.neo4j.repository.support.ExecutableReactiveQuery; import org.springframework.data.neo4j.repository.support.PreparedQuery; /** @@ -30,6 +32,7 @@ import org.springframework.data.neo4j.repository.support.PreparedQuery; * flavors. * * @author Michael J. Simons + * @author Gerrit Meier * @since 1.0 */ @FunctionalInterface @@ -53,4 +56,21 @@ interface Neo4jQueryExecution { } } } + + @RequiredArgsConstructor + class ReactiveQueryExecution implements Neo4jQueryExecution { + + private final ReactiveNeo4jClient neo4jClient; + + @Override + public Object execute(PreparedQuery description, boolean asCollectionQuery) { + + ExecutableReactiveQuery executableQuery = ExecutableReactiveQuery.create(description, neo4jClient); + if (asCollectionQuery) { + return executableQuery.getResults(); + } else { + return executableQuery.getSingleResult(); + } + } + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java new file mode 100644 index 000000000..3e2cd8e89 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import lombok.RequiredArgsConstructor; + +import java.lang.reflect.Method; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; + +/** + * Lookup strategy for queries. This is the internal api of the {@code query package}. + * + * @author Gerrit Meier + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +@RequiredArgsConstructor +public final class ReactiveNeo4jQueryLookupStrategy implements QueryLookupStrategy { + + private final ReactiveNeo4jClient neo4jClient; + private final Neo4jMappingContext mappingContext; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) + */ + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + NamedQueries namedQueries) { + + Neo4jQueryMethod queryMethod = new Neo4jQueryMethod(method, metadata, factory); + String namedQueryName = queryMethod.getNamedQueryName(); + + if (namedQueries.hasQuery(namedQueryName)) { + return ReactiveStringBasedNeo4jQuery.create(neo4jClient, mappingContext, evaluationContextProvider, queryMethod, + namedQueries.getQuery(namedQueryName)); + } else if (queryMethod.hasQueryAnnotation()) { + return ReactiveStringBasedNeo4jQuery.create(neo4jClient, mappingContext, evaluationContextProvider, queryMethod); + } else { + return new ReactivePartTreeNeo4jQuery(neo4jClient, mappingContext, queryMethod); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java new file mode 100644 index 000000000..f32f624a0 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import static java.util.stream.Collectors.*; + +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.neo4j.driver.types.Point; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.repository.query.parser.PartTree.OrPart; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.util.Assert; + +/** + * Implementation of {@link RepositoryQuery} for derived finder methods. + * + * @author Gerrit Meier + * @since 1.0 + */ +@Slf4j +final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { + + /** + * A set of the temporal types that are directly passable to the driver and support a meaningful comparision in a + * temporal sense (after, before). + * See > COMPARABLE_TEMPORAL_TYPES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(LocalDate.class, OffsetTime.class, ZonedDateTime.class, + LocalDateTime.class))); + + private static final EnumSet TYPES_SUPPORTING_CASE_INSENSITIVITY = EnumSet + .of(Part.Type.CONTAINING, Part.Type.ENDING_WITH, Part.Type.LIKE, Part.Type.NEGATING_SIMPLE_PROPERTY, + Part.Type.NOT_CONTAINING, + Part.Type.NOT_LIKE, Part.Type.SIMPLE_PROPERTY, Part.Type.STARTING_WITH); + + private final PartTree tree; + + ReactivePartTreeNeo4jQuery( + ReactiveNeo4jClient neo4jClient, + Neo4jMappingContext mappingContext, + Neo4jQueryMethod queryMethod + ) { + super(neo4jClient, mappingContext, queryMethod); + + this.tree = new PartTree(queryMethod.getName(), domainType); + + // Validate parts. Sort properties will be validated by Spring Data already. + PartValidator validator = new PartValidator(queryMethod); + this.tree.flatMap(OrPart::stream).forEach(validator::validatePart); + } + + @Override + protected PreparedQuery prepareQuery(Object[] parameters) { + + Neo4jParameters formalParameters = (Neo4jParameters) this.queryMethod.getParameters(); + ParameterAccessor actualParameters = new ParametersParameterAccessor(formalParameters, parameters); + CypherQueryCreator queryCreator = new CypherQueryCreator( + mappingContext, domainType, tree, formalParameters, actualParameters + ); + + String cypherQuery = queryCreator.createQuery(); + Map boundedParameters = formalParameters + .getBindableParameters().stream() + .collect(toMap(Neo4jQueryMethod.Neo4jParameter::getNameOrIndex, + formalParameter -> convertParameter(parameters[formalParameter.getIndex()]))); + + return PreparedQuery.queryFor(super.domainType).withCypherQuery(cypherQuery) + .withParameters(boundedParameters) + .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) + .build(); + } + + /** + * Checks whether the given part can be queried without case sensitivity. + * + * @param part + * @return True when {@code part} can be queried case insensitive. + */ + static boolean canIgnoreCase(Part part) { + return part.getProperty().getLeafType() == String.class && TYPES_SUPPORTING_CASE_INSENSITIVITY + .contains(part.getType()); + } + + @Override + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + @Override + protected boolean isLimiting() { + return tree.isLimiting(); + } + + static class PartValidator { + + private final Neo4jQueryMethod queryMethod; + + PartValidator(Neo4jQueryMethod queryMethod) { + this.queryMethod = queryMethod; + } + + void validatePart(Part part) { + + validateIgnoreCase(part); + switch (part.getType()) { + case AFTER: + case BEFORE: + validateTemporalProperty(part); + break; + case IS_EMPTY: + case IS_NOT_EMPTY: + validateCollectionProperty(part); + break; + case NEAR: + case WITHIN: + validatePointProperty(part); + break; + } + } + + private void validateIgnoreCase(Part part) { + + Assert.state(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), + () -> String.format( + "Can not derive query for '%s': Only the case of String based properties can be ignored within the following keywords: %s", + queryMethod, + formatTypes(TYPES_SUPPORTING_CASE_INSENSITIVITY))); + } + + private void validateTemporalProperty(Part part) { + + Assert.state(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String + .format( + "Can not derive query for '%s': The keywords %s work only with properties with one of the following types: %s", + queryMethod, formatTypes(Collections.singletonList(part.getType())), + COMPARABLE_TEMPORAL_TYPES)); + } + + private void validateCollectionProperty(Part part) { + + Assert.state(part.getProperty().getLeafProperty().isCollection(), () -> String + .format("Can not derive query for '%s': The keywords %s work only with collection properties", + queryMethod, + formatTypes(Collections.singletonList(part.getType())))); + } + + private void validatePointProperty(Part part) { + + Assert.state(ClassTypeInformation.from(Point.class) + .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), () -> String + .format("Can not derive query for '%s': %s works only with spatial properties", queryMethod, + part.getType())); + } + + private static String formatTypes(Collection types) { + return types.stream().flatMap(t -> t.getKeywords().stream()).collect(joining(", ", "[", "]")); + } + + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java new file mode 100644 index 000000000..fbf7512a8 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.query; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.springframework.data.mapping.MappingException; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.SpelEvaluator; +import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Implementation of {@link RepositoryQuery} for query methods annotated with {@link Query @Query}. + * + * + * The flow to handle queries with SpEL parameters is as follows + *

    + *
  1. Parse template as something that has SpEL-expressions in it
  2. + *
  3. Replace the SpEL-expressions with Neo4j Statement template parameters
  4. + *
  5. The parameters passed here _and_ the values that might have been computed during SpEL-parsing
  6. + *
+ * The main ingredient is a SpelEvaluator, that parses a template and replaces SpEL expressions + * with real Neo4j parameters. + * + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { + + /** + * Used for extracting SpEL expressions inside Cypher query templates. + */ + static final SpelQueryContext SPEL_QUERY_CONTEXT = SpelQueryContext + .of(ReactiveStringBasedNeo4jQuery::parameterNameSource, ReactiveStringBasedNeo4jQuery::replacementSource); + + /** + * Is this a count projection? + */ + private final boolean countQuery; + + /** + * Is this an exists projection? + */ + private final boolean existsQuery; + + /** + * Is this a modifying delete query? + */ + private final boolean deleteQuery; + + /** + * Used to evaluate the expression found while parsing the cypher template of this query against the actual parameters + * with the help of the formal parameters during the building of the {@link PreparedQuery}. + */ + private final SpelEvaluator spelEvaluator; + + /** + * The Cypher string used for this query. The cypher query will not be changed after parsed via {@link #SPEL_QUERY_CONTEXT}. + * All SpEL expressions will be substituted via "native" parameter placeholders. This will be done via the {@link #spelEvaluator}. + */ + private final String cypherQuery; + + /** + * Create a {@link ReactiveStringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The annotation + * is expected to have a value. + * + * @param neo4jClient + * @param mappingContext + * @param evaluationContextProvider + * @param queryMethod + * @return A new instance of a String based Neo4j query. + */ + static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext, + QueryMethodEvaluationContextProvider evaluationContextProvider, + Neo4jQueryMethod queryMethod) { + + Query queryAnnotation = queryMethod.getQueryAnnotation() + .orElseThrow(() -> new MappingException("Expected @Query annotation on the query method!")); + + String cypherTemplate = Optional.ofNullable(queryAnnotation.value()) + .filter(StringUtils::hasText) + .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not.")); + + return new ReactiveStringBasedNeo4jQuery(neo4jClient, mappingContext, evaluationContextProvider, queryMethod, + cypherTemplate, queryAnnotation.count(), queryAnnotation.exists(), queryAnnotation.delete()); + } + + /** + * Create a {@link ReactiveStringBasedNeo4jQuery} based on an explicit Cypher template. + * + * @param neo4jClient + * @param mappingContext + * @param evaluationContextProvider + * @param queryMethod + * @param cypherTemplate The template to use. + * @return A new instance of a String based Neo4j query. + */ + static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext, + QueryMethodEvaluationContextProvider evaluationContextProvider, + Neo4jQueryMethod queryMethod, String cypherTemplate) { + + Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template."); + + return new ReactiveStringBasedNeo4jQuery(neo4jClient, mappingContext, evaluationContextProvider, queryMethod, + cypherTemplate, false, false, false); + } + + private ReactiveStringBasedNeo4jQuery(ReactiveNeo4jClient neo4jClient, + Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider, + Neo4jQueryMethod queryMethod, String cypherTemplate, boolean countQuery, + boolean existsQuery, boolean deleteQuery) { + + super(neo4jClient, mappingContext, queryMethod); + + this.countQuery = countQuery; + this.existsQuery = existsQuery; + this.deleteQuery = deleteQuery; + + SpelExtractor spelExtractor = SPEL_QUERY_CONTEXT.parse(cypherTemplate); + this.spelEvaluator = new SpelEvaluator(evaluationContextProvider, queryMethod.getParameters(), spelExtractor); + this.cypherQuery = spelExtractor.getQueryString(); + } + + static String getQueryTemplate(Query queryAnnotation) { + + return Optional.ofNullable(queryAnnotation.value()) + .filter(StringUtils::hasText) + .orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not.")); + } + + @Override + protected PreparedQuery prepareQuery(Object[] parameters) { + + return PreparedQuery.queryFor(super.domainType) + .withCypherQuery(cypherQuery) + .withParameters(bindParameters(parameters)) + .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) // Null is fine + .build(); + } + + @Override + public boolean isCountQuery() { + return countQuery; + } + + @Override + public boolean isExistsQuery() { + return existsQuery; + } + + @Override + public boolean isDeleteQuery() { + return deleteQuery; + } + + @Override + protected boolean isLimiting() { + return false; + } + + Map bindParameters(Object[] actualParameters) { + + final Parameters formalParameters = queryMethod.getParameters(); + + Map resolvedParameters = new HashMap<>(spelEvaluator.evaluate(actualParameters)); + formalParameters.stream() + .filter(Parameter::isBindable) + .forEach(parameter -> { + + int parameterIndex = parameter.getIndex(); + Object parameterValue = super.convertParameter(actualParameters[parameterIndex]); + + // Add the parameter under its name when possible + parameter.getName() + .ifPresent(parameterName -> resolvedParameters.put(parameterName, parameterValue)); + // Always add under its index. + resolvedParameters.put(Integer.toString(parameterIndex), parameterValue); + }); + + return resolvedParameters; + } + + /** + * @param index + * @param originalSpelExpression Not used for configuring parameter names atm. + * @return + */ + private static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { + return "__SpEL__" + index; + } + + /** + * @param originalPrefix The prefix passed to the replacement source is either ':' or '?', so that isn't usable for + * Cypher templates and therefore ignored. + * @param parameterName + * @return + */ + private static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { + return "$" + parameterName; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java index 3fd62c1e3..b8767e671 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java @@ -31,8 +31,8 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.repository.NoResultException; -@RequiredArgsConstructor public -final class DefaultExecutableQuery implements ExecutableQuery { +@RequiredArgsConstructor +public final class DefaultExecutableQuery implements ExecutableQuery { private final PreparedQuery preparedQuery; private final Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java new file mode 100644 index 000000000..40b0045ac --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.apiguardian.api.API; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; + +/** + * Interface for controlling query execution. + * + * @param The type of the objects returned by this query. + * @author Gerrit Meier + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public interface ExecutableReactiveQuery { + + static ExecutableReactiveQuery create(PreparedQuery description, ReactiveNeo4jClient neo4jClient) { + + Class resultType = description.getResultType(); + Neo4jClient.MappingSpec, Flux, T> mappingSpec = neo4jClient + .newQuery(description.getCypherQuery()) + .bindAll(description.getParameters()) + .fetchAs(resultType); + + Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec = description + .getOptionalMappingFunction() + .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) + .orElse(mappingSpec); + + return new ReactiveExecutableQuery<>(fetchSpec); + } + + /** + * @return All results returned by this query. + */ + Flux getResults(); + + /** + * @return A single result + * @throws IncorrectResultSizeDataAccessException + */ + Mono getSingleResult(); + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveExecutableQuery.java new file mode 100644 index 000000000..d685c2e68 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveExecutableQuery.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.neo4j.driver.exceptions.NoSuchRecordException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.neo4j.core.Neo4jClient; + +@RequiredArgsConstructor +public final class ReactiveExecutableQuery implements ExecutableReactiveQuery { + + private final Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec; + + @Override + public Flux getResults() { + return fetchSpec.all(); + } + + @Override + public Mono getSingleResult() { + try { + return fetchSpec.one(); + } catch (NoSuchRecordException e) { + // This exception is thrown by the driver in both cases when there are 0 or 1+n records + // So there has been an incorrect result size, but not to few results but to many. + throw new IncorrectResultSizeDataAccessException(1); + } + } + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java new file mode 100644 index 000000000..b79a049fb --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import java.util.Optional; + +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.neo4j.repository.query.ReactiveNeo4jQueryLookupStrategy; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; + +/** + * Factory to create {@link ReactiveNeo4jRepository} instances. + * + * @author Gerrit Meier + * @since 1.0 + */ +final class ReactiveNeo4jRepositoryFactory extends RepositoryFactorySupport { + + private final ReactiveNeo4jClient neo4jClient; + + private final Neo4jMappingContext mappingContext; + + ReactiveNeo4jRepositoryFactory(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext) { + this.neo4jClient = neo4jClient; + this.mappingContext = mappingContext; + } + + @Override + public EntityInformation getEntityInformation(Class domainClass) { + return null; + } + + @Override + protected Object getTargetRepository(RepositoryInformation metadata) { + return getTargetRepositoryViaReflection(metadata, neo4jClient, mappingContext, metadata.getDomainType()); + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return SimpleReactiveNeo4jRepository.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) + */ + @Override + protected Optional getQueryLookupStrategy(Key key, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + + return Optional.of(new ReactiveNeo4jQueryLookupStrategy(neo4jClient, mappingContext, evaluationContextProvider)); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java new file mode 100644 index 000000000..7c50fce3f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import java.io.Serializable; + +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; + +/** + * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of + * repository factories via Spring configuration. + * + * @author Gerrit Meier + */ +public class ReactiveNeo4jRepositoryFactoryBean, S, ID extends Serializable> + extends TransactionalRepositoryFactoryBeanSupport { + + private ReactiveNeo4jClient neo4jClient; + + private Neo4jMappingContext neo4jMappingContext; + + /** + * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + protected ReactiveNeo4jRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + public void setNeo4jClient(ReactiveNeo4jClient neo4jClient) { + this.neo4jClient = neo4jClient; + } + + public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) { + this.neo4jMappingContext = neo4jMappingContext; + } + + @Override + protected RepositoryFactorySupport doCreateRepositoryFactory() { + return new ReactiveNeo4jRepositoryFactory(neo4jClient, neo4jMappingContext); + } + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java new file mode 100644 index 000000000..fd3877789 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import static java.util.Collections.*; +import static lombok.AccessLevel.*; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.core.schema.NodeDescription.*; +import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.ExampleMatcher.MatchMode; +import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.cypher.Condition; +import org.springframework.data.neo4j.core.cypher.Conditions; +import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Expression; +import org.springframework.data.neo4j.core.cypher.Functions; +import org.springframework.data.neo4j.core.cypher.Statement; +import org.springframework.data.neo4j.core.cypher.SymbolicName; +import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; +import org.springframework.data.neo4j.core.cypher.renderer.Renderer; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; +import org.springframework.data.neo4j.core.schema.IdDescription; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.support.ExampleMatcherAccessor; +import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * Repository base implementation for Neo4j. + * + * @author Gerrit Meier + * @author Michael J. Simons + */ +@Repository +@Transactional(readOnly = true) +@Slf4j +class SimpleReactiveNeo4jRepository implements ReactiveNeo4jRepository { + + private static final Renderer renderer = CypherRenderer.create(); + + private final ReactiveNeo4jClient neo4jClient; + + private final Neo4jMappingContext mappingContext; + + private final Class nodeClass; + + private final NodeDescription nodeDescription; + private final BiFunction mappingFunction; + + private final SchemaBasedStatementBuilder statementBuilder; + + private final Expression idExpression; + + SimpleReactiveNeo4jRepository(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext, Class nodeClass) { + + this.neo4jClient = neo4jClient; + this.mappingContext = mappingContext; + this.nodeClass = nodeClass; + + this.nodeDescription = (NodeDescription) mappingContext.getRequiredNodeDescription(nodeClass); + this.mappingFunction = mappingContext.getRequiredMappingFunctionFor(nodeClass); + + this.statementBuilder = createSchemaBasedStatementBuilder(mappingContext); + + final SymbolicName rootNode = Cypher.symbolicName("n"); + final IdDescription idDescription = this.nodeDescription.getIdDescription(); + switch (idDescription.getIdStrategy()) { + case INTERNAL: + idExpression = Functions.id(rootNode); + break; + case ASSIGNED: + case GENERATED: + idExpression = idDescription.getOptionalGraphPropertyName() + .map(propertyName -> property(rootNode.getName(), propertyName)).get(); + break; + default: + throw new IllegalStateException("Unsupported ID strategy: %s" + idDescription.getIdStrategy()); + } + } + + @Override + public Mono findOne(Example example) { + Predicate predicate = createPredicate(example); + Statement statement = statementBuilder + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) + .returning(asterisk()) + .build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getSingleResult(); + } + + @Override + public Flux findAll(Example example) { + Predicate predicate = createPredicate(example); + Statement statement = statementBuilder + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) + .returning(asterisk()) + .build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults(); + + } + + @Override + public Flux findAll(Example example, Sort sort) { + Predicate predicate = createPredicate(example); + Statement statement = statementBuilder + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) + .returning(asterisk()) + .orderBy(toSortItems(nodeDescription, sort)).build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults(); + } + + @Override + public Mono count(Example example) { + Predicate predicate = createPredicate(example); + Statement statement = statementBuilder + .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) + .returning(Functions.count(asterisk())) + .build(); + + return createExecutableQuery(Long.class, statement, predicate.parameters).getSingleResult(); + + } + + @Override + public Mono findById(ID id) { + Statement statement = statementBuilder + .prepareMatchOf(nodeDescription, Optional.of(idExpression.isEqualTo(literalOf(id)))) + .returning(asterisk()) + .build(); + return createExecutableQuery(statement).getSingleResult(); + } + + @Override + public Mono findById(Publisher idPublisher) { + return Mono.from(idPublisher).flatMap(this::findById); + } + + @Override + public Flux findAll(Sort sort) { + Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + .returning(asterisk()) + .orderBy(toSortItems(nodeDescription, sort)) + .build(); + + return createExecutableQuery(statement).getResults(); + } + + @Override + public Mono existsById(ID id) { + return findById(id).hasElement(); + } + + @Override + public Mono exists(Example example) { + return findAll(example).hasElements(); + } + + @Override + public Mono existsById(Publisher idPublisher) { + return Mono.from(idPublisher).flatMap(this::existsById); + } + + @Override + public Flux findAll() { + Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + .returning(asterisk()).build(); + return createExecutableQuery(statement).getResults(); + } + + @Override + public Flux findAllById(Iterable ids) { + Statement statement = statementBuilder + .prepareMatchOf(nodeDescription, Optional.of(idExpression.in((parameter("ids"))))) + .returning(asterisk()) + .build(); + + return createExecutableQuery(nodeClass, statement, singletonMap("ids", ids)).getResults(); + } + + @Override + public Flux findAllById(Publisher idStream) { + return Flux.from(idStream).buffer().flatMap(this::findAllById); + } + + @Override + public Mono count() { + Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + .returning(Functions.count(asterisk())).build(); + + return createExecutableQuery(Long.class, statement, Collections.emptyMap()).getSingleResult(); + + } + + @Override public Mono save(S entity) { + return null; + } + + @Override public Flux saveAll(Iterable entities) { + return null; + } + + @Override public Flux saveAll(Publisher entityStream) { + return null; + } + + @Override public Mono deleteById(ID id) { + return null; + } + + @Override public Mono deleteById(Publisher id) { + return null; + } + + @Override public Mono delete(T entity) { + return null; + } + + @Override public Mono deleteAll(Iterable entities) { + return null; + } + + @Override public Mono deleteAll(Publisher entityStream) { + return null; + } + + @Override public Mono deleteAll() { + return null; + } + + /** + * This wraps all information necessary to predicate a match: A root condition and actual parameters to fill + * in formal parameters inside the condition. + */ + @RequiredArgsConstructor(access = PRIVATE) + private static class Predicate { + + private final NodeDescription nodeDescription; + + private Condition condition = Conditions.noCondition(); + + private final Map parameters = new HashMap<>(); + + void add(MatchMode matchMode, Condition additionalCondition) { + + switch (matchMode) { + case ALL: + this.condition = this.condition.and(additionalCondition); + break; + case ANY: + this.condition = this.condition.or(additionalCondition); + break; + default: + throw new IllegalArgumentException("Unsupported match mode: " + matchMode); + } + } + } + + private Predicate createPredicate(Example example) { + + NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + SymbolicName rootNode = Cypher.symbolicName(NAME_OF_ROOT_NODE); + Collection graphProperties = probeNodeDescription.getGraphProperties(); + DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); + ExampleMatcher matcher = example.getMatcher(); + MatchMode mode = matcher.getMatchMode(); + ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(matcher); + + Predicate predicate = new Predicate(probeNodeDescription); + for (GraphPropertyDescription graphProperty : graphProperties) { + + // TODO Relationships are not traversed. + + String currentPath = graphProperty.getFieldName(); + if (matcherAccessor.isIgnoredPath(currentPath)) { + continue; + } + + boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.useInternalIds(); + String propertyName = graphProperty.getPropertyName(); + + PropertyValueTransformer transformer = matcherAccessor.getValueTransformerForPath(currentPath); + Optional optionalValue = transformer + .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); + + if (!optionalValue.isPresent()) { + if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) { + predicate.add(mode, property(rootNode, propertyName).isNull()); + } + continue; + } + + if (graphProperty.isRelationship()) { + log.error("Querying by example does not support traversing of relationships."); + } else if (graphProperty.isIdProperty() && probeNodeDescription.useInternalIds()) { + predicate.add(mode, idExpression.isEqualTo(literalOf(optionalValue.get()))); + } else { + Expression property = property(rootNode, propertyName); + Expression parameter = parameter(propertyName); + Condition condition = property.isEqualTo(parameter); + + if (String.class.equals(graphProperty.getActualType())) { + + if (matcherAccessor.isIgnoreCaseForPath(currentPath)) { + property = Functions.toLower(property); + parameter = Functions.toLower(parameter); + } + + switch (matcherAccessor.getStringMatcherForPath(currentPath)) { + case DEFAULT: + case EXACT: + // This needs to be recreated as both property and parameter might have changed above + condition = property.isEqualTo(parameter); + break; + case CONTAINING: + condition = property.contains(parameter); + break; + case STARTING: + condition = property.startsWith(parameter); + break; + case ENDING: + condition = property.endsWith(parameter); + break; + case REGEX: + condition = property.matches(parameter); + break; + default: + throw new IllegalArgumentException( + "Unsupported StringMatcher " + matcherAccessor.getStringMatcherForPath(currentPath)); + } + } + predicate.add(mode, condition); + predicate.parameters.put(propertyName, optionalValue.get()); + } + } + + return predicate; + } + + private ExecutableReactiveQuery createExecutableQuery(Statement statement) { + return createExecutableQuery(nodeClass, statement, Collections.emptyMap()); + } + + private ExecutableReactiveQuery createExecutableQuery(Class resultType, Statement statement, + Map parameters) { + + BiFunction mappingFunctionToUse = this.mappingFunction; + if (!this.nodeClass.equals(resultType)) { + mappingFunctionToUse = mappingContext.getMappingFunctionFor(resultType).orElse(null); + } + + PreparedQuery queryDescription = PreparedQuery.queryFor(resultType) + .withCypherQuery(renderer.render(statement)) + .withParameters(parameters) + .usingMappingFunction(mappingFunctionToUse) + .build(); + + return ExecutableReactiveQuery.create(queryDescription, neo4jClient); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonRepository.java similarity index 99% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonRepository.java index 138cd9204..5db6af8f8 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonRepository.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration; +package org.springframework.data.neo4j.integration.blocking; import java.time.LocalDate; import java.util.List; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithAllConstructor.java new file mode 100644 index 000000000..b1ab1bf0a --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithAllConstructor.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration.blocking; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDate; +import java.util.List; + +import org.neo4j.driver.types.Point; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + */ +@Getter +@Setter +@Node +@ToString +@AllArgsConstructor +@EqualsAndHashCode +public class PersonWithAllConstructor { + + @Id + private final Long id; + + private final String name; + + @Property("first_name") + private final String firstName; + + private final String sameValue; + + private final Boolean cool; + + private final Long personNumber; + + private final LocalDate bornOn; + + private final String nullable; + + private final List things; + + private final Point place; +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithNoConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithNoConstructor.java new file mode 100644 index 000000000..a0d8830ae --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithNoConstructor.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration.blocking; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; + +@Getter +@Setter +@Node +@ToString +public class PersonWithNoConstructor { + + @Id private Long id; + + private String name; + + @Property("first_name") + private String firstName; +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithWither.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithWither.java new file mode 100644 index 000000000..677be2aac --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithWither.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration.blocking; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +@Getter +@Setter +@Node +@ToString +public class PersonWithWither { + + @Id private final Long id; + + private final String name; + + private PersonWithWither(Long id, String name) { + this.id = id; + this.name = name; + } + + public PersonWithWither withId(Long newId) { + return new PersonWithWither(newId, this.name); + } + + public PersonWithWither withName(String newName) { + return new PersonWithWither(this.id, newName); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/RepositoryIT.java similarity index 99% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/RepositoryIT.java index 9411a3a20..fb13f7f11 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/RepositoryIT.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration; +package org.springframework.data.neo4j.integration.blocking; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Range.Bound.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingRepository.java new file mode 100644 index 000000000..e7b50717a --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration.blocking; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Michael J. Simons + */ +public interface ThingRepository extends CrudRepository { + List findFirstByOrderByNameDesc(); + + List findTop5ByOrderByNameDesc(); +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingWithAssignedId.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingWithAssignedId.java new file mode 100644 index 000000000..cb9f03bb6 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingWithAssignedId.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration.blocking; + +import static org.springframework.data.neo4j.core.schema.Id.Strategy.*; + +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +/** + * Has an assigned id and a changeable name. + * + * @author Michael J. Simons + */ +@Node("Thing") +public class ThingWithAssignedId { + + @Id(strategy = ASSIGNED) + private final String theId; + + private String name; + + public ThingWithAssignedId(String theId) { + this.theId = theId; + } + + public String getTheId() { + return theId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithAllConstructor.java similarity index 96% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithAllConstructor.java index dcd2f97af..24c6bde12 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithAllConstructor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration; +package org.springframework.data.neo4j.integration.rx; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithNoConstructor.java similarity index 95% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithNoConstructor.java index db84bd235..e91297f51 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithNoConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithNoConstructor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration; +package org.springframework.data.neo4j.integration.rx; import lombok.Getter; import lombok.Setter; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithWither.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithWither.java similarity index 95% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithWither.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithWither.java index a9894d7a6..7289950ec 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/PersonWithWither.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithWither.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration; +package org.springframework.data.neo4j.integration.rx; import lombok.Getter; import lombok.Setter; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactivePersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactivePersonRepository.java new file mode 100644 index 000000000..f2df42ef0 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactivePersonRepository.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration.rx; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Gerrit Meier + */ +public interface ReactivePersonRepository extends ReactiveNeo4jRepository { + + @Transactional + @Query("RETURN 1") + Mono customQuery(); + + @Query("MATCH (n:PersonWithAllConstructor) return n") + Flux getAllPersonsViaQuery(); + + @Query("MATCH (n:PersonWithAllConstructor{name:'Test'}) return n") + Mono getOnePersonViaQuery(); + + Mono findOneByNameAndFirstName(String name, String firstName); + + Mono findOneByNameAndFirstNameAllIgnoreCase(String name, String firstName); + + Flux findAllByNameOrName(String aName, String anotherName); + + Flux findAllBySameValue(String sameValue); +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactiveRepositoryIT.java new file mode 100644 index 000000000..6a7498904 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactiveRepositoryIT.java @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.integration.rx; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; +import org.neo4j.driver.types.Point; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; +import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Gerrit Meier + */ +@ExtendWith(Neo4jExtension.class) +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ReactiveRepositoryIT.Config.class) +class ReactiveRepositoryIT { + + private static final String TEST_PERSON1_NAME = "Test"; + private static final String TEST_PERSON2_NAME = "Test2"; + private static final String TEST_PERSON1_FIRST_NAME = "Ernie"; + private static final String TEST_PERSON2_FIRST_NAME = "Bert"; + private static final LocalDate TEST_PERSON1_BORN_ON = LocalDate.of(2019, 1, 1); + private static final LocalDate TEST_PERSON2_BORN_ON = LocalDate.of(2019, 2, 1); + private static final String TEST_PERSON_SAMEVALUE = "SameValue"; + private static final Point NEO4J_HQ = Values.point(4326, 12.994823, 55.612191).asPoint(); + private static final Point SFO = Values.point(4326, -122.38681, 37.61649).asPoint(); + private static final Point CLARION = Values.point(4326, 12.994243, 55.607726).asPoint(); + private static final Point MINC = Values.point(4326, 12.994039, 55.611496).asPoint(); + private static final long NOT_EXISTING_NODE_ID = 3123131231L; + private static Neo4jConnectionSupport neo4jConnectionSupport; + @Autowired private ReactivePersonRepository repository; + @Autowired private Driver driver; + private long id1; + private long id2; + private PersonWithAllConstructor person1; + private PersonWithAllConstructor person2; + + static PersonWithAllConstructor personExample(String sameValue) { + return new PersonWithAllConstructor(null, null, null, sameValue, null, null, null, null, null, null); + } + + @BeforeEach + void setupData() { + + Transaction transaction = driver.session().beginTransaction(); + transaction.run("MATCH (n) detach delete n"); + + id1 = transaction.run("" + "CREATE (n:PersonWithAllConstructor) " + + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + + "RETURN id(n)", + Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", + NEO4J_HQ)) + .next().get(0).asLong(); + + id2 = transaction.run( + "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", + Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) + .next().get(0).asLong(); + + transaction.success(); + transaction.close(); + + person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, + 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ); + + person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, + false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO); + } + + @Test + void findAll() { + List personList = Arrays.asList(person1, person2); + + StepVerifier.create(repository.findAll()).expectNextMatches(personList::contains) + .expectNextMatches(personList::contains).verifyComplete(); + } + + @Test + void findById() { + StepVerifier.create(repository.findById(id1)).expectNext(person1).verifyComplete(); + } + + @Test + void findAllByIds() { + List personList = Arrays.asList(person1, person2); + + StepVerifier.create(repository.findAllById(Arrays.asList(id1, id2))).expectNextMatches(personList::contains) + .expectNextMatches(personList::contains).verifyComplete(); + } + + @Test + void findAllByIdsPublisher() { + List personList = Arrays.asList(person1, person2); + + StepVerifier.create(repository.findAllById(Flux.just(id1, id2))).expectNextMatches(personList::contains) + .expectNextMatches(personList::contains).verifyComplete(); + } + + @Test + void findByIdNoMatch() { + StepVerifier.create(repository.findById(NOT_EXISTING_NODE_ID)).verifyComplete(); + } + + @Test + void findByIdPublisher() { + StepVerifier.create(repository.findById(Mono.just(id1))).expectNext(person1).verifyComplete(); + } + + @Test + void findByIdPublisherNoMatch() { + StepVerifier.create(repository.findById(Mono.just(NOT_EXISTING_NODE_ID))).verifyComplete(); + } + + @Test + void findAllWithSortByOrderDefault() { + StepVerifier.create(repository.findAll(Sort.by("name"))).expectNext(person1, person2).verifyComplete(); + } + + @Test + void findAllWithSortByOrderAsc() { + StepVerifier.create(repository.findAll(Sort.by(Sort.Order.asc("name")))).expectNext(person1, person2) + .verifyComplete(); + } + + @Test + void findAllWithSortByOrderDesc() { + StepVerifier.create(repository.findAll(Sort.by(Sort.Order.desc("name")))).expectNext(person2, person1) + .verifyComplete(); + } + + @Test + void findOneByExample() { + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + + StepVerifier.create(repository.findOne(example)).expectNext(person1).verifyComplete(); + } + + @Test + void findAllByExample() { + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + StepVerifier.create(repository.findAll(example)).expectNext(person1).verifyComplete(); + } + + @Test + void findAllByExampleWithDifferentMatchers() { + PersonWithAllConstructor person; + Example example; + Iterable persons; + + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, null, + null, null, null); + example = Example.of(person, ExampleMatcher.matchingAny()); + + StepVerifier.create(repository.findAll(example)).expectNext(person1, person2).verifyComplete(); + + person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, null, + null, null, null, null, null); + example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name")); + + StepVerifier.create(repository.findAll(example)).expectNext(person1, person2).verifyComplete(); + + person = new PersonWithAllConstructor(null, + TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(), + TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null, null, null, + null); + example = Example.of(person, ExampleMatcher.matchingAll() + .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.ENDING, true)) + .withMatcher("firstName", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING)) + .withMatcher("sameValue", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING))); + + StepVerifier.create(repository.findAll(example)).expectNext(person2).verifyComplete(); + + person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null, null, null, null); + example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(ExampleMatcher.StringMatcher.REGEX)); + + StepVerifier.create(repository.findAll(example)).expectNext(person1).verifyComplete(); + + example = Example.of(person, + ExampleMatcher.matchingAll().withStringMatcher(ExampleMatcher.StringMatcher.REGEX).withIncludeNullValues()); + + StepVerifier.create(repository.findAll(example)).verifyComplete(); + } + + @Test + void findAllByExampleWithSort() { + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + + StepVerifier.create(repository.findAll(example, Sort.by(Sort.Direction.DESC, "name"))).expectNext(person2, person1) + .verifyComplete(); + } + + @Test + void existsById() { + StepVerifier.create(repository.existsById(id1)).expectNext(true).verifyComplete(); + } + + @Test + void existsByIdNoMatch() { + StepVerifier.create(repository.existsById(NOT_EXISTING_NODE_ID)).expectNext(false).verifyComplete(); + } + + @Test + void existsByIdPublisher() { + StepVerifier.create(repository.existsById(id1)).expectNext(true).verifyComplete(); + } + + @Test + void existsByIdPublisherNoMatch() { + StepVerifier.create(repository.existsById(NOT_EXISTING_NODE_ID)).expectNext(false).verifyComplete(); + } + + @Test + void existsByExample() { + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + StepVerifier.create(repository.exists(example)).expectNext(true).verifyComplete(); + + } + + @Test + void count() { + StepVerifier.create(repository.count()).expectNext(2L).verifyComplete(); + } + + @Test + void countByExample() { + Example example = Example.of(person1); + StepVerifier.create(repository.count(example)).expectNext(1L).verifyComplete(); + } + + @Test + void callCustomCypher() { + StepVerifier.create(repository.customQuery()).expectNext(1L).verifyComplete(); + } + + @Test + void loadAllPersonsWithAllConstructor() { + List personList = Arrays.asList(person1, person2); + + StepVerifier.create(repository.getAllPersonsViaQuery()).expectNextMatches(personList::contains) + .expectNextMatches(personList::contains).verifyComplete(); + } + + @Test + void loadOnePersonWithAllConstructor() { + StepVerifier.create(repository.getOnePersonViaQuery()).expectNext(person1).verifyComplete(); + } + + @Test + void findBySimplePropertiesAnded() { + + StepVerifier.create(repository.findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME)) + .expectNext(person1).verifyComplete(); + + StepVerifier.create(repository.findOneByNameAndFirstNameAllIgnoreCase(TEST_PERSON1_NAME.toUpperCase(), + TEST_PERSON1_FIRST_NAME.toUpperCase())).expectNext(person1).verifyComplete(); + + } + + @Test + void findBySimplePropertiesOred() { + StepVerifier.create(repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME)) + .expectNext(person1, person2).verifyComplete(); + } + + @Test + void findBySimpleProperty() { + List personList = Arrays.asList(person1, person2); + + StepVerifier.create(repository.findAllBySameValue(TEST_PERSON_SAMEVALUE)).expectNextMatches(personList::contains) + .expectNextMatches(personList::contains).verifyComplete(); + } + + @Configuration + @EnableReactiveNeo4jRepositories + @EnableTransactionManagement + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + public Driver driver() { + + return neo4jConnectionSupport.openConnection(); + } + + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingRepository.java similarity index 94% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingRepository.java index 57343e84b..7352b8f73 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingRepository.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration; +package org.springframework.data.neo4j.integration.rx; import java.util.List; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingWithAssignedId.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingWithAssignedId.java similarity index 95% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingWithAssignedId.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingWithAssignedId.java index 4bfca4085..7b1231bf5 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingWithAssignedId.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingWithAssignedId.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration; +package org.springframework.data.neo4j.integration.rx; import static org.springframework.data.neo4j.core.schema.Id.Strategy.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java index 0789a220f..0164068e2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java @@ -105,7 +105,9 @@ public class Neo4jExtension implements BeforeAllCallback { static class ContainerAdapter implements ExtensionContext.Store.CloseableResource { - private final Neo4jContainer neo4jContainer = new Neo4jContainer<>() + private final String imageName = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_VERSION")).orElse("3.5.0"); + + private final Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:" + imageName) .withoutAuthentication(); public String getBoltUrl() { diff --git a/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/KotlinPerson.kt b/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/blocking/KotlinPerson.kt similarity index 93% rename from spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/KotlinPerson.kt rename to spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/blocking/KotlinPerson.kt index 7bf8f11ea..b1d0e1146 100644 --- a/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/KotlinPerson.kt +++ b/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/blocking/KotlinPerson.kt @@ -17,7 +17,7 @@ * limitations under the License. */ -package org.springframework.data.neo4j.integration +package org.springframework.data.neo4j.integration.blocking import org.springframework.data.neo4j.core.schema.Id import org.springframework.data.neo4j.core.schema.Node From a9aa01a9210a5b288baf8f91f8d7a49a120fda76 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 24 May 2019 12:17:40 +0200 Subject: [PATCH 097/342] Use EntityInformation and decouple ReactiveSortingRepository and ReactiveQueryByExampleExecutor. --- .../config/Neo4jConfigurationSupport.java | 2 +- .../mapping/DefaultNeo4jPersistentEntity.java | 7 - .../neo4j/core/schema/NodeDescription.java | 2 - .../repository/query/CypherAdapterUtils.java | 56 +++- .../repository/query/Neo4jQueryExecution.java | 6 +- .../support/DefaultExecutableQuery.java | 61 ---- .../DefaultNeo4jEntityInformation.java | 71 ++++ .../support/DefaultPreparedQuery.java | 74 ----- .../repository/support/ExecutableQuery.java | 58 ++-- .../support/ExecutableReactiveQuery.java | 35 +- .../support/Neo4jEntityInformation.java | 58 ++++ .../support/Neo4jRepositoryFactory.java | 44 ++- .../neo4j/repository/support/Predicate.java | 179 ++++++++++ .../repository/support/PreparedQuery.java | 39 ++- .../support/ReactiveExecutableQuery.java | 50 --- .../ReactiveNeo4jRepositoryFactory.java | 42 ++- .../support/SimpleNeo4jRepository.java | 306 +++--------------- .../support/SimpleQueryByExampleExecutor.java | 154 +++++++++ .../SimpleReactiveNeo4jRepository.java | 253 ++------------- .../SimpleReactiveQueryByExampleExecutor.java | 128 ++++++++ .../neo4j/core/cypher/RelationshipTest.java | 4 +- .../blocking/PersonWithAllConstructor.java | 68 ---- .../blocking/PersonWithNoConstructor.java | 41 --- .../blocking/PersonWithWither.java | 50 --- .../blocking/ThingWithAssignedId.java | 54 ---- .../PersonRepository.java | 6 +- .../RepositoryIT.java | 14 +- .../ThingRepository.java | 3 +- .../ReactivePersonRepository.java | 3 +- .../ReactiveRepositoryIT.java | 9 +- .../ReactiveThingRepository.java} | 5 +- .../PersonWithAllConstructor.java | 2 +- .../PersonWithNoConstructor.java | 2 +- .../{rx => shared}/PersonWithWither.java | 2 +- .../{rx => shared}/ThingWithAssignedId.java | 2 +- .../{blocking => shared}/KotlinPerson.kt | 2 +- 36 files changed, 912 insertions(+), 980 deletions(-) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultPreparedQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveExecutableQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithAllConstructor.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithNoConstructor.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithWither.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingWithAssignedId.java rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{blocking => imperative}/PersonRepository.java (95%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{blocking => imperative}/RepositoryIT.java (97%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{blocking => imperative}/ThingRepository.java (87%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{rx => reactive}/ReactivePersonRepository.java (92%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{rx => reactive}/ReactiveRepositoryIT.java (97%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{rx/ThingRepository.java => reactive/ReactiveThingRepository.java} (79%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{rx => shared}/PersonWithAllConstructor.java (96%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{rx => shared}/PersonWithNoConstructor.java (94%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{rx => shared}/PersonWithWither.java (95%) rename spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/{rx => shared}/ThingWithAssignedId.java (95%) rename spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/{blocking => shared}/KotlinPerson.kt (93%) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java index 716208906..15c497d2c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java @@ -92,7 +92,7 @@ public abstract class Neo4jConfigurationSupport { * @return the base packages to scan for mapped {@link Node} classes * or an empty collection to not enable scanning for entities. */ - protected final Collection getMappingBasePackages() { + protected Collection getMappingBasePackages() { Package mappingBasePackage = getClass().getPackage(); return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName()); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index 7acbed270..c9ac9503e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Optional; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.Id; @@ -86,12 +85,6 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity propertyAccessor = this.getPropertyAccessor(instance); - return propertyAccessor.getProperty(this.getRequiredIdProperty()); - } - @Override public void verify() { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java index 6b6d55d21..710e7993c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -71,6 +71,4 @@ public interface NodeDescription { default boolean useInternalIds() { return this.getIdDescription().getIdStrategy() == Id.Strategy.INTERNAL; } - - Object extractId(T entity); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java index 91bab6def..027359349 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java @@ -27,6 +27,7 @@ import java.util.Optional; import java.util.function.Function; import org.apiguardian.api.API; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Conditions; @@ -35,6 +36,8 @@ import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Node; import org.springframework.data.neo4j.core.cypher.SortItem; +import org.springframework.data.neo4j.core.cypher.StatementBuilder; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndDelete; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; import org.springframework.data.neo4j.core.cypher.SymbolicName; @@ -43,6 +46,7 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Schema; +import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; /** * A set of shared utils to adapt {@code org.springframework.data.neo4j.core.cypher} functionality with Spring Data @@ -113,6 +117,15 @@ public final class CypherAdapterUtils { this.schema = schema; } + /** + * @param nodeDescription + * @return + * @see #prepareMatchOf(NodeDescription, Optional) + */ + public OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription) { + return prepareMatchOf(nodeDescription, Optional.empty()); + } + /** * This will create a match statement that fits the given node description and may contains additional conditions. * The {@code WITH} clause of this statement contains all nodes and relationships necessary to map a record to @@ -120,7 +133,7 @@ public final class CypherAdapterUtils { *

* It is recommended to use {@link Cypher#asterisk()} to return everything from the query in the end. *

- * The root node is guaranted to have the symbolic name {@code n}. + * The root node is guaranteed to have the symbolic name {@code n}. * * @param nodeDescription The node description for which a match clause should be generated * @param condition Optional conditions to add @@ -147,6 +160,47 @@ public final class CypherAdapterUtils { } } + public static BuildableMatch addPagingParameter( + NodeDescription nodeDescription, + Pageable pageable, + StatementBuilder.OngoingMatchAndReturn returning) { + + Sort sort = pageable.getSort(); + + long skip = pageable.getOffset(); + + int pageSize = pageable.getPageSize(); + + return returning.orderBy(toSortItems(nodeDescription, sort)).skip(skip).limit(pageSize); + } + + /** + * @param nodeDescription + * @return + * @see Neo4jEntityInformation#getIdExpression() + */ + public static Expression createIdExpression(final NodeDescription nodeDescription) { + + final SymbolicName rootNode = Cypher.symbolicName(NAME_OF_ROOT_NODE); + final IdDescription idDescription = nodeDescription.getIdDescription(); + Expression idExpression; + switch (idDescription.getIdStrategy()) { + case INTERNAL: + idExpression = Functions.id(rootNode); + break; + case ASSIGNED: + case GENERATED: + idExpression = idDescription.getOptionalGraphPropertyName() + .map(propertyName -> property(rootNode.getName(), propertyName)).get(); + break; + default: + throw new IllegalStateException("Unsupported ID strategy: %s" + idDescription.getIdStrategy()); + } + + return idExpression; + } + + private CypherAdapterUtils() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java index 5c6dfb6d7..0091182ae 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java @@ -46,9 +46,9 @@ interface Neo4jQueryExecution { private final Neo4jClient neo4jClient; @Override - public Object execute(PreparedQuery description, boolean asCollectionQuery) { + public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { - ExecutableQuery executableQuery = ExecutableQuery.create(description, neo4jClient); + ExecutableQuery executableQuery = ExecutableQuery.create(neo4jClient, preparedQuery); if (asCollectionQuery) { return executableQuery.getResults(); } else { @@ -65,7 +65,7 @@ interface Neo4jQueryExecution { @Override public Object execute(PreparedQuery description, boolean asCollectionQuery) { - ExecutableReactiveQuery executableQuery = ExecutableReactiveQuery.create(description, neo4jClient); + ExecutableReactiveQuery executableQuery = ExecutableReactiveQuery.create(neo4jClient, description); if (asCollectionQuery) { return executableQuery.getResults(); } else { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java deleted file mode 100644 index b8767e671..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultExecutableQuery.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import static java.util.stream.Collectors.*; - -import lombok.RequiredArgsConstructor; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -import org.neo4j.driver.exceptions.NoSuchRecordException; -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.repository.NoResultException; - -@RequiredArgsConstructor -public final class DefaultExecutableQuery implements ExecutableQuery { - - private final PreparedQuery preparedQuery; - private final Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec; - - @Override - public List getResults() { - return fetchSpec.all().stream().collect(toList()); - } - - @Override - public Optional getSingleResult() { - try { - return fetchSpec.one(); - } catch (NoSuchRecordException e) { - // This exception is thrown by the driver in both cases when there are 0 or 1+n records - // So there has been an incorrect result size, but not to few results but to many. - throw new IncorrectResultSizeDataAccessException(1); - } - } - - @Override - public T getRequiredSingleResult() { - return fetchSpec.one() - .orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery())); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java new file mode 100644 index 000000000..77a2ddc6f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import java.util.function.BiFunction; + +import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; +import org.springframework.data.neo4j.core.cypher.Expression; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.repository.query.CypherAdapterUtils; +import org.springframework.data.repository.core.support.PersistentEntityInformation; + +/** + * Default implementation of Neo4j specific entity information. + * + * @author Michael J. Simons + * @soundtrack Bear McCreary - Battlestar Galactica Season 1 + * @since 1.0 + */ +final class DefaultNeo4jEntityInformation extends PersistentEntityInformation + implements Neo4jEntityInformation { + + private final NodeDescription nodeDescription; + private final BiFunction mappingFunction; + private final Expression idExpression; + + DefaultNeo4jEntityInformation( + Neo4jPersistentEntity entityMetaData, + BiFunction mappingFunction + ) { + super(entityMetaData); + + this.nodeDescription = entityMetaData; + this.mappingFunction = mappingFunction; + + this.idExpression = CypherAdapterUtils.createIdExpression(nodeDescription); + } + + @Override + public Expression getIdExpression() { + return this.idExpression; + } + + @Override + public NodeDescription getNodeDescription() { + return this.nodeDescription; + } + + @Override + public BiFunction getMappingFunction() { + return mappingFunction; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultPreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultPreparedQuery.java deleted file mode 100644 index e2d254e25..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultPreparedQuery.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import java.util.Map; -import java.util.Optional; -import java.util.function.BiFunction; - -import org.neo4j.driver.Record; -import org.neo4j.driver.types.TypeSystem; -import org.springframework.lang.Nullable; - -/** - * Typed preparation of a query that is used to create an {@link ExecutableQuery} of the same type. - *

- * When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure - * that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected - * by the mapping function to work correctly. - * - * @param The type of the objects returned by this query. - * @author Michael J. Simons - * @soundtrack Deichkind - Noch fünf Minuten Mutti - * @since 1.0 - */ -final class DefaultPreparedQuery implements PreparedQuery { - - private final Class resultType; - private final String cypherQuery; - private final Map parameters; - private final @Nullable BiFunction mappingFunction; - - DefaultPreparedQuery(OptionalBuildSteps optionalBuildSteps) { - this.resultType = optionalBuildSteps.resultType; - this.mappingFunction = (BiFunction) optionalBuildSteps.mappingFunction; - this.cypherQuery = optionalBuildSteps.cypherQuery; - this.parameters = optionalBuildSteps.parameters; - } - - @Override - public Class getResultType() { - return this.resultType; - } - - @Override - public Optional> getOptionalMappingFunction() { - return Optional.ofNullable(mappingFunction); - } - - @Override - public String getCypherQuery() { - return this.cypherQuery; - } - - @Override - public Map getParameters() { - return this.parameters; - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java index 387411776..9c47ff13c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java @@ -18,11 +18,17 @@ */ package org.springframework.data.neo4j.repository.support; +import static java.util.stream.Collectors.*; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + import java.util.Collection; import java.util.List; import java.util.Optional; import org.apiguardian.api.API; +import org.neo4j.driver.exceptions.NoSuchRecordException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.repository.NoResultException; @@ -36,36 +42,42 @@ import org.springframework.data.neo4j.repository.NoResultException; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public interface ExecutableQuery { +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class ExecutableQuery { - static ExecutableQuery create(PreparedQuery description, Neo4jClient neo4jClient) { + public static ExecutableQuery create(Neo4jClient neo4jClient, PreparedQuery preparedQuery) { - Class resultType = description.getResultType(); Neo4jClient.MappingSpec, Collection, T> mappingSpec = neo4jClient - .newQuery(description.getCypherQuery()) - .bindAll(description.getParameters()) - .fetchAs(resultType); - Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec = description.getOptionalMappingFunction() - .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) + .newQuery(preparedQuery.getCypherQuery()) + .bindAll(preparedQuery.getParameters()) + .fetchAs(preparedQuery.getResultType()); + Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec = preparedQuery + .getOptionalMappingFunction() + .map(f -> mappingSpec.mappedBy(f)) .orElse(mappingSpec); - return new DefaultExecutableQuery<>(description, fetchSpec); + return new ExecutableQuery<>(preparedQuery, fetchSpec); } - /** - * @return All results returned by this query. - */ - List getResults(); + private final PreparedQuery preparedQuery; + private final Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec; - /** - * @return A single result - * @throws IncorrectResultSizeDataAccessException - */ - Optional getSingleResult(); + public List getResults() { + return fetchSpec.all().stream().collect(toList()); + } - /** - * @return A single result - * @throws NoResultException - */ - T getRequiredSingleResult(); + public Optional getSingleResult() { + try { + return fetchSpec.one(); + } catch (NoSuchRecordException e) { + // This exception is thrown by the driver in both cases when there are 0 or 1+n records + // So there has been an incorrect result size, but not to few results but to many. + throw new IncorrectResultSizeDataAccessException(1); + } + } + + public T getRequiredSingleResult() { + return fetchSpec.one() + .orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery())); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java index 40b0045ac..b9ff8a1da 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java @@ -18,10 +18,13 @@ */ package org.springframework.data.neo4j.repository.support; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.apiguardian.api.API; +import org.neo4j.driver.exceptions.NoSuchRecordException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; @@ -31,36 +34,50 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jClient; * * @param The type of the objects returned by this query. * @author Gerrit Meier + * @author Michael J. Simons * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public interface ExecutableReactiveQuery { +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class ExecutableReactiveQuery { - static ExecutableReactiveQuery create(PreparedQuery description, ReactiveNeo4jClient neo4jClient) { + public static ExecutableReactiveQuery create(ReactiveNeo4jClient neo4jClient, PreparedQuery preparedQuery) { - Class resultType = description.getResultType(); + Class resultType = preparedQuery.getResultType(); Neo4jClient.MappingSpec, Flux, T> mappingSpec = neo4jClient - .newQuery(description.getCypherQuery()) - .bindAll(description.getParameters()) + .newQuery(preparedQuery.getCypherQuery()) + .bindAll(preparedQuery.getParameters()) .fetchAs(resultType); - Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec = description + Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec = preparedQuery .getOptionalMappingFunction() .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) .orElse(mappingSpec); - return new ReactiveExecutableQuery<>(fetchSpec); + return new ExecutableReactiveQuery<>(fetchSpec); } + private final Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec; + /** * @return All results returned by this query. */ - Flux getResults(); + public Flux getResults() { + return fetchSpec.all(); + } /** * @return A single result * @throws IncorrectResultSizeDataAccessException */ - Mono getSingleResult(); + public Mono getSingleResult() { + try { + return fetchSpec.one(); + } catch (NoSuchRecordException e) { + // This exception is thrown by the driver in both cases when there are 0 or 1+n records + // So there has been an incorrect result size, but not to few results but to many. + throw new IncorrectResultSizeDataAccessException(1); + } + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java new file mode 100644 index 000000000..b48f4548d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import java.util.function.BiFunction; + +import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; +import org.springframework.data.neo4j.core.cypher.Expression; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.repository.core.EntityInformation; + +/** + * Neo4j specific contract for {@link EntityInformation entity informations}. + * + * @param The type of the entity + * @param The type of the id + * @author Michael J. Simons + * @soundtrack Bear McCreary - Battlestar Galactica Season 1 + * @since 1.0 + */ +public interface Neo4jEntityInformation extends EntityInformation { + + /** + * Retrieves an expression to be used in Cypher statements to retrieve the generated or internal id for a node + * fitting the given {@code nodeDescription} under a symbolic name of {@code 'n'}. + * + * @return An expression to address nodes corresponding to entities of the given typoe by id. + */ + Expression getIdExpression(); + + /** + * @return The full schema based description for the underlying entity. + */ + NodeDescription getNodeDescription(); + + /** + * @return A mapping function that creates instances of {@code T} for the given record. + * @see org.springframework.data.neo4j.core.schema.Schema#getMappingFunctionFor(Class) + */ + BiFunction getMappingFunction(); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 40f9447f2..bb3562c2e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -18,16 +18,24 @@ */ package org.springframework.data.neo4j.repository.support; -import java.util.Optional; +import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.CypherAdapterUtils; import org.springframework.data.neo4j.repository.query.Neo4jQueryLookupStrategy; -import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -45,19 +53,45 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { private final Neo4jMappingContext mappingContext; + private final SchemaBasedStatementBuilder schemaBasedStatementBuilder; + Neo4jRepositoryFactory(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext) { this.neo4jClient = neo4jClient; this.mappingContext = mappingContext; + + this.schemaBasedStatementBuilder = CypherAdapterUtils.createSchemaBasedStatementBuilder(this.mappingContext); } @Override - public EntityInformation getEntityInformation(Class domainClass) { - return null; + public Neo4jEntityInformation getEntityInformation(Class domainClass) { + + Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + BiFunction mappingFunction = mappingContext.getRequiredMappingFunctionFor(domainClass); + + return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction); } @Override protected Object getTargetRepository(RepositoryInformation metadata) { - return getTargetRepositoryViaReflection(metadata, neo4jClient, mappingContext, metadata.getDomainType()); + + Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); + return getTargetRepositoryViaReflection(metadata, + neo4jClient, entityInformation, schemaBasedStatementBuilder + ); + } + + @Override + protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + + RepositoryFragments fragments = RepositoryFragments.empty(); + + Object byExampleExecutor = getTargetRepositoryViaReflection( + SimpleQueryByExampleExecutor.class, + neo4jClient, mappingContext, schemaBasedStatementBuilder); + + fragments = fragments.append(RepositoryFragment.implemented(byExampleExecutor)); + + return fragments; } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java new file mode 100644 index 000000000..fbe2afe4f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import static lombok.AccessLevel.*; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.core.schema.NodeDescription.*; +import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.neo4j.core.cypher.Condition; +import org.springframework.data.neo4j.core.cypher.Conditions; +import org.springframework.data.neo4j.core.cypher.Cypher; +import org.springframework.data.neo4j.core.cypher.Expression; +import org.springframework.data.neo4j.core.cypher.Functions; +import org.springframework.data.neo4j.core.cypher.StatementBuilder; +import org.springframework.data.neo4j.core.cypher.SymbolicName; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.support.ExampleMatcherAccessor; +import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; + +/** + * Support class for "query by example" executors. + *

+ * This wraps all information necessary to predicate a match: A root condition and actual parameters to fill + * in formal parameters inside the condition. + * + * @author Michael J. Simons + * @since 1.0 + */ +@Slf4j +@RequiredArgsConstructor(access = PRIVATE) final class Predicate { + + static Predicate create(Neo4jMappingContext mappingContext, Example example) { + + NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); + + SymbolicName rootNode = Cypher.symbolicName(NAME_OF_ROOT_NODE); + Collection graphProperties = probeNodeDescription.getGraphProperties(); + DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); + ExampleMatcher matcher = example.getMatcher(); + ExampleMatcher.MatchMode mode = matcher.getMatchMode(); + ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(matcher); + + Predicate predicate = new Predicate(probeNodeDescription); + for (GraphPropertyDescription graphProperty : graphProperties) { + + // TODO Relationships are not traversed. + + String currentPath = graphProperty.getFieldName(); + if (matcherAccessor.isIgnoredPath(currentPath)) { + continue; + } + + boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.useInternalIds(); + String propertyName = graphProperty.getPropertyName(); + + ExampleMatcher.PropertyValueTransformer transformer = matcherAccessor + .getValueTransformerForPath(currentPath); + Optional optionalValue = transformer + .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); + + if (!optionalValue.isPresent()) { + if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) { + predicate.add(mode, property(rootNode, propertyName).isNull()); + } + continue; + } + + if (graphProperty.isRelationship()) { + log.error("Querying by example does not support traversing of relationships."); + } else if (graphProperty.isIdProperty() && probeNodeDescription.useInternalIds()) { + predicate + .add(mode, createIdExpression(predicate.nodeDescription).isEqualTo(literalOf(optionalValue.get()))); + } else { + Expression property = property(rootNode, propertyName); + Expression parameter = parameter(propertyName); + Condition condition = property.isEqualTo(parameter); + + if (String.class.equals(graphProperty.getActualType())) { + + if (matcherAccessor.isIgnoreCaseForPath(currentPath)) { + property = Functions.toLower(property); + parameter = Functions.toLower(parameter); + } + + switch (matcherAccessor.getStringMatcherForPath(currentPath)) { + case DEFAULT: + case EXACT: + // This needs to be recreated as both property and parameter might have changed above + condition = property.isEqualTo(parameter); + break; + case CONTAINING: + condition = property.contains(parameter); + break; + case STARTING: + condition = property.startsWith(parameter); + break; + case ENDING: + condition = property.endsWith(parameter); + break; + case REGEX: + condition = property.matches(parameter); + break; + default: + throw new IllegalArgumentException( + "Unsupported StringMatcher " + matcherAccessor.getStringMatcherForPath(currentPath)); + } + } + predicate.add(mode, condition); + predicate.parameters.put(propertyName, optionalValue.get()); + } + } + + return predicate; + } + + private final NodeDescription nodeDescription; + + private Condition condition = Conditions.noCondition(); + + private final Map parameters = new HashMap<>(); + + StatementBuilder.OngoingMatchAndWith f( + BiFunction, Optional, StatementBuilder.OngoingMatchAndWith> ff) { + return ff.apply(this.nodeDescription, Optional.of(this.condition)); + } + + private void add(ExampleMatcher.MatchMode matchMode, Condition additionalCondition) { + + switch (matchMode) { + case ALL: + this.condition = this.condition.and(additionalCondition); + break; + case ANY: + this.condition = this.condition.or(additionalCondition); + break; + default: + throw new IllegalArgumentException("Unsupported match mode: " + matchMode); + } + } + + public NodeDescription getNodeDescription() { + return nodeDescription; + } + + public Map getParameters() { + return Collections.unmodifiableMap(parameters); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java index fcbe4864a..978fb52d0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java @@ -37,30 +37,49 @@ import org.springframework.lang.Nullable; * by the mapping function to work correctly. * * @param The type of the objects returned by this query. - * @see ExecutableQuery#create(PreparedQuery, org.springframework.data.neo4j.core.Neo4jClient) * @author Michael J. Simons * @soundtrack Deichkind - Arbeit nervt * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public interface PreparedQuery { +public final class PreparedQuery { - static RequiredBuildStep queryFor(Class resultType) { + public static RequiredBuildStep queryFor(Class resultType) { return new RequiredBuildStep(resultType); } - Class getResultType(); + private final Class resultType; + private final String cypherQuery; + private final Map parameters; + private final @Nullable BiFunction mappingFunction; - Optional> getOptionalMappingFunction(); + private PreparedQuery(OptionalBuildSteps optionalBuildSteps) { + this.resultType = optionalBuildSteps.resultType; + this.mappingFunction = (BiFunction) optionalBuildSteps.mappingFunction; + this.cypherQuery = optionalBuildSteps.cypherQuery; + this.parameters = optionalBuildSteps.parameters; + } - String getCypherQuery(); + public Class getResultType() { + return this.resultType; + } - Map getParameters(); + public Optional> getOptionalMappingFunction() { + return Optional.ofNullable(mappingFunction); + } + + public String getCypherQuery() { + return this.cypherQuery; + } + + public Map getParameters() { + return this.parameters; + } /** * @param The concrete type of this build step. */ - class RequiredBuildStep { + public static class RequiredBuildStep { private final Class resultType; private RequiredBuildStep(Class resultType) { @@ -75,7 +94,7 @@ public interface PreparedQuery { /** * @param The concrete type of this build step. */ - class OptionalBuildSteps { + public static class OptionalBuildSteps { final Class resultType; final String cypherQuery; @@ -105,7 +124,7 @@ public interface PreparedQuery { } public PreparedQuery build() { - return new DefaultPreparedQuery<>(this); + return new PreparedQuery<>(this); } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveExecutableQuery.java deleted file mode 100644 index d685c2e68..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveExecutableQuery.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import lombok.RequiredArgsConstructor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.neo4j.driver.exceptions.NoSuchRecordException; -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.neo4j.core.Neo4jClient; - -@RequiredArgsConstructor -public final class ReactiveExecutableQuery implements ExecutableReactiveQuery { - - private final Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec; - - @Override - public Flux getResults() { - return fetchSpec.all(); - } - - @Override - public Mono getSingleResult() { - try { - return fetchSpec.one(); - } catch (NoSuchRecordException e) { - // This exception is thrown by the driver in both cases when there are 0 or 1+n records - // So there has been an incorrect result size, but not to few results but to many. - throw new IncorrectResultSizeDataAccessException(1); - } - } - -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java index b79a049fb..4141a77b5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -19,15 +19,22 @@ package org.springframework.data.neo4j.repository.support; import java.util.Optional; +import java.util.function.BiFunction; +import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.neo4j.repository.query.CypherAdapterUtils; +import org.springframework.data.neo4j.repository.query.CypherAdapterUtils.SchemaBasedStatementBuilder; import org.springframework.data.neo4j.repository.query.ReactiveNeo4jQueryLookupStrategy; -import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -36,6 +43,7 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro * Factory to create {@link ReactiveNeo4jRepository} instances. * * @author Gerrit Meier + * @author Michael J. Simons * @since 1.0 */ final class ReactiveNeo4jRepositoryFactory extends RepositoryFactorySupport { @@ -44,19 +52,45 @@ final class ReactiveNeo4jRepositoryFactory extends RepositoryFactorySupport { private final Neo4jMappingContext mappingContext; + private final SchemaBasedStatementBuilder schemaBasedStatementBuilder; + ReactiveNeo4jRepositoryFactory(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext) { this.neo4jClient = neo4jClient; this.mappingContext = mappingContext; + + this.schemaBasedStatementBuilder = CypherAdapterUtils.createSchemaBasedStatementBuilder(this.mappingContext); } @Override - public EntityInformation getEntityInformation(Class domainClass) { - return null; + public Neo4jEntityInformation getEntityInformation(Class domainClass) { + + Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + BiFunction mappingFunction = mappingContext.getRequiredMappingFunctionFor(domainClass); + + return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction); } @Override protected Object getTargetRepository(RepositoryInformation metadata) { - return getTargetRepositoryViaReflection(metadata, neo4jClient, mappingContext, metadata.getDomainType()); + + Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); + return getTargetRepositoryViaReflection(metadata, + neo4jClient, entityInformation, schemaBasedStatementBuilder + ); + } + + @Override + protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + + RepositoryFragments fragments = RepositoryFragments.empty(); + + SimpleReactiveQueryByExampleExecutor byExampleExecutor = getTargetRepositoryViaReflection( + SimpleReactiveQueryByExampleExecutor.class, + neo4jClient, mappingContext, schemaBasedStatementBuilder); + + fragments = fragments.append(RepositoryFragment.implemented(byExampleExecutor)); + + return fragments; } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 0e7209320..8b0006f16 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -19,54 +19,34 @@ package org.springframework.data.neo4j.repository.support; import static java.util.Collections.*; -import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.core.schema.NodeDescription.*; import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; import java.util.function.LongSupplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import org.neo4j.driver.Record; import org.neo4j.driver.summary.ResultSummary; -import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.ExampleMatcher; -import org.springframework.data.domain.ExampleMatcher.MatchMode; -import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.cypher.Condition; -import org.springframework.data.neo4j.core.cypher.Conditions; -import org.springframework.data.neo4j.core.cypher.Cypher; -import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; -import org.springframework.data.neo4j.core.cypher.SymbolicName; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndReturn; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; -import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.support.PageableExecutionUtils; -import org.springframework.data.support.ExampleMatcherAccessor; -import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -75,58 +55,35 @@ import org.springframework.transaction.annotation.Transactional; * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ @Repository @Transactional(readOnly = true) @Slf4j -class SimpleNeo4jRepository implements Neo4jRepository { +class SimpleNeo4jRepository implements PagingAndSortingRepository { private static final Renderer renderer = CypherRenderer.create(); private final Neo4jClient neo4jClient; - private final Neo4jMappingContext mappingContext; - - private final Class nodeClass; + private final Neo4jEntityInformation entityInformation; private final NodeDescription nodeDescription; - private final BiFunction mappingFunction; private final SchemaBasedStatementBuilder statementBuilder; - private final Expression idExpression; - - SimpleNeo4jRepository(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, Class nodeClass) { - + SimpleNeo4jRepository(Neo4jClient neo4jClient, Neo4jEntityInformation entityInformation, + SchemaBasedStatementBuilder statementBuilder) { this.neo4jClient = neo4jClient; - this.mappingContext = mappingContext; - this.nodeClass = nodeClass; - - this.nodeDescription = (NodeDescription) mappingContext.getRequiredNodeDescription(nodeClass); - this.mappingFunction = mappingContext.getRequiredMappingFunctionFor(nodeClass); - - this.statementBuilder = createSchemaBasedStatementBuilder(mappingContext); - - final SymbolicName rootNode = Cypher.symbolicName("n"); - final IdDescription idDescription = this.nodeDescription.getIdDescription(); - switch (idDescription.getIdStrategy()) { - case INTERNAL: - idExpression = Functions.id(rootNode); - break; - case ASSIGNED: - case GENERATED: - idExpression = idDescription.getOptionalGraphPropertyName() - .map(propertyName -> property(rootNode.getName(), propertyName)).get(); - break; - default: - throw new IllegalStateException("Unsupported ID strategy: %s" + idDescription.getIdStrategy()); - } + this.entityInformation = entityInformation; + this.nodeDescription = this.entityInformation.getNodeDescription(); + this.statementBuilder = statementBuilder; } @Override public Iterable findAll(Sort sort) { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(nodeDescription) .returning(asterisk()) .orderBy(toSortItems(nodeDescription, sort)) .build(); @@ -137,10 +94,10 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Page findAll(Pageable pageable) { - StatementBuilder.OngoingMatchAndReturn returning = statementBuilder - .prepareMatchOf(nodeDescription, Optional.empty()).returning(asterisk()); + OngoingMatchAndReturn returning = statementBuilder.prepareMatchOf(nodeDescription) + .returning(asterisk()); - StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); + StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(nodeDescription, pageable, returning); Statement statement = returningWithPaging.build(); @@ -149,34 +106,6 @@ class SimpleNeo4jRepository implements Neo4jRepository { return PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier); } - @Override - public Page findAll(Example example, Pageable pageable) { - - Predicate predicate = createPredicate(example); - StatementBuilder.OngoingMatchAndReturn returning = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)).returning(asterisk()); - - StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(pageable, returning); - - Statement statement = returningWithPaging.build(); - - List page = createExecutableQuery(example.getProbeType(), statement, predicate.parameters) - .getResults(); - LongSupplier totalCountSupplier = this::count; - return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); - } - - private StatementBuilder.BuildableMatch addPagingParameter(Pageable pageable, StatementBuilder.OngoingMatchAndReturn returning) { - - Sort sort = pageable.getSort(); - - long skip = pageable.getOffset(); - - int pageSize = pageable.getPageSize(); - - return returning.orderBy(toSortItems(nodeDescription, sort)).skip(skip).limit(pageSize); - } - @Override @Transactional public S save(S entity) { @@ -193,7 +122,8 @@ class SimpleNeo4jRepository implements Neo4jRepository { public Optional findById(ID id) { Statement statement = statementBuilder - .prepareMatchOf(nodeDescription, Optional.of(idExpression.isEqualTo(literalOf(id)))) + .prepareMatchOf(nodeDescription, + Optional.of(entityInformation.getIdExpression().isEqualTo(literalOf(id)))) .returning(asterisk()) .build(); return createExecutableQuery(statement).getSingleResult(); @@ -207,7 +137,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Override public Iterable findAll() { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(nodeDescription) .returning(asterisk()).build(); return createExecutableQuery(statement).getResults(); } @@ -216,20 +146,24 @@ class SimpleNeo4jRepository implements Neo4jRepository { public Iterable findAllById(Iterable ids) { Statement statement = statementBuilder - .prepareMatchOf(nodeDescription, Optional.of(idExpression.in((parameter("ids"))))) + .prepareMatchOf(nodeDescription, Optional.of(entityInformation.getIdExpression().in((parameter("ids"))))) .returning(asterisk()) .build(); - return createExecutableQuery(nodeClass, statement, singletonMap("ids", ids)).getResults(); + return createExecutableQuery(statement, singletonMap("ids", ids)).getResults(); } @Override public long count() { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(nodeDescription) .returning(Functions.count(asterisk())).build(); - return createExecutableQuery(Long.class, statement, Collections.emptyMap()).getRequiredSingleResult(); + PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) + .withCypherQuery(renderer.render(statement)) + .build(); + return ExecutableQuery.create(this.neo4jClient, preparedQuery) + .getRequiredSingleResult(); } @Override @@ -237,11 +171,12 @@ class SimpleNeo4jRepository implements Neo4jRepository { public void deleteById(ID id) { String nameOfParameter = "id"; - Condition condition = idExpression.isEqualTo(parameter(nameOfParameter)); + Condition condition = this.entityInformation.getIdExpression().isEqualTo(parameter(nameOfParameter)); log.debug("Deleting entity with id {} ", id); - Statement statement = statementBuilder.prepareDeleteOf(nodeDescription, Optional.of(condition)).build(); + Statement statement = statementBuilder + .prepareDeleteOf(nodeDescription, Optional.of(condition)).build(); ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)) .bind(id).to(nameOfParameter) .run(); @@ -253,7 +188,7 @@ class SimpleNeo4jRepository implements Neo4jRepository { @Transactional public void delete(T entity) { - ID id = (ID) this.nodeDescription.extractId(entity); + ID id = (ID) this.entityInformation.getId(entity); this.deleteById(id); } @@ -262,10 +197,10 @@ class SimpleNeo4jRepository implements Neo4jRepository { public void deleteAll(Iterable entities) { String nameOfParameter = "ids"; - Condition condition = idExpression.in(parameter(nameOfParameter)); + Condition condition = entityInformation.getIdExpression().in(parameter(nameOfParameter)); List ids = StreamSupport.stream(entities.spliterator(), false) - .map(this.nodeDescription::extractId).collect(Collectors.toList()); + .map(this.entityInformation::getId).collect(Collectors.toList()); log.debug("Deleting all entities with the following ids: {} ", ids); @@ -289,186 +224,17 @@ class SimpleNeo4jRepository implements Neo4jRepository { log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); } - @Override - public Optional findOne(Example example) { - - Predicate predicate = createPredicate(example); - Statement statement = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) - .returning(asterisk()) - .build(); - - return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getSingleResult(); - } - - @Override - public Iterable findAll(Example example) { - - Predicate predicate = createPredicate(example); - Statement statement = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) - .returning(asterisk()) - .build(); - - return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults(); - } - - @Override - public Iterable findAll(Example example, Sort sort) { - - Predicate predicate = createPredicate(example); - Statement statement = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) - .returning(asterisk()) - .orderBy(toSortItems(nodeDescription, sort)).build(); - - return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults(); - } - - @Override - public long count(Example example) { - - Predicate predicate = createPredicate(example); - Statement statement = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) - .returning(Functions.count(asterisk())) - .build(); - - return createExecutableQuery(Long.class, statement, predicate.parameters).getRequiredSingleResult(); - } - - @Override - public boolean exists(Example example) { - return findAll(example).iterator().hasNext(); - } - - /** - * This wraps all information necessary to predicate a match: A root condition and actual parameters to fill - * in formal parameters inside the condition. - */ - @RequiredArgsConstructor(access = PRIVATE) - private static class Predicate { - - private final NodeDescription nodeDescription; - - private Condition condition = Conditions.noCondition(); - - private final Map parameters = new HashMap<>(); - - void add(MatchMode matchMode, Condition additionalCondition) { - - switch (matchMode) { - case ALL: - this.condition = this.condition.and(additionalCondition); - break; - case ANY: - this.condition = this.condition.or(additionalCondition); - break; - default: - throw new IllegalArgumentException("Unsupported match mode: " + matchMode); - } - } - } - - private Predicate createPredicate(Example example) { - - NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); - SymbolicName rootNode = Cypher.symbolicName(NAME_OF_ROOT_NODE); - Collection graphProperties = probeNodeDescription.getGraphProperties(); - DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); - ExampleMatcher matcher = example.getMatcher(); - MatchMode mode = matcher.getMatchMode(); - ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(matcher); - - Predicate predicate = new Predicate(probeNodeDescription); - for (GraphPropertyDescription graphProperty : graphProperties) { - - // TODO Relationships are not traversed. - - String currentPath = graphProperty.getFieldName(); - if (matcherAccessor.isIgnoredPath(currentPath)) { - continue; - } - - boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.useInternalIds(); - String propertyName = graphProperty.getPropertyName(); - - PropertyValueTransformer transformer = matcherAccessor.getValueTransformerForPath(currentPath); - Optional optionalValue = transformer - .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); - - if (!optionalValue.isPresent()) { - if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) { - predicate.add(mode, property(rootNode, propertyName).isNull()); - } - continue; - } - - if (graphProperty.isRelationship()) { - log.error("Querying by example does not support traversing of relationships."); - } else if (graphProperty.isIdProperty() && probeNodeDescription.useInternalIds()) { - predicate.add(mode, idExpression.isEqualTo(literalOf(optionalValue.get()))); - } else { - Expression property = property(rootNode, propertyName); - Expression parameter = parameter(propertyName); - Condition condition = property.isEqualTo(parameter); - - if (String.class.equals(graphProperty.getActualType())) { - - if (matcherAccessor.isIgnoreCaseForPath(currentPath)) { - property = Functions.toLower(property); - parameter = Functions.toLower(parameter); - } - - switch (matcherAccessor.getStringMatcherForPath(currentPath)) { - case DEFAULT: - case EXACT: - // This needs to be recreated as both property and parameter might have changed above - condition = property.isEqualTo(parameter); - break; - case CONTAINING: - condition = property.contains(parameter); - break; - case STARTING: - condition = property.startsWith(parameter); - break; - case ENDING: - condition = property.endsWith(parameter); - break; - case REGEX: - condition = property.matches(parameter); - break; - default: - throw new IllegalArgumentException( - "Unsupported StringMatcher " + matcherAccessor.getStringMatcherForPath(currentPath)); - } - } - predicate.add(mode, condition); - predicate.parameters.put(propertyName, optionalValue.get()); - } - } - - return predicate; - } - private ExecutableQuery createExecutableQuery(Statement statement) { - return createExecutableQuery(nodeClass, statement, Collections.emptyMap()); + return createExecutableQuery(statement, Collections.emptyMap()); } - private ExecutableQuery createExecutableQuery(Class resultType, Statement statement, - Map parameters) { + private ExecutableQuery createExecutableQuery(Statement statement, Map parameters) { - BiFunction mappingFunctionToUse = this.mappingFunction; - if (!this.nodeClass.equals(resultType)) { - mappingFunctionToUse = mappingContext.getMappingFunctionFor(resultType).orElse(null); - } - - PreparedQuery queryDescription = PreparedQuery.queryFor(resultType) + PreparedQuery preparedQuery = PreparedQuery.queryFor(this.entityInformation.getJavaType()) .withCypherQuery(renderer.render(statement)) .withParameters(parameters) - .usingMappingFunction(mappingFunctionToUse) + .usingMappingFunction(this.entityInformation.getMappingFunction()) .build(); - - return ExecutableQuery.create(queryDescription, neo4jClient); + return ExecutableQuery.create(this.neo4jClient, preparedQuery); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java new file mode 100644 index 000000000..ac10311c9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import static lombok.AccessLevel.*; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.LongSupplier; + +import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.cypher.Functions; +import org.springframework.data.neo4j.core.cypher.Statement; +import org.springframework.data.neo4j.core.cypher.StatementBuilder; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableMatch; +import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; +import org.springframework.data.neo4j.core.cypher.renderer.Renderer; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.data.repository.support.PageableExecutionUtils; + +/** + * A fragment for repositories providing "Query by example" functionality. + * + * @param + * @author Michael J. Simons + * @since 1.0 + */ +@Slf4j +@RequiredArgsConstructor(access = PACKAGE) +class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { + + private static final Renderer renderer = CypherRenderer.create(); + + private final Neo4jClient neo4jClient; + + private final Neo4jMappingContext mappingContext; + + private final SchemaBasedStatementBuilder statementBuilder; + + @Override + public Optional findOne(Example example) { + + Predicate predicate = Predicate.create(mappingContext, example); + Statement statement = predicate.f(statementBuilder::prepareMatchOf) + .returning(asterisk()) + .build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.getParameters()).getSingleResult(); + } + + @Override + public Iterable findAll(Example example) { + + Predicate predicate = Predicate.create(mappingContext, example); + Statement statement = predicate.f(statementBuilder::prepareMatchOf) + .returning(asterisk()) + .build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.getParameters()).getResults(); + } + + @Override + public Iterable findAll(Example example, Sort sort) { + + Predicate predicate = Predicate.create(mappingContext, example); + Statement statement = predicate.f(statementBuilder::prepareMatchOf) + .returning(asterisk()) + .orderBy(toSortItems(predicate.getNodeDescription(), sort)).build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.getParameters()).getResults(); + } + + @Override + public long count(Example example) { + + Predicate predicate = Predicate.create(mappingContext, example); + Statement statement = predicate.f(statementBuilder::prepareMatchOf) + .returning(Functions.count(asterisk())) + .build(); + + PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) + .withCypherQuery(renderer.render(statement)) + .withParameters(predicate.getParameters()) + .build(); + return ExecutableQuery.create(this.neo4jClient, preparedQuery) + .getRequiredSingleResult(); + } + + @Override + public boolean exists(Example example) { + return findAll(example).iterator().hasNext(); + } + + @Override + public Page findAll(Example example, Pageable pageable) { + + Predicate predicate = Predicate.create(mappingContext, example); + StatementBuilder.OngoingMatchAndReturn returning = predicate + .f(statementBuilder::prepareMatchOf) + .returning(asterisk()); + + BuildableMatch returningWithPaging = addPagingParameter(predicate.getNodeDescription(), pageable, returning); + + Statement statement = returningWithPaging.build(); + + List page = createExecutableQuery(example.getProbeType(), statement, predicate.getParameters()).getResults(); + LongSupplier totalCountSupplier = () -> this.count(example); + return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); + } + + private ExecutableQuery createExecutableQuery(Class resultType, Statement statement, + Map parameters) { + + BiFunction mappingFunctionToUse + = this.mappingContext.getMappingFunctionFor(resultType).orElse(null); + + PreparedQuery preparedQuery = PreparedQuery.queryFor(resultType) + .withCypherQuery(renderer.render(statement)) + .withParameters(parameters) + .usingMappingFunction(mappingFunctionToUse) + .build(); + return ExecutableQuery.create(this.neo4jClient, preparedQuery); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java index fd3877789..cbac67f7a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java @@ -19,48 +19,26 @@ package org.springframework.data.neo4j.repository.support; import static java.util.Collections.*; -import static lombok.AccessLevel.*; import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.core.schema.NodeDescription.*; import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; -import org.neo4j.driver.Record; -import org.neo4j.driver.types.TypeSystem; import org.reactivestreams.Publisher; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.ExampleMatcher; -import org.springframework.data.domain.ExampleMatcher.MatchMode; -import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.cypher.Condition; -import org.springframework.data.neo4j.core.cypher.Conditions; -import org.springframework.data.neo4j.core.cypher.Cypher; -import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.SymbolicName; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; -import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; -import org.springframework.data.support.ExampleMatcherAccessor; -import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; +import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -69,104 +47,35 @@ import org.springframework.transaction.annotation.Transactional; * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ @Repository @Transactional(readOnly = true) @Slf4j -class SimpleReactiveNeo4jRepository implements ReactiveNeo4jRepository { +class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository { private static final Renderer renderer = CypherRenderer.create(); private final ReactiveNeo4jClient neo4jClient; - private final Neo4jMappingContext mappingContext; - - private final Class nodeClass; + private final Neo4jEntityInformation entityInformation; private final NodeDescription nodeDescription; - private final BiFunction mappingFunction; private final SchemaBasedStatementBuilder statementBuilder; - private final Expression idExpression; - - SimpleReactiveNeo4jRepository(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext, Class nodeClass) { - + SimpleReactiveNeo4jRepository(ReactiveNeo4jClient neo4jClient, Neo4jEntityInformation entityInformation, + SchemaBasedStatementBuilder statementBuilder) { this.neo4jClient = neo4jClient; - this.mappingContext = mappingContext; - this.nodeClass = nodeClass; - - this.nodeDescription = (NodeDescription) mappingContext.getRequiredNodeDescription(nodeClass); - this.mappingFunction = mappingContext.getRequiredMappingFunctionFor(nodeClass); - - this.statementBuilder = createSchemaBasedStatementBuilder(mappingContext); - - final SymbolicName rootNode = Cypher.symbolicName("n"); - final IdDescription idDescription = this.nodeDescription.getIdDescription(); - switch (idDescription.getIdStrategy()) { - case INTERNAL: - idExpression = Functions.id(rootNode); - break; - case ASSIGNED: - case GENERATED: - idExpression = idDescription.getOptionalGraphPropertyName() - .map(propertyName -> property(rootNode.getName(), propertyName)).get(); - break; - default: - throw new IllegalStateException("Unsupported ID strategy: %s" + idDescription.getIdStrategy()); - } - } - - @Override - public Mono findOne(Example example) { - Predicate predicate = createPredicate(example); - Statement statement = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) - .returning(asterisk()) - .build(); - - return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getSingleResult(); - } - - @Override - public Flux findAll(Example example) { - Predicate predicate = createPredicate(example); - Statement statement = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) - .returning(asterisk()) - .build(); - - return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults(); - - } - - @Override - public Flux findAll(Example example, Sort sort) { - Predicate predicate = createPredicate(example); - Statement statement = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) - .returning(asterisk()) - .orderBy(toSortItems(nodeDescription, sort)).build(); - - return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults(); - } - - @Override - public Mono count(Example example) { - Predicate predicate = createPredicate(example); - Statement statement = statementBuilder - .prepareMatchOf(predicate.nodeDescription, Optional.of(predicate.condition)) - .returning(Functions.count(asterisk())) - .build(); - - return createExecutableQuery(Long.class, statement, predicate.parameters).getSingleResult(); - + this.entityInformation = entityInformation; + this.nodeDescription = this.entityInformation.getNodeDescription(); + this.statementBuilder = statementBuilder; } @Override public Mono findById(ID id) { Statement statement = statementBuilder - .prepareMatchOf(nodeDescription, Optional.of(idExpression.isEqualTo(literalOf(id)))) + .prepareMatchOf(nodeDescription, Optional.of(entityInformation.getIdExpression().isEqualTo(literalOf(id)))) .returning(asterisk()) .build(); return createExecutableQuery(statement).getSingleResult(); @@ -192,11 +101,6 @@ class SimpleReactiveNeo4jRepository implements ReactiveNeo4jRepository Mono exists(Example example) { - return findAll(example).hasElements(); - } - @Override public Mono existsById(Publisher idPublisher) { return Mono.from(idPublisher).flatMap(this::existsById); @@ -212,11 +116,11 @@ class SimpleReactiveNeo4jRepository implements ReactiveNeo4jRepository findAllById(Iterable ids) { Statement statement = statementBuilder - .prepareMatchOf(nodeDescription, Optional.of(idExpression.in((parameter("ids"))))) + .prepareMatchOf(nodeDescription, Optional.of(entityInformation.getIdExpression().in((parameter("ids"))))) .returning(asterisk()) .build(); - return createExecutableQuery(nodeClass, statement, singletonMap("ids", ids)).getResults(); + return createExecutableQuery(statement, singletonMap("ids", ids)).getResults(); } @Override @@ -229,8 +133,11 @@ class SimpleReactiveNeo4jRepository implements ReactiveNeo4jRepository preparedQuery = PreparedQuery.queryFor(Long.class) + .withCypherQuery(renderer.render(statement)) + .build(); + return ExecutableReactiveQuery.create(this.neo4jClient, preparedQuery) + .getSingleResult(); } @Override public Mono save(S entity) { @@ -269,133 +176,17 @@ class SimpleReactiveNeo4jRepository implements ReactiveNeo4jRepository nodeDescription; - - private Condition condition = Conditions.noCondition(); - - private final Map parameters = new HashMap<>(); - - void add(MatchMode matchMode, Condition additionalCondition) { - - switch (matchMode) { - case ALL: - this.condition = this.condition.and(additionalCondition); - break; - case ANY: - this.condition = this.condition.or(additionalCondition); - break; - default: - throw new IllegalArgumentException("Unsupported match mode: " + matchMode); - } - } - } - - private Predicate createPredicate(Example example) { - - NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); - SymbolicName rootNode = Cypher.symbolicName(NAME_OF_ROOT_NODE); - Collection graphProperties = probeNodeDescription.getGraphProperties(); - DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); - ExampleMatcher matcher = example.getMatcher(); - MatchMode mode = matcher.getMatchMode(); - ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(matcher); - - Predicate predicate = new Predicate(probeNodeDescription); - for (GraphPropertyDescription graphProperty : graphProperties) { - - // TODO Relationships are not traversed. - - String currentPath = graphProperty.getFieldName(); - if (matcherAccessor.isIgnoredPath(currentPath)) { - continue; - } - - boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.useInternalIds(); - String propertyName = graphProperty.getPropertyName(); - - PropertyValueTransformer transformer = matcherAccessor.getValueTransformerForPath(currentPath); - Optional optionalValue = transformer - .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); - - if (!optionalValue.isPresent()) { - if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) { - predicate.add(mode, property(rootNode, propertyName).isNull()); - } - continue; - } - - if (graphProperty.isRelationship()) { - log.error("Querying by example does not support traversing of relationships."); - } else if (graphProperty.isIdProperty() && probeNodeDescription.useInternalIds()) { - predicate.add(mode, idExpression.isEqualTo(literalOf(optionalValue.get()))); - } else { - Expression property = property(rootNode, propertyName); - Expression parameter = parameter(propertyName); - Condition condition = property.isEqualTo(parameter); - - if (String.class.equals(graphProperty.getActualType())) { - - if (matcherAccessor.isIgnoreCaseForPath(currentPath)) { - property = Functions.toLower(property); - parameter = Functions.toLower(parameter); - } - - switch (matcherAccessor.getStringMatcherForPath(currentPath)) { - case DEFAULT: - case EXACT: - // This needs to be recreated as both property and parameter might have changed above - condition = property.isEqualTo(parameter); - break; - case CONTAINING: - condition = property.contains(parameter); - break; - case STARTING: - condition = property.startsWith(parameter); - break; - case ENDING: - condition = property.endsWith(parameter); - break; - case REGEX: - condition = property.matches(parameter); - break; - default: - throw new IllegalArgumentException( - "Unsupported StringMatcher " + matcherAccessor.getStringMatcherForPath(currentPath)); - } - } - predicate.add(mode, condition); - predicate.parameters.put(propertyName, optionalValue.get()); - } - } - - return predicate; - } - private ExecutableReactiveQuery createExecutableQuery(Statement statement) { - return createExecutableQuery(nodeClass, statement, Collections.emptyMap()); + return createExecutableQuery(statement, Collections.emptyMap()); } - private ExecutableReactiveQuery createExecutableQuery(Class resultType, Statement statement, - Map parameters) { + private ExecutableReactiveQuery createExecutableQuery(Statement statement, Map parameters) { - BiFunction mappingFunctionToUse = this.mappingFunction; - if (!this.nodeClass.equals(resultType)) { - mappingFunctionToUse = mappingContext.getMappingFunctionFor(resultType).orElse(null); - } - - PreparedQuery queryDescription = PreparedQuery.queryFor(resultType) + PreparedQuery preparedQuery = PreparedQuery.queryFor(this.entityInformation.getJavaType()) .withCypherQuery(renderer.render(statement)) .withParameters(parameters) - .usingMappingFunction(mappingFunctionToUse) + .usingMappingFunction(this.entityInformation.getMappingFunction()) .build(); - - return ExecutableReactiveQuery.create(queryDescription, neo4jClient); + return ExecutableReactiveQuery.create(neo4jClient, preparedQuery); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java new file mode 100644 index 000000000..4fcecee9c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.repository.support; + +import static lombok.AccessLevel.*; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.function.BiFunction; + +import org.neo4j.driver.Record; +import org.neo4j.driver.types.TypeSystem; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.cypher.Functions; +import org.springframework.data.neo4j.core.cypher.Statement; +import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; +import org.springframework.data.neo4j.core.cypher.renderer.Renderer; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; + +/** + * A fragment for repositories providing "Query by example" functionality in a reactive way. + * + * @param + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +@Slf4j +@RequiredArgsConstructor(access = PACKAGE) +class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleExecutor { + + private static final Renderer renderer = CypherRenderer.create(); + + private final ReactiveNeo4jClient neo4jClient; + + private final Neo4jMappingContext mappingContext; + + private final SchemaBasedStatementBuilder statementBuilder; + + @Override + public Mono findOne(Example example) { + + Predicate predicate = Predicate.create(mappingContext, example); + Statement statement = predicate.f(statementBuilder::prepareMatchOf) + .returning(asterisk()) + .build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.getParameters()).getSingleResult(); + } + + @Override + public Flux findAll(Example example) { + + Predicate predicate = Predicate.create(mappingContext, example); + Statement statement = predicate.f(statementBuilder::prepareMatchOf) + .returning(asterisk()) + .build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.getParameters()).getResults(); + } + + @Override + public Flux findAll(Example example, Sort sort) { + + Predicate predicate = Predicate.create(mappingContext, example); + Statement statement = predicate.f(statementBuilder::prepareMatchOf) + .returning(asterisk()) + .orderBy(toSortItems(predicate.getNodeDescription(), sort)).build(); + + return createExecutableQuery(example.getProbeType(), statement, predicate.getParameters()).getResults(); + } + + @Override + public Mono count(Example example) { + + Predicate predicate = Predicate.create(mappingContext, example); + Statement statement = predicate.f(statementBuilder::prepareMatchOf) + .returning(Functions.count(asterisk())) + .build(); + + return createExecutableQuery(Long.class, statement, predicate.getParameters()).getSingleResult(); + } + + @Override + public Mono exists(Example example) { + return findAll(example).hasElements(); + } + + private ExecutableReactiveQuery createExecutableQuery(Class resultType, Statement statement, + Map parameters) { + + BiFunction mappingFunctionToUse + = this.mappingContext.getMappingFunctionFor(resultType).orElse(null); + + PreparedQuery queryDescription = PreparedQuery.queryFor(resultType) + .withCypherQuery(renderer.render(statement)) + .withParameters(parameters) + .usingMappingFunction(mappingFunctionToUse) + .build(); + + return ExecutableReactiveQuery.create(neo4jClient, queryDescription); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java index f3192b628..f42a273ea 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java @@ -30,9 +30,9 @@ class RelationshipTest { @Test void preconditionsShouldBeAsserted() { - assertThatIllegalArgumentException().isThrownBy(() -> Relationship.create(null, null, Node.create("a"), null)) + assertThatIllegalArgumentException().isThrownBy(() -> Relationship.create(null, null, Node.create("a"), new String[0])) .withMessage("Left node is required."); - assertThatIllegalArgumentException().isThrownBy(() -> Relationship.create(Node.create("a"), null, null, null)) + assertThatIllegalArgumentException().isThrownBy(() -> Relationship.create(Node.create("a"), null, null, new String[0])) .withMessage("Right node is required."); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithAllConstructor.java deleted file mode 100644 index b1ab1bf0a..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithAllConstructor.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.integration.blocking; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -import java.time.LocalDate; -import java.util.List; - -import org.neo4j.driver.types.Point; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Property; - -/** - * @author Gerrit Meier - * @author Michael J. Simons - */ -@Getter -@Setter -@Node -@ToString -@AllArgsConstructor -@EqualsAndHashCode -public class PersonWithAllConstructor { - - @Id - private final Long id; - - private final String name; - - @Property("first_name") - private final String firstName; - - private final String sameValue; - - private final Boolean cool; - - private final Long personNumber; - - private final LocalDate bornOn; - - private final String nullable; - - private final List things; - - private final Point place; -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithNoConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithNoConstructor.java deleted file mode 100644 index a0d8830ae..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithNoConstructor.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.integration.blocking; - -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Property; - -@Getter -@Setter -@Node -@ToString -public class PersonWithNoConstructor { - - @Id private Long id; - - private String name; - - @Property("first_name") - private String firstName; -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithWither.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithWither.java deleted file mode 100644 index 677be2aac..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonWithWither.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.integration.blocking; - -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; - -@Getter -@Setter -@Node -@ToString -public class PersonWithWither { - - @Id private final Long id; - - private final String name; - - private PersonWithWither(Long id, String name) { - this.id = id; - this.name = name; - } - - public PersonWithWither withId(Long newId) { - return new PersonWithWither(newId, this.name); - } - - public PersonWithWither withName(String newName) { - return new PersonWithWither(this.id, newName); - } -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingWithAssignedId.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingWithAssignedId.java deleted file mode 100644 index cb9f03bb6..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingWithAssignedId.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.integration.blocking; - -import static org.springframework.data.neo4j.core.schema.Id.Strategy.*; - -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; - -/** - * Has an assigned id and a changeable name. - * - * @author Michael J. Simons - */ -@Node("Thing") -public class ThingWithAssignedId { - - @Id(strategy = ASSIGNED) - private final String theId; - - private String name; - - public ThingWithAssignedId(String theId) { - this.theId = theId; - } - - public String getTheId() { - return theId; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/PersonRepository.java similarity index 95% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonRepository.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/PersonRepository.java index 5db6af8f8..18d44c835 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/PersonRepository.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.blocking; +package org.springframework.data.neo4j.integration.imperative; import java.time.LocalDate; import java.util.List; @@ -26,6 +26,10 @@ import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; +import org.springframework.data.neo4j.integration.shared.KotlinPerson; +import org.springframework.data.neo4j.integration.shared.PersonWithAllConstructor; +import org.springframework.data.neo4j.integration.shared.PersonWithNoConstructor; +import org.springframework.data.neo4j.integration.shared.PersonWithWither; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.repository.query.Param; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java similarity index 97% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/RepositoryIT.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index fb13f7f11..2d4e92637 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -16,13 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.blocking; +package org.springframework.data.neo4j.integration.imperative; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Range.Bound.*; import java.time.LocalDate; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -51,6 +52,11 @@ import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.config.AbstractNeo4jConfig; +import org.springframework.data.neo4j.integration.shared.KotlinPerson; +import org.springframework.data.neo4j.integration.shared.PersonWithAllConstructor; +import org.springframework.data.neo4j.integration.shared.PersonWithNoConstructor; +import org.springframework.data.neo4j.integration.shared.PersonWithWither; +import org.springframework.data.neo4j.integration.shared.ThingWithAssignedId; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; @@ -866,8 +872,12 @@ class RepositoryIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); + } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/ThingRepository.java similarity index 87% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingRepository.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/ThingRepository.java index e7b50717a..95b1d23c2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/blocking/ThingRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/ThingRepository.java @@ -16,10 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.blocking; +package org.springframework.data.neo4j.integration.imperative; import java.util.List; +import org.springframework.data.neo4j.integration.shared.ThingWithAssignedId; import org.springframework.data.repository.CrudRepository; /** diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactivePersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactivePersonRepository.java similarity index 92% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactivePersonRepository.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactivePersonRepository.java index f2df42ef0..2ca02c034 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactivePersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactivePersonRepository.java @@ -16,11 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.rx; +package org.springframework.data.neo4j.integration.reactive; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.data.neo4j.integration.shared.PersonWithAllConstructor; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.transaction.annotation.Transactional; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java similarity index 97% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactiveRepositoryIT.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java index 6a7498904..4269409c8 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.rx; +package org.springframework.data.neo4j.integration.reactive; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -24,6 +24,7 @@ import reactor.test.StepVerifier; import java.time.LocalDate; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; @@ -41,6 +42,7 @@ import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; +import org.springframework.data.neo4j.integration.shared.PersonWithAllConstructor; import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; @@ -322,9 +324,12 @@ class ReactiveRepositoryIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); } + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); + } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveThingRepository.java similarity index 79% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingRepository.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveThingRepository.java index 7352b8f73..015c26117 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveThingRepository.java @@ -16,16 +16,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.rx; +package org.springframework.data.neo4j.integration.reactive; import java.util.List; +import org.springframework.data.neo4j.integration.shared.ThingWithAssignedId; import org.springframework.data.repository.CrudRepository; /** * @author Michael J. Simons */ -public interface ThingRepository extends CrudRepository { +public interface ReactiveThingRepository extends CrudRepository { List findFirstByOrderByNameDesc(); List findTop5ByOrderByNameDesc(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java similarity index 96% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithAllConstructor.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java index 24c6bde12..1390cf1b2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.rx; +package org.springframework.data.neo4j.integration.shared; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithNoConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithNoConstructor.java similarity index 94% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithNoConstructor.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithNoConstructor.java index e91297f51..6fde45e6c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithNoConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithNoConstructor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.rx; +package org.springframework.data.neo4j.integration.shared; import lombok.Getter; import lombok.Setter; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithWither.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithWither.java similarity index 95% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithWither.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithWither.java index 7289950ec..4df50ad22 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/PersonWithWither.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithWither.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.rx; +package org.springframework.data.neo4j.integration.shared; import lombok.Getter; import lombok.Setter; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingWithAssignedId.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/ThingWithAssignedId.java similarity index 95% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingWithAssignedId.java rename to spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/ThingWithAssignedId.java index 7b1231bf5..0201bee97 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/rx/ThingWithAssignedId.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/ThingWithAssignedId.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.rx; +package org.springframework.data.neo4j.integration.shared; import static org.springframework.data.neo4j.core.schema.Id.Strategy.*; diff --git a/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/blocking/KotlinPerson.kt b/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/shared/KotlinPerson.kt similarity index 93% rename from spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/blocking/KotlinPerson.kt rename to spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/shared/KotlinPerson.kt index b1d0e1146..5638f695d 100644 --- a/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/blocking/KotlinPerson.kt +++ b/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/shared/KotlinPerson.kt @@ -17,7 +17,7 @@ * limitations under the License. */ -package org.springframework.data.neo4j.integration.blocking +package org.springframework.data.neo4j.integration.shared import org.springframework.data.neo4j.core.schema.Id import org.springframework.data.neo4j.core.schema.Node From 5cc8350b1f8bea15f6b0d8abdf3aca4409ce9d1d Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 24 May 2019 12:47:23 +0200 Subject: [PATCH 098/342] Move the concepts of Prepared and Executable queries back to the client. --- .../data/neo4j/core/DefaultNeo4jClient.java | 47 +++++++++++ .../core/DefaultReactiveNeo4jClient.java | 47 +++++++++++ .../data/neo4j/core/Neo4jClient.java | 25 ++++++ .../support => core}/PreparedQuery.java | 6 +- .../data/neo4j/core/ReactiveNeo4jClient.java | 30 +++++++ .../repository/query/AbstractNeo4jQuery.java | 2 +- .../query/AbstractReactiveNeo4jQuery.java | 2 +- .../repository/query/Neo4jQueryExecution.java | 10 +-- .../repository/query/PartTreeNeo4jQuery.java | 2 +- .../query/ReactivePartTreeNeo4jQuery.java | 2 +- .../query/ReactiveStringBasedNeo4jQuery.java | 2 +- .../query/StringBasedNeo4jQuery.java | 2 +- .../repository/support/ExecutableQuery.java | 83 ------------------- .../support/ExecutableReactiveQuery.java | 83 ------------------- .../support/SimpleNeo4jRepository.java | 6 +- .../support/SimpleQueryByExampleExecutor.java | 7 +- .../SimpleReactiveNeo4jRepository.java | 10 ++- .../SimpleReactiveQueryByExampleExecutor.java | 8 +- 18 files changed, 182 insertions(+), 192 deletions(-) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/{repository/support => core}/PreparedQuery.java (93%) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index 8bbfe1cfa..61cfec1d5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core; import static java.util.stream.Collectors.*; import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; @@ -30,6 +31,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -43,8 +45,11 @@ import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; import org.neo4j.driver.StatementRunner; +import org.neo4j.driver.exceptions.NoSuchRecordException; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.neo4j.repository.NoResultException; /** * Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to and interact with the database. @@ -135,6 +140,21 @@ class DefaultNeo4jClient implements Neo4jClient { return new DefaultRunnableDelegation<>(callback); } + @Override + public ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery) { + + Neo4jClient.MappingSpec, Collection, T> mappingSpec = this + .newQuery(preparedQuery.getCypherQuery()) + .bindAll(preparedQuery.getParameters()) + .fetchAs(preparedQuery.getResultType()); + Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec = preparedQuery + .getOptionalMappingFunction() + .map(f -> mappingSpec.mappedBy(f)) + .orElse(mappingSpec); + + return new DefaultExecutableQuery<>(preparedQuery, fetchSpec); + } + /** * Basically a holder of a cypher template supplier and a set of named parameters. It's main purpose is to * orchestrate the running of things with a bit of logging. @@ -323,4 +343,31 @@ class DefaultNeo4jClient implements Neo4jClient { } } } + + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + final class DefaultExecutableQuery implements ExecutableQuery { + + private final PreparedQuery preparedQuery; + private final Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec; + + public List getResults() { + return fetchSpec.all().stream().collect(toList()); + } + + public Optional getSingleResult() { + try { + return fetchSpec.one(); + } catch (NoSuchRecordException e) { + // This exception is thrown by the driver in both cases when there are 0 or 1+n records + // So there has been an incorrect result size, but not to few results but to many. + throw new IncorrectResultSizeDataAccessException(1); + } + } + + public T getRequiredSingleResult() { + return fetchSpec.one() + .orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery())); + } + } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 4c83d9047..b38033df0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.core; import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,12 +37,14 @@ import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; +import org.neo4j.driver.exceptions.NoSuchRecordException; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.reactivestreams.Publisher; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; import org.springframework.data.neo4j.core.Neo4jClient.OngoingBindSpec; import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; @@ -128,6 +131,23 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return new DefaultReactiveRunnableDelegation<>(callback); } + @Override + public ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery) { + + Class resultType = preparedQuery.getResultType(); + Neo4jClient.MappingSpec, Flux, T> mappingSpec = this + .newQuery(preparedQuery.getCypherQuery()) + .bindAll(preparedQuery.getParameters()) + .fetchAs(resultType); + + Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec = preparedQuery + .getOptionalMappingFunction() + .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) + .orElse(mappingSpec); + + return new DefaultReactiveExecutableQuery<>(fetchSpec); + } + @RequiredArgsConstructor class DefaultReactiveRunnableSpec implements ReactiveRunnableSpec { @@ -300,4 +320,31 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { DefaultReactiveNeo4jClient::asyncCancel); } } + + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + final class DefaultReactiveExecutableQuery implements ExecutableQuery { + + private final Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec; + + /** + * @return All results returned by this query. + */ + public Flux getResults() { + return fetchSpec.all(); + } + + /** + * @return A single result + * @throws IncorrectResultSizeDataAccessException + */ + public Mono getSingleResult() { + try { + return fetchSpec.one(); + } catch (NoSuchRecordException e) { + // This exception is thrown by the driver in both cases when there are 0 or 1+n records + // So there has been an incorrect result size, but not to few results but to many. + throw new IncorrectResultSizeDataAccessException(1); + } + } + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java index dbd1e3cf1..65c91d040 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; @@ -82,6 +83,16 @@ public interface Neo4jClient { */ OngoingDelegation delegateTo(Function> callback); + /** + * Takes a prepared query, containing all the information about the cypher template to be used, needed parameters and + * an optional mapping function, and turns it into an executable query. + * + * @param preparedQuery + * @param The type of the objects returned by this query. + * @return + */ + ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery); + /** * Contract for a runnable query that can be either run returning it's result, run without results or be parameterized. */ @@ -241,4 +252,18 @@ public interface Neo4jClient { */ Optional run(); } + + /** + * An interface for controlling query execution. + * + * @param + */ + interface ExecutableQuery { + + List getResults(); + + Optional getSingleResult(); + + T getRequiredSingleResult(); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java index 978fb52d0..ec0c388f4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/PreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.springframework.data.neo4j.core; import java.util.Collections; import java.util.HashMap; @@ -30,7 +30,9 @@ import org.neo4j.driver.types.TypeSystem; import org.springframework.lang.Nullable; /** - * Typed preparation of a query that is used to create an {@link ExecutableQuery} of the same type. + * Typed preparation of a query that is used to create either an executable query. + * Executable queries come in two fashions: imperative and reactive. Depending on which client is used to retrieve one, + * you get one or the other. *

* When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure * that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java index 8bdf0a6e9..4b417071c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java @@ -29,6 +29,7 @@ import org.apiguardian.api.API; import org.neo4j.driver.Driver; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.summary.ResultSummary; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.core.Neo4jClient.BindSpec; import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; @@ -77,6 +78,16 @@ public interface ReactiveNeo4jClient { */ OngoingReactiveDelegation delegateTo(Function> callback); + /** + * Takes a prepared query, containing all the information about the cypher template to be used, needed parameters and + * an optional mapping function, and turns it into an executable query. + * + * @param preparedQuery + * @param The type of the objects returned by this query. + * @return + */ + ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery); + /** * Contract for a runnable query that can be either run returning it's result, run without results or be parameterized. */ @@ -150,4 +161,23 @@ public interface ReactiveNeo4jClient { */ Mono run(); } + + /** + * An interface for controlling query execution in a reactive fashion. + * + * @param + */ + interface ExecutableQuery { + + /** + * @return All results returned by this query. + */ + Flux getResults(); + + /** + * @return A single result + * @throws IncorrectResultSizeDataAccessException + */ + Mono getSingleResult(); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java index e76807dd2..8455771a2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -27,7 +27,7 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java index 773de4f68..fb7310977 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java @@ -27,7 +27,7 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java index 0091182ae..ffce33218 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java @@ -21,10 +21,8 @@ package org.springframework.data.neo4j.repository.query; import lombok.RequiredArgsConstructor; import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.repository.support.ExecutableQuery; -import org.springframework.data.neo4j.repository.support.ExecutableReactiveQuery; -import org.springframework.data.neo4j.repository.support.PreparedQuery; /** * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a @@ -48,7 +46,7 @@ interface Neo4jQueryExecution { @Override public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { - ExecutableQuery executableQuery = ExecutableQuery.create(neo4jClient, preparedQuery); + Neo4jClient.ExecutableQuery executableQuery = neo4jClient.toExecutableQuery(preparedQuery); if (asCollectionQuery) { return executableQuery.getResults(); } else { @@ -63,9 +61,9 @@ interface Neo4jQueryExecution { private final ReactiveNeo4jClient neo4jClient; @Override - public Object execute(PreparedQuery description, boolean asCollectionQuery) { + public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { - ExecutableReactiveQuery executableQuery = ExecutableReactiveQuery.create(neo4jClient, description); + ReactiveNeo4jClient.ExecutableQuery executableQuery = neo4jClient.toExecutableQuery(preparedQuery); if (asCollectionQuery) { return executableQuery.getResults(); } else { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 394ce588c..162820cb6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -38,7 +38,7 @@ import org.neo4j.driver.types.Point; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; -import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java index f32f624a0..57e02a5f4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java @@ -38,7 +38,7 @@ import org.neo4j.driver.types.Point; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; -import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java index fbf7512a8..9bc8a50a8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -25,7 +25,7 @@ import java.util.Optional; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index a6742e60f..7f011f1c7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -25,7 +25,7 @@ import java.util.Optional; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.repository.support.PreparedQuery; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java deleted file mode 100644 index 9c47ff13c..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableQuery.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import static java.util.stream.Collectors.*; - -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -import org.apiguardian.api.API; -import org.neo4j.driver.exceptions.NoSuchRecordException; -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.repository.NoResultException; - -/** - * Interface for controlling query execution. - * - * @param The type of the objects returned by this query. - * @author Michael J. Simons - * @soundtrack Deichkind - Niveau weshalb warum - * @since 1.0 - */ -@API(status = API.Status.INTERNAL, since = "1.0") -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public final class ExecutableQuery { - - public static ExecutableQuery create(Neo4jClient neo4jClient, PreparedQuery preparedQuery) { - - Neo4jClient.MappingSpec, Collection, T> mappingSpec = neo4jClient - .newQuery(preparedQuery.getCypherQuery()) - .bindAll(preparedQuery.getParameters()) - .fetchAs(preparedQuery.getResultType()); - Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec = preparedQuery - .getOptionalMappingFunction() - .map(f -> mappingSpec.mappedBy(f)) - .orElse(mappingSpec); - - return new ExecutableQuery<>(preparedQuery, fetchSpec); - } - - private final PreparedQuery preparedQuery; - private final Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec; - - public List getResults() { - return fetchSpec.all().stream().collect(toList()); - } - - public Optional getSingleResult() { - try { - return fetchSpec.one(); - } catch (NoSuchRecordException e) { - // This exception is thrown by the driver in both cases when there are 0 or 1+n records - // So there has been an incorrect result size, but not to few results but to many. - throw new IncorrectResultSizeDataAccessException(1); - } - } - - public T getRequiredSingleResult() { - return fetchSpec.one() - .orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery())); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java deleted file mode 100644 index b9ff8a1da..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ExecutableReactiveQuery.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.repository.support; - -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.apiguardian.api.API; -import org.neo4j.driver.exceptions.NoSuchRecordException; -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; - -/** - * Interface for controlling query execution. - * - * @param The type of the objects returned by this query. - * @author Gerrit Meier - * @author Michael J. Simons - * @since 1.0 - */ -@API(status = API.Status.INTERNAL, since = "1.0") -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public final class ExecutableReactiveQuery { - - public static ExecutableReactiveQuery create(ReactiveNeo4jClient neo4jClient, PreparedQuery preparedQuery) { - - Class resultType = preparedQuery.getResultType(); - Neo4jClient.MappingSpec, Flux, T> mappingSpec = neo4jClient - .newQuery(preparedQuery.getCypherQuery()) - .bindAll(preparedQuery.getParameters()) - .fetchAs(resultType); - - Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec = preparedQuery - .getOptionalMappingFunction() - .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) - .orElse(mappingSpec); - - return new ExecutableReactiveQuery<>(fetchSpec); - } - - private final Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec; - - /** - * @return All results returned by this query. - */ - public Flux getResults() { - return fetchSpec.all(); - } - - /** - * @return A single result - * @throws IncorrectResultSizeDataAccessException - */ - public Mono getSingleResult() { - try { - return fetchSpec.one(); - } catch (NoSuchRecordException e) { - // This exception is thrown by the driver in both cases when there are 0 or 1+n records - // So there has been an incorrect result size, but not to few results but to many. - throw new IncorrectResultSizeDataAccessException(1); - } - } - -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 8b0006f16..0aa1cbe63 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -37,6 +37,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.Neo4jClient.ExecutableQuery; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; @@ -162,7 +164,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) .withCypherQuery(renderer.render(statement)) .build(); - return ExecutableQuery.create(this.neo4jClient, preparedQuery) + return neo4jClient.toExecutableQuery(preparedQuery) .getRequiredSingleResult(); } @@ -235,6 +237,6 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository .withParameters(parameters) .usingMappingFunction(this.entityInformation.getMappingFunction()) .build(); - return ExecutableQuery.create(this.neo4jClient, preparedQuery); + return neo4jClient.toExecutableQuery(preparedQuery); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java index ac10311c9..6d8cf2dfc 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java @@ -38,6 +38,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.Neo4jClient.ExecutableQuery; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; @@ -112,8 +114,7 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { .withCypherQuery(renderer.render(statement)) .withParameters(predicate.getParameters()) .build(); - return ExecutableQuery.create(this.neo4jClient, preparedQuery) - .getRequiredSingleResult(); + return neo4jClient.toExecutableQuery(preparedQuery).getRequiredSingleResult(); } @Override @@ -149,6 +150,6 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { .withParameters(parameters) .usingMappingFunction(mappingFunctionToUse) .build(); - return ExecutableQuery.create(this.neo4jClient, preparedQuery); + return neo4jClient.toExecutableQuery(preparedQuery); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java index cbac67f7a..32191488b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java @@ -32,7 +32,9 @@ import java.util.Optional; import org.reactivestreams.Publisher; import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient.ExecutableQuery; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; @@ -136,7 +138,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) .withCypherQuery(renderer.render(statement)) .build(); - return ExecutableReactiveQuery.create(this.neo4jClient, preparedQuery) + return this.neo4jClient.toExecutableQuery(preparedQuery) .getSingleResult(); } @@ -176,17 +178,17 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< return null; } - private ExecutableReactiveQuery createExecutableQuery(Statement statement) { + private ExecutableQuery createExecutableQuery(Statement statement) { return createExecutableQuery(statement, Collections.emptyMap()); } - private ExecutableReactiveQuery createExecutableQuery(Statement statement, Map parameters) { + private ExecutableQuery createExecutableQuery(Statement statement, Map parameters) { PreparedQuery preparedQuery = PreparedQuery.queryFor(this.entityInformation.getJavaType()) .withCypherQuery(renderer.render(statement)) .withParameters(parameters) .usingMappingFunction(this.entityInformation.getMappingFunction()) .build(); - return ExecutableReactiveQuery.create(neo4jClient, preparedQuery); + return neo4jClient.toExecutableQuery(preparedQuery); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java index 4fcecee9c..b611b11c4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java @@ -34,7 +34,9 @@ import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.domain.Example; import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient.ExecutableQuery; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; @@ -111,18 +113,18 @@ class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleE return findAll(example).hasElements(); } - private ExecutableReactiveQuery createExecutableQuery(Class resultType, Statement statement, + private ExecutableQuery createExecutableQuery(Class resultType, Statement statement, Map parameters) { BiFunction mappingFunctionToUse = this.mappingContext.getMappingFunctionFor(resultType).orElse(null); - PreparedQuery queryDescription = PreparedQuery.queryFor(resultType) + PreparedQuery preparedQuery = PreparedQuery.queryFor(resultType) .withCypherQuery(renderer.render(statement)) .withParameters(parameters) .usingMappingFunction(mappingFunctionToUse) .build(); - return ExecutableReactiveQuery.create(neo4jClient, queryDescription); + return neo4jClient.toExecutableQuery(preparedQuery); } } From 202edd0a730e3672952b91a99f50f041695d8e48 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 28 May 2019 17:18:54 +0200 Subject: [PATCH 099/342] Evolve the Cypher DSL. This adds support for * setting and removing properties of nodes * setting and removing labels of nodes * MERGE, CREATE and UNWIND clauses * starting statements with WITH It also decouples unit and integration tests for the Cypher renderer. --- .../data/neo4j/core/cypher/Create.java | 47 + .../data/neo4j/core/cypher/Cypher.java | 86 +- .../core/cypher/DefaultStatementBuilder.java | 520 ++++-- .../data/neo4j/core/cypher/Delete.java | 2 +- .../neo4j/core/cypher/ExpressionList.java | 2 +- .../data/neo4j/core/cypher/Expressions.java | 6 + .../neo4j/core/cypher/FunctionInvocation.java | 2 +- .../data/neo4j/core/cypher/Functions.java | 20 +- .../neo4j/core/cypher/ListExpression.java | 53 + .../data/neo4j/core/cypher/ListLiteral.java | 12 +- .../data/neo4j/core/cypher/Literal.java | 2 +- .../data/neo4j/core/cypher/MapExpression.java | 11 +- .../data/neo4j/core/cypher/Match.java | 2 +- .../data/neo4j/core/cypher/Merge.java | 48 + .../neo4j/core/cypher/MultiPartQuery.java | 2 +- .../data/neo4j/core/cypher/Node.java | 73 +- .../data/neo4j/core/cypher/NodeLabel.java | 46 + .../data/neo4j/core/cypher/NodeLabels.java | 47 + .../data/neo4j/core/cypher/Operation.java | 49 +- .../data/neo4j/core/cypher/Operations.java | 20 +- .../data/neo4j/core/cypher/Operator.java | 13 +- .../data/neo4j/core/cypher/Order.java | 2 +- .../data/neo4j/core/cypher/Pattern.java | 4 +- .../data/neo4j/core/cypher/Properties.java | 47 + .../data/neo4j/core/cypher/Property.java | 12 +- .../data/neo4j/core/cypher/RegularQuery.java | 2 +- .../data/neo4j/core/cypher/Relationship.java | 2 +- .../neo4j/core/cypher/RelationshipDetail.java | 2 +- .../data/neo4j/core/cypher/Remove.java | 46 + .../data/neo4j/core/cypher/Return.java | 2 +- .../data/neo4j/core/cypher/ReturnBody.java | 2 +- .../data/neo4j/core/cypher/Set.java | 46 + .../neo4j/core/cypher/SinglePartQuery.java | 2 +- .../data/neo4j/core/cypher/Statement.java | 2 +- .../neo4j/core/cypher/StatementBuilder.java | 175 +- .../data/neo4j/core/cypher/SymbolicName.java | 7 +- .../data/neo4j/core/cypher/Unwind.java | 58 + .../data/neo4j/core/cypher/Where.java | 2 +- .../data/neo4j/core/cypher/With.java | 2 +- .../data/neo4j/core/cypher/package-info.java | 8 + .../cypher/renderer/RenderingVisitor.java | 130 +- .../cypher/support/ReflectiveVisitor.java | 23 +- .../core/cypher/support/TypedSubtree.java | 6 +- .../repository/query/CypherAdapterUtils.java | 15 +- .../neo4j/repository/support/Predicate.java | 4 +- .../support/SimpleNeo4jRepository.java | 7 +- .../support/SimpleQueryByExampleExecutor.java | 6 +- .../data/neo4j/core/cypher/CypherIT.java | 1617 +++++++++++++++++ .../data/neo4j/core/cypher/CypherTest.java | 1007 +--------- .../data/neo4j/core/cypher/FunctionsTest.java | 43 + .../data/neo4j/core/cypher/NodeTest.java | 18 +- 51 files changed, 3115 insertions(+), 1247 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Create.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListExpression.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Merge.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabel.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabels.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Properties.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Remove.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Set.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Unwind.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Create.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Create.java new file mode 100644 index 000000000..ba2640ff0 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Create.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * See Create. + * + * @author Michael J. Simons + * @soundtrack Die Ärzte - Seitenhirsch + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Create implements UpdatingClause { + + private final Pattern pattern; + + Create(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + this.pattern.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index 58bc65f9b..3f4e0029b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -20,7 +20,10 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesMatch; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndWith; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingUnwind; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingUpdate; import org.springframework.lang.Nullable; /** @@ -48,6 +51,11 @@ public final class Cypher { return Node.create(primaryLabel, additionalLabels); } + public static Node node(String primaryLabel, MapExpression properties, String... additionalLabels) { + + return Node.create(primaryLabel, properties, additionalLabels); + } + /** * @return A node matching any node. */ @@ -129,11 +137,69 @@ public final class Cypher { * @param pattern The patterns to match * @return An ongoing match that is used to specify an optional where and a required return clause */ - public static OngoingMatchWithoutWhere match(PatternElement... pattern) { + public static OngoingReadingWithoutWhere match(PatternElement... pattern) { return Statement.builder().match(pattern); } + /** + * Starts building a statement based on a {@code CREATE} clause. + * + * @param pattern The patterns to create + * @return An ongoing {@code CREATE} that can be used to specify {@code WITH} and {@code RETURNING} etc. + */ + public static OngoingUpdate create(PatternElement... pattern) { + + return Statement.builder().create(pattern); + } + + /** + * Starts a statement with a leading {@code WITH}. Those are usefull for passing on lists of various type that + * can be unwound later on etc. A leading {@code WITH} cannot be used with patterns obviously and needs its + * arguments to have an alias. + * + * @param expressions One ore more aliased expressions. + * @return An onging with clause. + */ + public static OngoingReadingAndWith with(AliasedExpression... expressions) { + + return Statement.builder().with(expressions); + } + + /** + * Starts building a statement based on a {@code MERGE} clause. + * + * @param pattern The patterns to merge + * @return An ongoing {@code MERGE} that can be used to specify {@code WITH} and {@code RETURNING} etc. + */ + public static OngoingUpdate merge(PatternElement... pattern) { + + return Statement.builder().merge(pattern); + } + + /** + * Starts building a statement starting with an {@code UNWIND} clause. The expression needs to be an expression + * evaluating to a list, otherwise the query will fail. + * + * @param expression The expression to unwind + * @return An ongoing {@code UNWIND}. + */ + public static OngoingUnwind unwind(Expression expression) { + + return Statement.builder().unwind(expression); + } + + /** + * Starts building a statement starting with an {@code UNWIND} clause. The expressions passed will be turned into a + * list expression + * @param expressions + * @return + */ + public static OngoingUnwind unwind(Expression... expressions) { + + return Statement.builder().unwind(Cypher.listOf(expressions)); + } + /** * Creates a new {@link SortItem} to be used as part of an {@link Order}. * @@ -156,6 +222,17 @@ public final class Cypher { return MapExpression.create(keysAndValues); } + /** + * Creates a {@link ListExpression list-expression} from several expressions. + * + * @param expressions + * @return + */ + public static ListExpression listOf(Expression... expressions) { + + return ListExpression.create(expressions); + } + /** * Creates a new {@link NullLiteral} from the given {@code object}. * @@ -163,7 +240,7 @@ public final class Cypher { * @return a new {@link NullLiteral}. * @throws IllegalArgumentException when the object cannot be represented as a literal */ - public static Literal literalOf(@Nullable Object object) { + public static Literal literalOf(@Nullable Object object) { if (object == null) { return NullLiteral.INSTANCE; @@ -174,6 +251,9 @@ public final class Cypher { if (object instanceof Number) { return new NumberLiteral((Number) object); } + if (object instanceof Iterable) { + return new ListLiteral((Iterable>) object); + } throw new IllegalArgumentException("Unsupported literal type: " + object.getClass()); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index fc2331324..5268561df 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -18,14 +18,17 @@ */ package org.springframework.data.neo4j.core.cypher; +import static org.springframework.data.neo4j.core.cypher.DefaultStatementBuilder.UpdateType.*; + import java.util.ArrayList; import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import java.util.Optional; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithWhere; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReading; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingWithWhere; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -37,9 +40,10 @@ import org.springframework.util.Assert; */ class DefaultStatementBuilder implements StatementBuilder, - OngoingMatch, - OngoingMatchWithWhere, - OngoingMatchWithoutWhere { + OngoingReading, + StatementBuilder.OngoingUpdate, + OngoingReadingWithWhere, + OngoingReadingWithoutWhere { /** * Current list of reading or update clauses to be generated. @@ -51,47 +55,90 @@ class DefaultStatementBuilder */ private DefaultMatchBuilder currentOngoingMatch; + /** + * The latest ongoing update to be build + */ + private DefaultStatementWithUpdateBuilder currentOngoingUpdate; + /** * A list of already build withs. */ private final List multiPartElements = new ArrayList<>(); @Override - public OngoingMatchWithoutWhere optionalMatch(PatternElement... pattern) { + public OngoingReadingWithoutWhere optionalMatch(PatternElement... pattern) { return this.match(true, pattern); } @Override - public OngoingMatchWithoutWhere match(PatternElement... pattern) { + public OngoingReadingWithoutWhere match(PatternElement... pattern) { return this.match(false, pattern); } - private OngoingMatchWithoutWhere match(boolean optional, PatternElement... pattern) { + private OngoingReadingWithoutWhere match(boolean optional, PatternElement... pattern) { Assert.notNull(pattern, "Patterns to match are required."); Assert.notEmpty(pattern, "At least one pattern to match is required."); + if (this.currentOngoingMatch != null) { this.currentSinglePartElements.add(this.currentOngoingMatch.buildMatch()); } this.currentOngoingMatch = new DefaultMatchBuilder(optional); - this.currentOngoingMatch.matchList.addAll(Arrays.asList(pattern)); + this.currentOngoingMatch.patternList.addAll(Arrays.asList(pattern)); return this; } @Override - public OngoingMatchAndReturn returning(Expression... expressions) { + public OngoingUpdate create(PatternElement... pattern) { + + return update(CREATE, pattern); + } + + @Override + public OngoingUpdate merge(PatternElement... pattern) { + + return update(MERGE, pattern); + } + + @Override + public OngoingUnwind unwind(Expression expression) { + return new DefaultUnwindBuilder(expression); + } + + private OngoingUpdate update(UpdateType updateType, PatternElement... pattern) { + + Assert.notNull(pattern, "Patterns to create are required."); + Assert.notEmpty(pattern, "At least one pattern to create is required."); + Assert.isTrue(MERGE_OR_CREATE.contains(updateType), + "Only CREATE and MERGE clauses can be used without a preceding reading clause."); + + if (this.currentOngoingMatch != null) { + this.currentSinglePartElements.add(this.currentOngoingMatch.buildMatch()); + } + this.currentOngoingMatch = null; + + if (this.currentOngoingUpdate != null) { + this.currentSinglePartElements.add(this.currentOngoingUpdate.buildUpdatingClause()); + } + + this.currentOngoingUpdate = new DefaultStatementWithUpdateBuilder(updateType, pattern); + return this; + } + + @Override + public OngoingReadingAndReturn returning(Expression... expressions) { return returning(false, expressions); } @Override - public OngoingMatchAndReturn returningDistinct(Expression... expressions) { + public OngoingReadingAndReturn returningDistinct(Expression... expressions) { return returning(true, expressions); } - private OngoingMatchAndReturn returning(boolean distinct, Expression... expressions) { + private OngoingReadingAndReturn returning(boolean distinct, Expression... expressions) { DefaultStatementWithReturnBuilder ongoingMatchAndReturn = new DefaultStatementWithReturnBuilder(distinct); ongoingMatchAndReturn.addExpressions(expressions); @@ -99,18 +146,23 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndWithWithoutWhere with(Expression... expressions) { + public OngoingReadingAndWith with(AliasedExpression... expressions) { + return with(false, expressions); + } + + @Override + public OngoingReadingAndWithWithoutWhere with(Expression... expressions) { return with(false, expressions); } @Override - public OngoingMatchAndWithWithoutWhere withDistinct(Expression... expressions) { + public OngoingReadingAndWithWithoutWhere withDistinct(Expression... expressions) { return with(true, expressions); } - private OngoingMatchAndWithWithoutWhere with(boolean distinct, Expression... expressions) { + private OngoingReadingAndWithWithoutWhere with(boolean distinct, Expression... expressions) { DefaultStatementWithWithBuilder ongoingMatchAndWith = new DefaultStatementWithWithBuilder(distinct); ongoingMatchAndWith.addExpressions(expressions); @@ -118,53 +170,99 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndDelete delete(Expression... expressions) { + public OngoingMatchAndUpdate delete(Expression... expressions) { - return new DefaultStatementWithDeleteBuilder(false, expressions); + return new DefaultStatementWithUpdateBuilder(DELETE, expressions); } @Override - public OngoingMatchAndDelete detachDelete(Expression... expressions) { + public OngoingMatchAndUpdate detachDelete(Expression... expressions) { - return new DefaultStatementWithDeleteBuilder(true, expressions); + return new DefaultStatementWithUpdateBuilder(DETACH_DELETE, expressions); } @Override - public OngoingMatchWithWhere where(Condition newCondition) { + public OngoingMatchAndUpdate set(Expression... expressions) { + + return new DefaultStatementWithUpdateBuilder(SET, expressions); + } + + @Override + public OngoingMatchAndUpdate set(Node named, String... label) { + + return new DefaultStatementWithUpdateBuilder(SET, Operations.set(named, label)); + } + + @Override + public OngoingMatchAndUpdate remove(Property... properties) { + + return new DefaultStatementWithUpdateBuilder(REMOVE, properties); + } + + @Override + public OngoingMatchAndUpdate remove(Node named, String... label) { + + return new DefaultStatementWithUpdateBuilder(REMOVE, Operations.set(named, label)); + } + + @Override + public OngoingReadingWithWhere where(Condition newCondition) { this.currentOngoingMatch.conditionBuilder.where(newCondition); return this; } @Override - public OngoingMatchWithWhere and(Condition additionalCondition) { + public OngoingReadingWithWhere and(Condition additionalCondition) { this.currentOngoingMatch.conditionBuilder.and(additionalCondition); return this; } @Override - public OngoingMatchWithWhere or(Condition additionalCondition) { + public OngoingReadingWithWhere or(Condition additionalCondition) { this.currentOngoingMatch.conditionBuilder.or(additionalCondition); return this; } - protected final List buildMatchList() { - List completeMatchesList = new ArrayList(this.currentSinglePartElements); + @Override + public Statement build() { + + return buildImpl(null); + } + + protected Statement buildImpl(@Nullable Return returning) { + SinglePartQuery singlePartQuery = SinglePartQuery.create(buildListOfVisitables(), returning); + + if (multiPartElements.isEmpty()) { + return singlePartQuery; + } else { + return new MultiPartQuery(multiPartElements, singlePartQuery); + } + } + + protected final List buildListOfVisitables() { + + List visitables = new ArrayList(this.currentSinglePartElements); + if (this.currentOngoingMatch != null) { - completeMatchesList.add(this.currentOngoingMatch.buildMatch()); + visitables.add(this.currentOngoingMatch.buildMatch()); } this.currentOngoingMatch = null; + + if (this.currentOngoingUpdate != null) { + visitables.add(this.currentOngoingUpdate.buildUpdatingClause()); + } + this.currentOngoingUpdate = null; + this.currentSinglePartElements.clear(); - return completeMatchesList; + return visitables; } protected final DefaultStatementBuilder addWith(Optional optionalWith) { - optionalWith.ifPresent(with -> { - multiPartElements.add(new MultiPartElement(buildMatchList(), with)); - }); + optionalWith.ifPresent(with -> multiPartElements.add(new MultiPartElement(buildListOfVisitables(), with))); return this; } @@ -181,7 +279,7 @@ class DefaultStatementBuilder } protected class DefaultStatementWithReturnBuilder - implements OngoingMatchAndReturn, OngoingOrderDefinition, OngoingMatchAndReturnWithOrder { + implements OngoingReadingAndReturn, OngoingOrderDefinition, OngoingMatchAndReturnWithOrder { protected final List returnList = new ArrayList<>(); protected final List sortItemList = new ArrayList<>(); @@ -195,7 +293,7 @@ class DefaultStatementBuilder } @Override - public final OngoingMatchAndReturn orderBy(SortItem... sortItem) { + public final OngoingReadingAndReturn orderBy(SortItem... sortItem) { Arrays.stream(sortItem).forEach(this.sortItemList::add); return this; } @@ -212,21 +310,21 @@ class DefaultStatementBuilder } @Override - public final OngoingMatchAndReturn descending() { + public final OngoingReadingAndReturn descending() { this.sortItemList.add(this.lastSortItem.descending()); this.lastSortItem = null; return this; } @Override - public final OngoingMatchAndReturn ascending() { + public final OngoingReadingAndReturn ascending() { this.sortItemList.add(this.lastSortItem.ascending()); this.lastSortItem = null; return this; } @Override - public final OngoingMatchAndReturn skip(@Nullable Number number) { + public final OngoingReadingAndReturn skip(@Nullable Number number) { if (number != null) { skip = Skip.create(number); @@ -235,7 +333,7 @@ class DefaultStatementBuilder } @Override - public final OngoingMatchAndReturn limit(@Nullable Number number) { + public final OngoingReadingAndReturn limit(@Nullable Number number) { if (number != null) { limit = Limit.create(number); @@ -245,13 +343,20 @@ class DefaultStatementBuilder @Override public Statement build() { - SinglePartQuery singlePartQuery = SinglePartQuery.create(buildMatchList(), buildReturn().orElse(null)); - if (multiPartElements.isEmpty()) { - return singlePartQuery; - } else { - return new MultiPartQuery(multiPartElements, singlePartQuery); + Return returning = null; + if (!returnList.isEmpty()) { + + ExpressionList returnItems = new ExpressionList(this.returnList); + + if (lastSortItem != null) { + sortItemList.add(lastSortItem); + } + Order order = sortItemList.size() > 0 ? new Order(sortItemList) : null; + returning = new Return(distinct, returnItems, order, skip, limit); } + + return DefaultStatementBuilder.this.buildImpl(returning); } protected final void addExpressions(Expression... expressions) { @@ -261,21 +366,6 @@ class DefaultStatementBuilder this.returnList.addAll(Arrays.asList(expressions)); } - - protected final Optional buildReturn() { - - if (returnList.isEmpty()) { - return Optional.empty(); - } - - ExpressionList returnItems = new ExpressionList(this.returnList); - - if (lastSortItem != null) { - sortItemList.add(lastSortItem); - } - Order order = sortItemList.size() > 0 ? new Order(sortItemList) : null; - return Optional.of(new Return(distinct, returnItems, order, skip, limit)); - } } /** @@ -310,14 +400,14 @@ class DefaultStatementBuilder * Ongoing with extends from {@link WithBuilderSupport} and therefore from {@Defaultd} */ protected final class DefaultStatementWithWithBuilder extends WithBuilderSupport - implements OngoingMatchAndWithWithoutWhere, OngoingMatchAndWithWithWhere { + implements OngoingReadingAndWithWithoutWhere, OngoingReadingAndWithWithWhere { protected DefaultStatementWithWithBuilder(boolean distinct) { super(distinct); } @Override - public OngoingMatchAndReturn returning(Expression... expressions) { + public OngoingReadingAndReturn returning(Expression... expressions) { return DefaultStatementBuilder.this .addWith(buildWith()) @@ -325,7 +415,7 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndReturn returningDistinct(Expression... expressions) { + public OngoingReadingAndReturn returningDistinct(Expression... expressions) { return DefaultStatementBuilder.this .addWith(buildWith()) @@ -333,7 +423,7 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndDelete delete(Expression... expressions) { + public OngoingMatchAndUpdate delete(Expression... expressions) { return DefaultStatementBuilder.this .addWith(buildWith()) @@ -341,7 +431,7 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndDelete detachDelete(Expression... expressions) { + public OngoingMatchAndUpdate detachDelete(Expression... expressions) { return DefaultStatementBuilder.this .addWith(buildWith()) @@ -349,7 +439,39 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndWithWithoutWhere with(Expression... expressions) { + public OngoingMatchAndUpdate set(Expression... expressions) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .set(expressions); + } + + @Override + public OngoingMatchAndUpdate set(Node node, String... label) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .set(node, label); + } + + @Override + public OngoingMatchAndUpdate remove(Node node, String... label) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .remove(node, label); + } + + @Override + public OngoingMatchAndUpdate remove(Property... properties) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .remove(properties); + } + + @Override + public OngoingReadingAndWithWithoutWhere with(Expression... expressions) { return DefaultStatementBuilder.this .addWith(buildWith()) @@ -357,7 +479,7 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndWithWithoutWhere withDistinct(Expression... expressions) { + public OngoingReadingAndWithWithoutWhere withDistinct(Expression... expressions) { return DefaultStatementBuilder.this .addWith(buildWith()) @@ -365,28 +487,28 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndWithWithWhere where(Condition newCondition) { + public OngoingReadingAndWithWithWhere where(Condition newCondition) { super.conditionBuilder.where(newCondition); return this; } @Override - public OngoingMatchAndWithWithWhere and(Condition additionalCondition) { + public OngoingReadingAndWithWithWhere and(Condition additionalCondition) { super.conditionBuilder.and(additionalCondition); return this; } @Override - public OngoingMatchAndWithWithWhere or(Condition additionalCondition) { + public OngoingReadingAndWithWithWhere or(Condition additionalCondition) { super.conditionBuilder.or(additionalCondition); return this; } @Override - public OngoingMatchWithoutWhere match(PatternElement... pattern) { + public OngoingReadingWithoutWhere match(PatternElement... pattern) { return DefaultStatementBuilder.this .addWith(buildWith()) @@ -394,91 +516,224 @@ class DefaultStatementBuilder } @Override - public OngoingMatchWithoutWhere optionalMatch(PatternElement... pattern) { + public OngoingReadingWithoutWhere optionalMatch(PatternElement... pattern) { return DefaultStatementBuilder.this .addWith(buildWith()) .optionalMatch(pattern); } - } - protected final class DefaultStatementWithDeleteBuilder extends WithBuilderSupport - implements OngoingMatchAndDelete { + @Override + public OngoingUpdate create(PatternElement... pattern) { - private final List deleteList; - private final boolean detach; - - protected DefaultStatementWithDeleteBuilder(boolean detach, Expression... expressions) { - super(false); - this.detach = detach; - - Assert.notNull(expressions, "Expressions to delete are required."); - Assert.notEmpty(expressions, "At least one expressions to delete is required."); - - this.deleteList = Arrays.asList(expressions); + return DefaultStatementBuilder.this + .addWith(buildWith()) + .create(pattern); } @Override - public OngoingMatchAndReturn returning(Expression... expressions) { + public OngoingUpdate merge(PatternElement... pattern) { - Assert.notNull(expressions, "Expressions to return are required."); - Assert.notEmpty(expressions, "At least one expressions to return is required."); + return DefaultStatementBuilder.this + .addWith(buildWith()) + .merge(pattern); + } - super.returnList.addAll(Arrays.asList(expressions)); + @Override + public OngoingUnwind unwind(Expression expression) { + + return DefaultStatementBuilder.this + .addWith(buildWith()) + .unwind(expression); + } + } + + /** + * A private enum for distinguishing updating clauses. + */ + enum UpdateType { + DELETE, DETACH_DELETE, SET, REMOVE, + CREATE, MERGE; + } + + private static final EnumSet MERGE_OR_CREATE = EnumSet.of(CREATE, MERGE); + + protected final class DefaultStatementWithUpdateBuilder extends WithBuilderSupport + implements OngoingMatchAndUpdate { + + private final List expressions; + private final UpdateType updateType; + + protected DefaultStatementWithUpdateBuilder(UpdateType updateType, PatternElement... pattern) { + super(false); + + this.updateType = updateType; + + Assert.notNull(pattern, "Patterns to create are required."); + Assert.notEmpty(pattern, "At least one pattern to create is required."); + + this.expressions = Arrays.asList(pattern); + } + + protected DefaultStatementWithUpdateBuilder(UpdateType updateType, Expression... expressions) { + super(false); + + this.updateType = updateType; + + Assert.notNull(expressions, "Modifying expressions are required."); + Assert.notEmpty(expressions, "At least one expressions is required."); + + switch (this.updateType) { + case DETACH_DELETE: + case DELETE: + this.expressions = Arrays.asList(expressions); + break; + case SET: + this.expressions = prepareSetExpressions(expressions); + break; + case REMOVE: + this.expressions = Arrays.asList(expressions); + break; + default: + throw new IllegalArgumentException("Unsupported update type " + updateType); + } + } + + List prepareSetExpressions(Expression... possibleSetOperations) { + List propertyOperations = new ArrayList<>(); + + List listOfExpressions = new ArrayList<>(); + for (Expression possibleSetOperation : possibleSetOperations) { + if (possibleSetOperation instanceof Operation) { + propertyOperations.add(possibleSetOperation); + } else { + listOfExpressions.add(possibleSetOperation); + } + + } + + if (listOfExpressions.size() % 2 != 0) { + throw new IllegalArgumentException("The list of expression to set must be even."); + } + for (int i = 0; i < listOfExpressions.size(); i += 2) { + propertyOperations.add(Operations.set(listOfExpressions.get(i), listOfExpressions.get(i + 1))); + } + + return propertyOperations; + } + + @Override + public OngoingReadingAndReturn returning(Expression... returnedExpressions) { + + Assert.notNull(returnedExpressions, "Expressions to return are required."); + Assert.notEmpty(returnedExpressions, "At least one expressions to return is required."); + + super.returnList.addAll(Arrays.asList(returnedExpressions)); return this; } @Override - public OngoingMatchAndReturn returningDistinct(Expression... expressions) { + public OngoingReadingAndReturn returningDistinct(Expression... returnedExpressions) { - returning(expressions); + returning(returnedExpressions); super.distinct = true; return this; } + @Override + public OngoingMatchAndUpdate delete(Expression... deletedExpressions) { + return delete(false, deletedExpressions); + } + + @Override + public OngoingMatchAndUpdate detachDelete(Expression... deletedExpressions) { + return delete(true, deletedExpressions); + } + + private OngoingMatchAndUpdate delete(boolean nextDetach, Expression... deletedExpressions) { + DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); + return DefaultStatementBuilder.this.new DefaultStatementWithUpdateBuilder( + nextDetach ? DETACH_DELETE : DELETE, deletedExpressions); + } + + @Override + public OngoingMatchAndUpdate set(Expression... keyValuePairs) { + + DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); + return DefaultStatementBuilder.this.new DefaultStatementWithUpdateBuilder(SET, keyValuePairs); + } + + @Override + public OngoingMatchAndUpdate set(Node node, String... label) { + + DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); + return DefaultStatementBuilder.this.new DefaultStatementWithUpdateBuilder(SET, Operations.set(node, label)); + } + + @Override + public OngoingMatchAndUpdate remove(Node node, String... label) { + + DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); + return DefaultStatementBuilder.this.new DefaultStatementWithUpdateBuilder(REMOVE, + Operations.set(node, label)); + } + + @Override + public OngoingMatchAndUpdate remove(Property... properties) { + + DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); + return DefaultStatementBuilder.this.new DefaultStatementWithUpdateBuilder(REMOVE, properties); + } + + @Override + public OngoingReadingAndWithWithoutWhere with(Expression... returnedExpressions) { + return this.with(false, returnedExpressions); + } + + @Override + public OngoingReadingAndWithWithoutWhere withDistinct(Expression... returnedExpressions) { + return this.with(true, returnedExpressions); + } + + private OngoingReadingAndWithWithoutWhere with(boolean distinct, Expression... returnedExpressions) { + DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); + return DefaultStatementBuilder.this + .addWith(buildWith()) + .with(distinct, returnedExpressions); + } + @Override public Statement build() { - DefaultStatementBuilder.this.addUpdatingClause(buildDelete()); + DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); return super.build(); } - @Override - public OngoingMatchAndDelete delete(Expression... expressions) { - return delete(false, expressions); - } + private UpdatingClause buildUpdatingClause() { - @Override - public OngoingMatchAndDelete detachDelete(Expression... expressions) { - return delete(true, expressions); - } + if (MERGE_OR_CREATE.contains(updateType)) { + final Pattern pattern = new Pattern(this.expressions); + switch (updateType) { + case CREATE: + return new Create(pattern); + case MERGE: + return new Merge(pattern); + } + } else { + final ExpressionList expressionsList = new ExpressionList(this.expressions); + switch (updateType) { + case DETACH_DELETE: + return new Delete(expressionsList, true); + case DELETE: + return new Delete(expressionsList, false); + case SET: + return new Set(expressionsList); + case REMOVE: + return new Remove(expressionsList); + } + } - private OngoingMatchAndDelete delete(boolean nextDetach, Expression... expressions) { - DefaultStatementBuilder.this.addUpdatingClause(buildDelete()); - return DefaultStatementBuilder.this.new DefaultStatementWithDeleteBuilder(nextDetach, expressions); - } - - @Override - public OngoingMatchAndWithWithoutWhere with(Expression... expressions) { - return this.with(false, expressions); - } - - @Override - public OngoingMatchAndWithWithoutWhere withDistinct(Expression... expressions) { - return this.with(true, expressions); - } - - private OngoingMatchAndWithWithoutWhere with(boolean distinct, Expression... expressions) { - DefaultStatementBuilder.this.addUpdatingClause(buildDelete()); - return DefaultStatementBuilder.this - .addWith(buildWith()) - .with(distinct, expressions); - } - - private Delete buildDelete() { - - ExpressionList deleteItems = new ExpressionList(this.deleteList); - return new Delete(deleteItems, this.detach); + throw new IllegalArgumentException("Unsupported update type " + updateType); } } @@ -486,7 +741,7 @@ class DefaultStatementBuilder static final class DefaultMatchBuilder { - private final List matchList = new ArrayList<>(); + private final List patternList = new ArrayList<>(); private final DefaultConditionBuilder conditionBuilder = new DefaultConditionBuilder(); @@ -497,11 +752,26 @@ class DefaultStatementBuilder } Match buildMatch() { - Pattern pattern = new Pattern(this.matchList); + Pattern pattern = new Pattern(this.patternList); return new Match(optional, pattern, conditionBuilder.buildCondition().map(Where::new).orElse(null)); } } + final class DefaultUnwindBuilder implements OngoingUnwind { + + private final Expression expressionToUnwind; + + DefaultUnwindBuilder(Expression expressionToUnwind) { + this.expressionToUnwind = expressionToUnwind; + } + + @Override + public OngoingReading as(String variable) { + DefaultStatementBuilder.this.currentSinglePartElements.add(new Unwind(expressionToUnwind, variable)); + return DefaultStatementBuilder.this; + } + } + static final class DefaultConditionBuilder { protected Condition condition; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java index d65efa5e6..0f25e353b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java @@ -22,7 +22,7 @@ import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** - * See Delete. + * See Delete. * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java index 372d650d9..04c78961e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java @@ -32,7 +32,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; * @author Michael J. Simons * @since 1.0 */ -class ExpressionList extends TypedSubtree { +class ExpressionList> extends TypedSubtree { ExpressionList(List returnItems) { super(returnItems); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java index da35e3f3c..7de07547b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.cypher; +import java.util.Arrays; + import org.apiguardian.api.API; /** @@ -42,6 +44,10 @@ public final class Expressions { } } + static Expression[] createSymbolicNames(String[] variables) { + return Arrays.stream(variables).map(SymbolicName::new).toArray(Expression[]::new); + } + private Expressions() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java index 2ef66fc4c..f738908d4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java @@ -23,7 +23,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; /** * See - * FunctionInvocation + * FunctionInvocation * * @author Gerrit Meier * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java index 46266ac36..4044e89b7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java @@ -131,7 +131,7 @@ public final class Functions { /** * Creates a function invocation for the {@code exists()} function. - * See exists + * See exists. * * @param expression The expression whos existence is to be evaluated * @return A function call for {@code exists()} for one expression @@ -145,7 +145,7 @@ public final class Functions { /** * Creates a function invocation for the {@code distance()} function. - * See exists + * See exists. * Both points need to be in the same coordinate system. * * @param point1 Point 1 @@ -162,7 +162,7 @@ public final class Functions { /** * Creates a function invocation for the {@code point()} function. - * See point + * See point. * * @param parameterMap The map of parameters for {@code point()} * @return A function call for {@code point()} @@ -174,6 +174,20 @@ public final class Functions { return new FunctionInvocation("point", parameterMap); } + /** + * Creates a function invocation for the {@code collect()} function. + * See collect. + * + * @param expression The things to collect + * @return A function call for {@code collect()} + */ + public static FunctionInvocation collect(Expression expression) { + + Assert.notNull(expression, "The expression to collect is required."); + + return new FunctionInvocation("collect", expression); + } + private Functions() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListExpression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListExpression.java new file mode 100644 index 000000000..43ae8fc55 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListExpression.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * Represents a list expression in contrast to an {@link ExpressionList} which is a list of expressions. The list expression + * here uses an expression list for it's content. + * + * @author Michael J. Simons + * @soundtrack Queen - Jazz + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class ListExpression implements Expression { + + static ListExpression create(Expression... expressions) { + + return new ListExpression(new ExpressionList<>(expressions)); + } + + private final ExpressionList content; + + private ListExpression(ExpressionList content) { + this.content = content; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + this.content.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java index cdca7f2ae..b9ec5c984 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java @@ -18,7 +18,8 @@ */ package org.springframework.data.neo4j.core.cypher; -import java.util.stream.Collectors; +import static java.util.stream.Collectors.*; + import java.util.stream.StreamSupport; import org.apiguardian.api.API; @@ -29,17 +30,16 @@ import org.apiguardian.api.API; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class ListLiteral extends Literal { +public final class ListLiteral extends Literal>> { - ListLiteral(Iterable content) { + ListLiteral(Iterable> content) { super(content); } @Override public String asString() { - return "[" - + StreamSupport.stream(getContent().spliterator(), false).map(Object::toString).collect(Collectors.joining(",")) - + "]"; + return StreamSupport.stream(getContent().spliterator(), false).map(Literal::asString).collect( + joining(", ", "[", "]")); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java index 05e9d56a3..cb17367aa 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java @@ -49,7 +49,7 @@ public abstract class Literal implements Expression { /** * The string representation should be designed in such a way the a renderer can use it correctly in * the given context of the literal, i.e. a literal containing a string should quote that string - * and escape all reservered characters. + * and escape all reserved characters. * * @return A string representation to be used literally in a cypher statement. */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java index 938f92760..aa9209b64 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java @@ -30,16 +30,17 @@ import org.springframework.util.Assert; /** * A dedicated map expression.

* Most of the comparision methods on this expression will not result in a sensible query fragment. - * A {@link MapExpression} will be useful as a concrete parameter to functions. + * A {@link MapExpression} is be useful as a concrete parameter to functions or as properties on {@link Node nodes} + * or {@link Relationship relationships}. * * @author Michael J. Simons * @soundtrack Rammstein - RAMMSTEIN * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class MapExpression extends TypedSubtree implements Expression { +public final class MapExpression> extends TypedSubtree implements Expression { - static MapExpression create(Object... input) { + static MapExpression create(Object... input) { Assert.state(input.length % 2 == 0, "Need an even number of input parameters"); List newContent = new ArrayList<>(input.length / 2); @@ -55,10 +56,10 @@ public final class MapExpression extends TypedSubtree implements Expre knownKeys.add(entry.getKey()); } - return new MapExpression(newContent); + return new MapExpression<>(newContent); } - public MapExpression(List children) { + private MapExpression(List children) { super(children); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java index 5b1ae2adb..572f44597 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java @@ -24,7 +24,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; /** - * See Match. + * See Match. * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Merge.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Merge.java new file mode 100644 index 000000000..f4c0d0bec --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Merge.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * See Create. + * + * @author Michael J. Simons + * @soundtrack Die Ärzte - Seitenhirsch + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Merge implements UpdatingClause { + + private final Pattern pattern; + + Merge(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + this.pattern.accept(visitor); + visitor.leave(this); + } +} + diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java index 6ff99da6c..ecd266084 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java @@ -26,7 +26,7 @@ import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** - * See MultiPartQuery. + * See MultiPartQuery. * * @author Michael J. Simons * @soundtrack Ferris MC - Ferris MC's Audiobiographie diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java index 48086bd9c..55b5bb3f2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java @@ -18,9 +18,12 @@ */ package org.springframework.data.neo4j.core.cypher; +import static java.util.stream.Collectors.*; + +import lombok.ToString; + import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -32,52 +35,62 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * See NodePattern. + * See NodePattern. * * @author Michael J. Simons * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") +@ToString(of = { "symbolicName", "labels" }) public final class Node implements PatternElement, Named, Expression, ExposesRelationships { static Node create(String primaryLabel, String... additionalLabels) { + return create(primaryLabel, null, additionalLabels); + } + + static Node create(String primaryLabel, MapExpression properties, String... additionalLabels) { + Assert.hasText(primaryLabel, "A primary label is required."); for (String additionalLabel : additionalLabels) { Assert.hasText(additionalLabel, "An empty label is not allowed."); } - return new Node(primaryLabel, additionalLabels); + return new Node(primaryLabel, properties == null ? null : new Properties(properties), additionalLabels); } /** - * @return A node without labels + * @return A node without labels and properties */ static Node create() { - return new Node(null); + return new Node(null, null); } private @Nullable final SymbolicName symbolicName; - private final List labels; + private final List labels; - Node(String primaryLabel, String... additionalLabels) { + private @Nullable final Properties properties; + + private Node(String primaryLabel, Properties properties, String... additionalLabels) { this.symbolicName = null; this.labels = new ArrayList<>(); if (!(primaryLabel == null || primaryLabel.isEmpty())) { - this.labels.add(primaryLabel); + this.labels.add(new NodeLabel(primaryLabel)); } - this.labels.addAll(Arrays.asList(additionalLabels)); + this.labels.addAll(Arrays.stream(additionalLabels).map(NodeLabel::new).collect(toList())); + this.properties = properties; } - Node(SymbolicName symbolicName, List labels) { + private Node(SymbolicName symbolicName, Properties properties, List labels) { this.symbolicName = symbolicName; this.labels = new ArrayList<>(labels); + this.properties = properties; } /** @@ -89,21 +102,41 @@ public final class Node implements PatternElement, Named, Expression, ExposesRel public Node named(String newSymbolicName) { Assert.hasText(newSymbolicName, "Symbolic name is required."); - return new Node(new SymbolicName(newSymbolicName), labels); + return new Node(new SymbolicName(newSymbolicName), properties, labels); + } + + /** + * Creates a a copy of this node with additional properties. Creates a node without properties when no properties + * * are passed to this method. + * + * @param newProperties the new properties + * @return The new node. + */ + public Node properties(@Nullable MapExpression newProperties) { + + return new Node(this.symbolicName, newProperties == null ? null : new Properties(newProperties), labels); + } + + /** + * Creates a a copy of this node with additional properties. Creates a node without properties when no properties + * are passed to this method. + * + * @param keysAndValues A list of key and values. Must be an even number, with alternating {@link String} and {@link Expression}. + * @return The new node. + */ + public Node properties(Object... keysAndValues) { + + MapExpression newProperties = null; + if (keysAndValues != null && keysAndValues.length != 0) { + newProperties = MapExpression.create(keysAndValues); + } + return properties(newProperties); } public Optional getSymbolicName() { return Optional.ofNullable(symbolicName); } - public List getLabels() { - return Collections.unmodifiableList(labels); - } - - public boolean isLabeled() { - return !this.labels.isEmpty(); - } - /** * Creates a new {@link Property} associated with this property container.. *

@@ -142,6 +175,8 @@ public final class Node implements PatternElement, Named, Expression, ExposesRel visitor.enter(this); Visitable.visitIfNotNull(this.symbolicName, visitor); + this.labels.forEach(label -> label.accept(visitor)); + Visitable.visitIfNotNull(this.properties, visitor); visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabel.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabel.java new file mode 100644 index 000000000..6055cfecc --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabel.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import lombok.ToString; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; + +/** + * Expression for a single Node label. + * + * @author Michael J. Simons + * @soundtrack Richard Gibbs - Battlestar Galactica OST + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +@ToString(of = { "value" }) +public final class NodeLabel implements Visitable { + + private final String value; + + NodeLabel(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabels.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabels.java new file mode 100644 index 000000000..58cc57709 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabels.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import java.util.List; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * Makes a list of {@link NodeLabel node labels} visitable. Only used during setting and removal of labels on nodes. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class NodeLabels implements Visitable { + + private final List values; + + NodeLabels(List values) { + this.values = values; + } + + public void accept(Visitor visitor) { + visitor.enter(this); + values.forEach(value -> value.accept(visitor)); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java index e3d3a17e9..07a2263f9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java @@ -18,7 +18,14 @@ */ package org.springframework.data.neo4j.core.cypher; +import static java.util.stream.Collectors.*; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; + import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.util.Assert; @@ -31,6 +38,13 @@ import org.springframework.util.Assert; @API(status = API.Status.INTERNAL, since = "1.0") public final class Operation implements Expression { + /** + * A set of operators triggering operations on labels. + */ + private static final EnumSet LABEL_OPERATORS = EnumSet.of(Operator.SET_LABEL); + private static final EnumSet NEEDS_GROUPING = EnumSet + .complementOf(EnumSet.of(Operator.Type.PROPERTY, Operator.Type.LABEL)); + static Operation create(Expression op1, Operator operator, Expression op2) { Assert.notNull(op1, "The first operand must not be null."); @@ -40,11 +54,31 @@ public final class Operation implements Expression { return new Operation(op1, operator, op2); } + static Operation create(Node op1, Operator operator, String... nodeLabels) { + + Assert.notNull(op1, "The first operand must not be null."); + Assert.state(op1.getSymbolicName().isPresent(), "The node must have a name."); + Assert.state(LABEL_OPERATORS.contains(operator), + () -> String.format("Only operators %s can be used to modify labels", + LABEL_OPERATORS)); + Assert.notEmpty(nodeLabels, "The labels cannot be empty."); + + List listOfNodeLabels = Arrays.stream(nodeLabels).map(NodeLabel::new).collect(toList()); + return new Operation(op1, operator, new NodeLabels(listOfNodeLabels)); + } + private final Expression left; private final Operator operator; - private final Expression right; + private final Visitable right; - private Operation(Expression left, Operator operator, Expression right) { + Operation(Expression left, Operator operator, Expression right) { + + this.left = left; + this.operator = operator; + this.right = right; + } + + Operation(Expression left, Operator operator, NodeLabels right) { this.left = left; this.operator = operator; @@ -55,9 +89,18 @@ public final class Operation implements Expression { public void accept(Visitor visitor) { visitor.enter(this); - left.accept(visitor); + Expressions.nameOrExpression(left).accept(visitor); operator.accept(visitor); right.accept(visitor); visitor.leave(this); } + + /** + * Checks, whether this operation needs grouping. + * + * @return True, if this operation needs grouping. + */ + public boolean needsGrouping() { + return NEEDS_GROUPING.contains(this.operator.getType()); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java index 5ad102f14..8a8283c3e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java @@ -27,7 +27,7 @@ import org.apiguardian.api.API; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class Operations { +final class Operations { /** * Creates a {@code +} operation. Both operands must evaluate to a compatible type. @@ -41,6 +41,24 @@ public final class Operations { return Operation.create(op1, Operator.ADDITION, op2); } + /** + * Creates a {@code =} operation. The left hand side should resolve to a property or to something which has labels + * or types to modify and the right hand side should either be new properties or labels. + * + * @param target The target that should be modified + * @param value The new value of the target + * @return A new operation. + */ + static Operation set(Expression target, Expression value) { + + return Operation.create(target, Operator.SET, value); + } + + static Operation set(Node target, String... label) { + + return Operation.create(target, Operator.SET_LABEL, label); + } + /** * Not to be instantiated. */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java index 3505f3f65..79dd7e2f9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java @@ -58,7 +58,16 @@ public enum Operator implements Visitable { MATCHES("=~"), // List operators - IN("IN"); + IN("IN"), + + // Property operators + SET("=", Type.PROPERTY), + GET(".", Type.PROPERTY), + MUTATE("+=", Type.PROPERTY), + + // Node operators + SET_LABEL("", Type.LABEL), + REMOVE_LABEL("", Type.LABEL); private final String representation; @@ -89,5 +98,7 @@ public enum Operator implements Visitable { BINARY, PREFIX, POSTFIX, + PROPERTY, + LABEL } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java index 0d9e92435..fdd85cf66 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java @@ -31,7 +31,7 @@ import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class Order extends TypedSubtree { +public final class Order> extends TypedSubtree { Order(List sortItems) { super(sortItems); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java index 45b32915d..2d47ddd60 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java @@ -27,13 +27,13 @@ import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; * A pattern is something that can be matched. It consists of one or more pattern elements. Those can be nodes or chains * of nodes and relationships. *

- * See Pattern. + * See Pattern. * * @author Michael J. Simons * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public final class Pattern extends TypedSubtree { +public final class Pattern> extends TypedSubtree { Pattern(List patternElements) { super(patternElements); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Properties.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Properties.java new file mode 100644 index 000000000..d6ce3e020 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Properties.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * Represents the properties of a {@link Node node} or a {@link Relationship relationship}. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Properties implements Visitable { + + private final MapExpression properties; + + Properties(MapExpression properties) { + this.properties = properties; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + this.properties.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java index b317c7021..2169450c4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java @@ -50,7 +50,7 @@ public final class Property implements Expression { } /** - * The expression describinng the container. + * The expression describing the container. */ private final Expression container; @@ -69,6 +69,16 @@ public final class Property implements Expression { return name; } + /** + * Creates a {@link Operation} setting this property to a new value. + * + * @param expression + * @return + */ + public Operation to(Expression expression) { + return Operations.set(this, expression); + } + @Override public void accept(Visitor visitor) { visitor.enter(this); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java index 0e464ea5f..e62cab0df 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java @@ -21,7 +21,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; /** - * See RegularQuery. + * See RegularQuery. * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java index 02837e773..4b935423c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java @@ -27,7 +27,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * See RelationshipPattern. + * See RelationshipPattern. * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java index ff2aef843..c6c806f4e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java @@ -32,7 +32,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * See RelationshipDetail + * See RelationshipDetail * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Remove.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Remove.java new file mode 100644 index 000000000..7c022e8d9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Remove.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * See Remove. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Remove implements UpdatingClause { + + private final ExpressionList setItems; + + Remove(ExpressionList setItems) { + this.setItems = setItems; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + setItems.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java index b7ca97d8b..5ff082331 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java @@ -23,7 +23,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** - * See Return. + * See Return. * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java index ed75ee687..ab436502e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java @@ -25,7 +25,7 @@ import org.springframework.lang.Nullable; /** * The container or "body" for return items, order and optional skip and things. - * See ReturnBody + * See ReturnBody * * @author Michael J. Simons * @soundtrack Ferris MC - Ferris MC's Audiobiographie diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Set.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Set.java new file mode 100644 index 000000000..9ea43d204 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Set.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * See Set. + * + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Set implements UpdatingClause { + + private final ExpressionList setItems; + + Set(ExpressionList setItems) { + this.setItems = setItems; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + setItems.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java index 20a1c02bd..c7412f1dc 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java @@ -29,7 +29,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * See SinglePartQuery. + * See SinglePartQuery. * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java index d540db0d7..e09ee53a2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java @@ -24,7 +24,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; /** * Shall be the common interfaces for queries that we support. *

- * For reference see: Cypher. + * For reference see: Cypher. * We have skipped the intermediate "Query" structure so a statement in the context of this generator is either a * {@link RegularQuery} or a {@code StandaloneCall}. * diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index 6051e95c9..a2a73ab1e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.cypher; +import static org.springframework.data.neo4j.core.cypher.Expressions.*; + import org.apiguardian.api.API; import org.springframework.lang.Nullable; @@ -30,26 +32,48 @@ import org.springframework.lang.Nullable; public interface StatementBuilder { /** - * See {@link Cypher#optionalMatch(PatternElement...)}. - * + * @see Cypher#optionalMatch(PatternElement...) * @return */ - OngoingMatchWithoutWhere optionalMatch(PatternElement... pattern); + OngoingReadingWithoutWhere optionalMatch(PatternElement... pattern); /** - * See {@link Cypher#match(PatternElement...)}. - * + * @see Cypher#match(PatternElement...) * @param pattern * @return */ - OngoingMatchWithoutWhere match(PatternElement... pattern); + OngoingReadingWithoutWhere match(PatternElement... pattern); + + /** + * @see Cypher#create(PatternElement...) + * @param pattern + * @return + */ + OngoingUpdate create(PatternElement... pattern); + + /** + * @see Cypher#merge(PatternElement...) + * @param pattern + * @return + */ + OngoingUpdate merge(PatternElement... pattern); + + OngoingUnwind unwind(Expression expression); + + OngoingReadingAndWith with(AliasedExpression... expressions); + + /** + * An ongoing update statement that can be used to chain more update statements or add a with or return clause. + */ + interface OngoingUpdate extends BuildableStatement, ExposesCreate, ExposesMerge, ExposesReturning, ExposesWith { + } /** * A match that exposes {@code returning} and {@code where} methods to add required information. * While the where clause is optional, an returning clause needs to be specified before the * statement can be build. */ - interface OngoingMatchWithoutWhere extends OngoingMatch, ExposesMatch { + interface OngoingReadingWithoutWhere extends OngoingReading, ExposesMatch, ExposesCreate, ExposesMerge { /** * Adds a where clause to this match. @@ -57,13 +81,13 @@ public interface StatementBuilder { * @param condition The new condition * @return A match restricted by a where clause with no return items yet. */ - OngoingMatchWithWhere where(Condition condition); + OngoingReadingWithWhere where(Condition condition); } /** * A match that has a non-empty {@code where}-part. THe returning clause is still open. */ - interface OngoingMatchWithWhere extends OngoingMatch, ExposesMatch, ExposesConditions { + interface OngoingReadingWithWhere extends OngoingReading, ExposesMatch, ExposesConditions { } interface ExposesConditions { @@ -92,19 +116,24 @@ public interface StatementBuilder { * A match that exposes {@code returning} and for which it is not decided whether the optional * where part has been used or note. */ - interface OngoingMatch extends ExposesReturning, ExposesWith, ExposesDelete { + interface OngoingReading extends ExposesReturning, ExposesWith, ExposesUpdatingClause, ExposesUnwind { + } + + interface OngoingUnwind { + + OngoingReading as(String variable); } /** * A match that knows what to return and which is ready to be build. */ - interface OngoingMatchAndReturn extends ExposesOrderBy, ExposesSkip, ExposesLimit { + interface OngoingReadingAndReturn extends ExposesOrderBy, ExposesSkip, ExposesLimit, BuildableStatement { } /** * A match that knows what to pipe to the next part of a multi part query. */ - interface OngoingMatchAndWithWithoutWhere extends OngoingMatchAndWith { + interface OngoingReadingAndWithWithoutWhere extends OngoingReadingAndWith { /** * Adds a where clause to this match. @@ -112,18 +141,19 @@ public interface StatementBuilder { * @param condition The new condition * @return A match restricted by a where clause with no return items yet. */ - OngoingMatchAndWithWithWhere where(Condition condition); + OngoingReadingAndWithWithWhere where(Condition condition); } - interface OngoingMatchAndWithWithWhere - extends OngoingMatchAndWith, ExposesConditions { + interface OngoingReadingAndWithWithWhere + extends OngoingReadingAndWith, ExposesConditions { } - interface OngoingMatchAndWith - extends OngoingMatch, ExposesMatch, ExposesOrderBy, ExposesSkip, ExposesLimit, ExposesReturning { + interface OngoingReadingAndWith + extends OngoingReading, ExposesMatch, ExposesOrderBy, ExposesSkip, ExposesLimit, ExposesReturning, + ExposesCreate, ExposesMerge { } - interface OngoingMatchAndReturnWithOrder extends ExposesSkip, ExposesLimit { + interface OngoingMatchAndReturnWithOrder extends ExposesSkip, ExposesLimit, BuildableStatement { /** * Adds another expression to the list of order items. @@ -154,9 +184,9 @@ public interface StatementBuilder { } /** - * A match that has all information required to be build and exposes a build method. + * A statement that has all information required to be build and exposes a build method. */ - interface BuildableMatch { + interface BuildableStatement { /** * @return The statement ready to be used, i.e. in a renderer. @@ -166,13 +196,22 @@ public interface StatementBuilder { interface ExposesReturning { + default OngoingReadingAndReturn returning(String... variables) { + return returning(createSymbolicNames(variables)); + } + /** * Create a match that returns one or more expressions. * * @param expressions The expressions to be returned. Must not be null and be at least one expression. * @return A match that can be build now */ - OngoingMatchAndReturn returning(Expression... expressions); + OngoingReadingAndReturn returning(Expression... expressions); + + default OngoingReadingAndReturn returningDistinct(String... variables) { + return returningDistinct(createSymbolicNames(variables)); + } + /** * Create a match that returns the distinct set of one or more expressions. @@ -180,7 +219,7 @@ public interface StatementBuilder { * @param expressions The expressions to be returned. Must not be null and be at least one expression. * @return A match that can be build now */ - OngoingMatchAndReturn returningDistinct(Expression... expressions); + OngoingReadingAndReturn returningDistinct(Expression... expressions); } /** @@ -188,13 +227,21 @@ public interface StatementBuilder { */ interface ExposesWith { + default OngoingReadingAndWithWithoutWhere with(String... variables) { + return with(createSymbolicNames(variables)); + } + /** * Create a match that returns one or more expressions. * * @param expressions The expressions to be returned. Must not be null and be at least one expression. * @return A match that can be build now */ - OngoingMatchAndWithWithoutWhere with(Expression... expressions); + OngoingReadingAndWithWithoutWhere with(Expression... expressions); + + default OngoingReadingAndWithWithoutWhere withDistinct(String... variables) { + return withDistinct(createSymbolicNames(variables)); + } /** * Create a match that returns the distinct set of one or more expressions. @@ -202,13 +249,13 @@ public interface StatementBuilder { * @param expressions The expressions to be returned. Must not be null and be at least one expression. * @return A match that can be build now */ - OngoingMatchAndWithWithoutWhere withDistinct(Expression... expressions); + OngoingReadingAndWithWithoutWhere withDistinct(Expression... expressions); } /** * A step that exposes several methods to specify ordering. */ - interface ExposesOrderBy extends BuildableMatch { + interface ExposesOrderBy { /** * Order the result set by one or more {@link SortItem sort items}. Those can be retrieved for @@ -218,7 +265,7 @@ public interface StatementBuilder { * @param The type of the step being returned * @return A build step that still offers methods for defining skip and limit */ - T orderBy(SortItem... sortItem); + T orderBy(SortItem... sortItem); /** * Order the result set by an expression. @@ -232,7 +279,7 @@ public interface StatementBuilder { /** * A step that exposes the {@link #skip(Number)} method. */ - interface ExposesSkip extends BuildableMatch { + interface ExposesSkip { /** * Adds a skip clause, skipping the given number of records. @@ -240,21 +287,27 @@ public interface StatementBuilder { * @param number How many records to skip. If this is null, then no records are skipped. * @return A step that only allows the limit of records to be specified. */ - ExposesLimit skip(@Nullable Number number); + T skip(@Nullable Number number); } /** * A step that exposes the {@link #limit(Number)} method. */ - interface ExposesLimit extends BuildableMatch { + interface ExposesLimit { /** * Limits the number of returned records. * @param number How many records to return. If this is null, all the records are returned. * @return A buildable match statement. */ - BuildableMatch limit(@Nullable Number number); + BuildableStatement limit(@Nullable Number number); + } + + /** + * A step providing all the supported updating clauses (DELETE, SET) + */ + interface ExposesUpdatingClause extends ExposesDelete, ExposesSetAndRemove { } /** @@ -262,13 +315,21 @@ public interface StatementBuilder { */ interface ExposesDelete { + default OngoingReadingAndWithWithoutWhere delete(String... variables) { + return delete(createSymbolicNames(variables)); + } + /** * Creates a delete step with one or more expressions to be deleted. * * @param expressions The expressions to be deleted. * @return A match with a delete clause that can be build now */ - OngoingMatchAndDelete delete(Expression... expressions); + T delete(Expression... expressions); + + default OngoingReadingAndWithWithoutWhere detachDelete(String... variables) { + return detachDelete(createSymbolicNames(variables)); + } /** * Starts building a delete step that will use {@code DETACH} to remove relationships. @@ -276,7 +337,28 @@ public interface StatementBuilder { * @param expressions The expressions to be deleted. * @return A match with a delete clause that can be build now */ - OngoingMatchAndDelete detachDelete(Expression... expressions); + T detachDelete(Expression... expressions); + } + + /** + * A step that exposes the set clause. + */ + interface ExposesSetAndRemove { + + /** + * Adds a {@code SET} clause to the statement. The list of expressions must be even, each pair will be turned into + * SET operation. + * + * @param expressions The list of expressions to use in a set clause. + * @return An ongoing match and update + */ + T set(Expression... expressions); + + T set(Node node, String... label); + + T remove(Node node, String... label); + + T remove(Property... properties); } /** @@ -290,7 +372,7 @@ public interface StatementBuilder { * @param pattern The patterns to match * @return An ongoing match that is used to specify an optional where and a required return clause */ - OngoingMatchWithoutWhere match(PatternElement... pattern); + OngoingReadingWithoutWhere match(PatternElement... pattern); /** * Adds another optional match clause. @@ -298,12 +380,35 @@ public interface StatementBuilder { * @param pattern The patterns to match * @return An ongoing match that is used to specify an optional where and a required return clause */ - OngoingMatchWithoutWhere optionalMatch(PatternElement... pattern); + OngoingReadingWithoutWhere optionalMatch(PatternElement... pattern); + } + + interface ExposesCreate { + + OngoingUpdate create(PatternElement... pattern); + } + + interface ExposesMerge { + + OngoingUpdate merge(PatternElement... pattern); + } + + interface ExposesUnwind { + + default OngoingUnwind unwind(Expression... expressions) { + return unwind(Cypher.listOf(expressions)); + } + + default OngoingUnwind unwind(String variable) { + return unwind(Cypher.symbolicName(variable)); + } + + OngoingUnwind unwind(Expression expression); } /** * A buildable step that will create a MATCH ... DELETE statement. */ - interface OngoingMatchAndDelete extends BuildableMatch, ExposesReturning, ExposesWith, ExposesDelete { + interface OngoingMatchAndUpdate extends ExposesReturning, ExposesWith, ExposesUpdatingClause { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java index 160406b8b..ffe393fff 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java @@ -18,18 +18,21 @@ */ package org.springframework.data.neo4j.core.cypher; +import lombok.ToString; + import org.apiguardian.api.API; /** * A symbolic name to identify nodes and relationships. *

- * See SchemaName - * SymbolicName + * See SchemaName + * SymbolicName * * @author Michael J. Simons * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") +@ToString(of = { "name" }) public final class SymbolicName implements Expression { private final String name; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Unwind.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Unwind.java new file mode 100644 index 000000000..c2a131378 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Unwind.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.support.Visitor; + +/** + * See Unwind. + * + * @author Michael J. Simons + * @soundtrack Queen - Jazz + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Unwind implements ReadingClause { + + private final Expression expressionToUnwind; + + private final String variable; + + Unwind(Expression expressionToUnwind, String variable) { + + if (expressionToUnwind instanceof Aliased) { + this.expressionToUnwind = new SymbolicName(((Aliased) expressionToUnwind).getAlias()); + } else { + this.expressionToUnwind = expressionToUnwind; + } + this.variable = variable; + } + + public String getVariable() { + return variable; + } + + @Override + public void accept(Visitor visitor) { + visitor.enter(this); + expressionToUnwind.accept(visitor); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java index 67305cbcc..cb8f13579 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java @@ -23,7 +23,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; /** - * Roughly corresponding to Where. + * Roughly corresponding to Where. * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java index 996d8af4f..c575464a6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java @@ -24,7 +24,7 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; /** - * See With. + * See With. * * @author Michael J. Simons * @soundtrack Ferris MC - Ferris MC's Audiobiographie diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java index f59f496f0..f3b665534 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java @@ -1,5 +1,13 @@ /** * Contains an internal DSL for creating Cypher. It is part of our internal API and may change without further notice. + *

+ * While we have extensive tests in place to check that queries SDN needs are correctly generated, the AST itself is not + * validated for type errors. That is, one could misuse the DSL here to create an AST that renders to a syntactically + * correct Cypher statement, that will explode during runtime. For example using wrongly typed expression (an expression + * referencing a list while a map is needed or something like that). + *

+ * With this in mind, please use this DSL consciously if you find it useful. It won't go away anytime soon, but might + * change in ways that break your code without further notice. * * @author Michael J. Simons */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index a68beecc3..08872a48c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -23,7 +23,6 @@ import static java.util.stream.Collectors.*; import java.util.HashSet; import java.util.Locale; import java.util.Optional; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,7 +48,6 @@ import org.springframework.lang.Nullable; */ class RenderingVisitor extends ReflectiveVisitor { - private static final String LABEL_SEPARATOR = ":"; private static final String TYPE_SEPARATOR = ":"; private static final Pattern LABEL_AND_TYPE_QUOTATION = Pattern.compile("`"); @@ -66,18 +64,23 @@ class RenderingVisitor extends ReflectiveVisitor { /** * This keeps track on which level of the tree a separator is needed. */ - private final Set separatorOnLevel = new HashSet<>(); + private final java.util.Set separatorOnLevel = new HashSet<>(); /** * Keeps track of named objects that have been already visited. */ - private final Set visitedNamed = new HashSet<>(); + private final java.util.Set visitedNamed = new HashSet<>(); /** * The current level in the tree of cypher elements. */ private int currentLevel = 0; + /** + * Will be set to true when entering an already visited node. + */ + private boolean skipNodeContent = false; + private void enableSeparator(int level, boolean on) { if (on) { separatorOnLevel.add(level); @@ -92,7 +95,11 @@ class RenderingVisitor extends ReflectiveVisitor { } @Override - protected void preEnter(Visitable visitable) { + protected boolean preEnter(Visitable visitable) { + + if (skipNodeContent) { + return false; + } int nextLevel = ++currentLevel + 1; if (visitable instanceof TypedSubtree) { @@ -103,6 +110,8 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append(separator); separator = null; } + + return !skipNodeContent; } @Override @@ -134,6 +143,22 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append(" WHERE "); } + void enter(Create create) { + builder.append("CREATE "); + } + + void leave(Create create) { + builder.append(" "); + } + + void enter(Merge merge) { + builder.append("MERGE "); + } + + void leave(Merge merge) { + builder.append(" "); + } + void enter(Return returning) { builder.append("RETURN "); if (returning.isDistinct()) { @@ -213,23 +238,32 @@ class RenderingVisitor extends ReflectiveVisitor { } void enter(Operation operation) { - builder.append("("); + + if (operation.needsGrouping()) { + builder.append("("); + } } void enter(Operator operator) { Operator.Type type = operator.getType(); - if (type == Operator.Type.BINARY || type == Operator.Type.POSTFIX) { + if (type == Operator.Type.LABEL) { + return; + } + if (type != Operator.Type.PREFIX) { builder.append(" "); } builder.append(operator.getRepresentation()); - if (type == Operator.Type.BINARY || type == Operator.Type.PREFIX) { + if (type != Operator.Type.POSTFIX) { builder.append(" "); } } void leave(Operation operation) { - builder.append(")"); + + if (operation.needsGrouping()) { + builder.append(")"); + } } void enter(CompoundCondition compoundCondition) { @@ -245,23 +279,36 @@ class RenderingVisitor extends ReflectiveVisitor { } void enter(Node node) { + builder.append("("); + + // This is only relevant for nodes in relationships. + // Otherwise all the labels would be rendered again. + node.getSymbolicName().map(SymbolicName::getName).ifPresent(symbolicName -> { + skipNodeContent = visitedNamed.contains(node); + visitedNamed.add(node); + + if (skipNodeContent) { + builder.append(symbolicName); + } + }); } void leave(Node node) { - // This is only relevant for nodes in relationships. - if (!(node.getSymbolicName().isPresent() && visitedNamed.contains(node))) { - builder - .append(node.isLabeled() ? LABEL_SEPARATOR : "") - .append(node.getLabels().stream().map(RenderingVisitor::escapeName) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(joining(LABEL_SEPARATOR))); - visitedNamed.add(node); - } - builder.append(")"); + + skipNodeContent = false; + } + + void enter(NodeLabel nodeLabel) { + + escapeName(nodeLabel.getValue()).ifPresent(label -> builder.append(":").append(label)); + } + + void enter(Properties properties) { + + builder.append(" "); } void enter(SymbolicName symbolicName) { @@ -309,6 +356,47 @@ class RenderingVisitor extends ReflectiveVisitor { builder.append("}"); } + void enter(ListExpression list) { + + builder.append("["); + } + + void leave(ListExpression list) { + + builder.append("]"); + } + + void enter(Unwind unwind) { + + builder.append("UNWIND "); + } + + void leave(Unwind unwind) { + + builder.append(" AS ") + .append(unwind.getVariable()) + .append(" "); + } + + + void enter(Set set) { + + builder.append("SET "); + } + + void leave(Set set) { + builder.append(" "); + } + + void enter(Remove remove) { + + builder.append("REMOVE "); + } + + void leave(Remove remove) { + builder.append(" "); + } + public String getRenderedContent() { return this.builder.toString(); } @@ -329,6 +417,4 @@ class RenderingVisitor extends ReflectiveVisitor { Matcher matcher = LABEL_AND_TYPE_QUOTATION.matcher(unescapedName); return Optional.of(String.format(Locale.ENGLISH, "`%s`", matcher.replaceAll("``"))); } - } - diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java index 4e9d825f6..da0a12d72 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java @@ -24,6 +24,8 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -61,14 +63,18 @@ public abstract class ReflectiveVisitor implements Visitor { private final Map> cachedHandles = new ConcurrentHashMap<>(); + /** Keeps track of the ASTs current level. */ + private Deque currentVisitedElements = new LinkedList<>(); + /** * This is a hook that is called with the uncasted, raw visitable just before entering a visitable. *

* The hook is called regardless wither a matching {@code enter} is found or not. * * @param visitable The visitable that is passed on to a matching enter after this call. + * @return true, when visiting of elements should be stopped until this element is left again. */ - protected abstract void preEnter(Visitable visitable); + protected abstract boolean preEnter(Visitable visitable); /** * This is a hook that is called with the uncasted, raw visitable just after leaving the visitable. @@ -81,14 +87,21 @@ public abstract class ReflectiveVisitor implements Visitor { @Override public final void enter(Visitable visitable) { - preEnter(visitable); - executeConcreteMethodIn(new TargetAndPhase(visitable.getClass(), Phase.ENTER), visitable); + + if (preEnter(visitable)) { + currentVisitedElements.push(visitable); + executeConcreteMethodIn(new TargetAndPhase(visitable.getClass(), Phase.ENTER), visitable); + } } @Override public final void leave(Visitable visitable) { - executeConcreteMethodIn(new TargetAndPhase(visitable.getClass(), Phase.LEAVE), visitable); - postLeave(visitable); + + if (currentVisitedElements.peek() == visitable) { + executeConcreteMethodIn(new TargetAndPhase(visitable.getClass(), Phase.LEAVE), visitable); + postLeave(visitable); + currentVisitedElements.pop(); + } } private void executeConcreteMethodIn(TargetAndPhase targetAndPhase, Visitable onVisitable) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java index bb9b4cea0..eb43976dc 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java @@ -26,12 +26,14 @@ import java.util.List; * This class helps to group items of the same type on the same level of the tree into a list structure that can be * recognized by visitors. * + * @param The children's type + * @param The concrete type of the implementing class. * @author Michael J. Simons * @since 1.0 */ -public abstract class TypedSubtree implements Visitable { +public abstract class TypedSubtree> implements Visitable { - private final List children; + protected final List children; protected TypedSubtree(T... children) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java index 027359349..6c25eabdf 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java @@ -37,9 +37,8 @@ import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Node; import org.springframework.data.neo4j.core.cypher.SortItem; import org.springframework.data.neo4j.core.cypher.StatementBuilder; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableMatch; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndDelete; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndWith; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableStatement; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndWith; import org.springframework.data.neo4j.core.cypher.SymbolicName; import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.Id; @@ -122,7 +121,7 @@ public final class CypherAdapterUtils { * @return * @see #prepareMatchOf(NodeDescription, Optional) */ - public OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription) { + public OngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription) { return prepareMatchOf(nodeDescription, Optional.empty()); } @@ -139,7 +138,7 @@ public final class CypherAdapterUtils { * @param condition Optional conditions to add * @return An ongoing match */ - public OngoingMatchAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition) { + public OngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition) { Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); IdDescription idDescription = nodeDescription.getIdDescription(); @@ -152,7 +151,7 @@ public final class CypherAdapterUtils { .with(expressions.toArray(new Expression[expressions.size()])); } - public OngoingMatchAndDelete prepareDeleteOf(NodeDescription nodeDescription, + public BuildableStatement prepareDeleteOf(NodeDescription nodeDescription, Optional condition) { Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); @@ -160,10 +159,10 @@ public final class CypherAdapterUtils { } } - public static BuildableMatch addPagingParameter( + public static BuildableStatement addPagingParameter( NodeDescription nodeDescription, Pageable pageable, - StatementBuilder.OngoingMatchAndReturn returning) { + StatementBuilder.OngoingReadingAndReturn returning) { Sort sort = pageable.getSort(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java index fbe2afe4f..6c516f54f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java @@ -150,8 +150,8 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; private final Map parameters = new HashMap<>(); - StatementBuilder.OngoingMatchAndWith f( - BiFunction, Optional, StatementBuilder.OngoingMatchAndWith> ff) { + StatementBuilder.OngoingReadingAndWith f( + BiFunction, Optional, StatementBuilder.OngoingReadingAndWith> ff) { return ff.apply(this.nodeDescription, Optional.of(this.condition)); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 0aa1cbe63..786fba834 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -43,7 +43,7 @@ import org.springframework.data.neo4j.core.cypher.Condition; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchAndReturn; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndReturn; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; import org.springframework.data.neo4j.core.schema.NodeDescription; @@ -96,10 +96,11 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Override public Page findAll(Pageable pageable) { - OngoingMatchAndReturn returning = statementBuilder.prepareMatchOf(nodeDescription) + OngoingReadingAndReturn returning = statementBuilder.prepareMatchOf(nodeDescription) .returning(asterisk()); - StatementBuilder.BuildableMatch returningWithPaging = addPagingParameter(nodeDescription, pageable, returning); + StatementBuilder.BuildableStatement returningWithPaging = addPagingParameter(nodeDescription, pageable, + returning); Statement statement = returningWithPaging.build(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java index 6d8cf2dfc..2bcffc43a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java @@ -43,7 +43,7 @@ import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableMatch; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableStatement; import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; @@ -126,11 +126,11 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { public Page findAll(Example example, Pageable pageable) { Predicate predicate = Predicate.create(mappingContext, example); - StatementBuilder.OngoingMatchAndReturn returning = predicate + StatementBuilder.OngoingReadingAndReturn returning = predicate .f(statementBuilder::prepareMatchOf) .returning(asterisk()); - BuildableMatch returningWithPaging = addPagingParameter(predicate.getNodeDescription(), pageable, returning); + BuildableStatement returningWithPaging = addPagingParameter(predicate.getNodeDescription(), pageable, returning); Statement statement = returningWithPaging.build(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java new file mode 100644 index 000000000..be0471064 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java @@ -0,0 +1,1617 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.neo4j.core.cypher.Conditions.*; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.springframework.data.neo4j.core.cypher.Functions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; +import org.springframework.data.neo4j.core.cypher.renderer.Renderer; + +/** + * @author Michael J. Simons + * @author Gerrit Meier + */ +class CypherIT { + + private final Renderer cypherRenderer = CypherRenderer.create(); + private final Node bikeNode = Cypher.node("Bike").named("b"); + private final Node userNode = Cypher.node("User").named("u"); + + @Nested + class SingleQuerySinglePart { + + @Nested + class ReadingAndReturn { + + @Test + void unrelatedNodes() { + Statement statement = Cypher.match(bikeNode, userNode, Cypher.node("U").named("o")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`), (u:`User`), (o:`U`) RETURN b, u"); + } + + @Test + void asterikShouldWork() { + Statement statement = Cypher.match(bikeNode, userNode, Cypher.node("U").named("o")) + .returning(asterisk()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`), (u:`User`), (o:`U`) RETURN *"); + } + + @Test + void aliasedExpressionsInReturn() { + Node unnamedNode = Cypher.node("ANode"); + Node namedNode = Cypher.node("AnotherNode").named("o"); + Statement statement = Cypher.match(unnamedNode, namedNode) + .returning(unnamedNode.as("theNode"), namedNode.as("theOtherNode")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (:`ANode`), (o:`AnotherNode`) RETURN (:`ANode`) AS theNode, o AS theOtherNode"); + } + + @Test + void simpleRelationship() { + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) RETURN b, u"); + } + + @Test + void simpleRelationshipWithReturn() { + Relationship owns = userNode + .relationshipTo(bikeNode, "OWNS").named("o"); + + Statement statement = Cypher + .match(owns) + .returning(bikeNode, userNode, owns) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[o:`OWNS`]->(b:`Bike`) RETURN b, u, o"); + } + + @Test + void chainedRelations() { + Node tripNode = Cypher.node("Trip").named("t"); + Statement statement = Cypher + .match(userNode + .relationshipTo(bikeNode, "OWNS").named("r1") + .relationshipTo(tripNode, "USED_ON").named("r2") + ) + .where(userNode.property("name").matches(".*aName")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`)-[r1:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); + + statement = Cypher + .match(userNode + .relationshipTo(bikeNode, "OWNS") + .relationshipTo(tripNode, "USED_ON").named("r2") + ) + .where(userNode.property("name").matches(".*aName")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); + + statement = Cypher + .match(userNode + .relationshipTo(bikeNode, "OWNS") + .relationshipTo(tripNode, "USED_ON").named("r2") + .relationshipFrom(userNode, "WAS_ON").named("x") + .relationshipBetween(Cypher.node("SOMETHING")).named("y") + ) + .where(userNode.property("name").matches(".*aName")) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`)<-[x:`WAS_ON`]-(u)-[y]-(:`SOMETHING`) WHERE u.name =~ '.*aName' RETURN b, u"); + } + + @Test + void sortOrderDefault() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(sort(userNode.property("name"))).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name"); + } + + @Test + void sortOrderAscending() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(Cypher.sort(userNode.property("name")).ascending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); + } + + @Test + void sortOrderDescending() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(Cypher.sort(userNode.property("name")).descending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name DESC"); + } + + @Test + void sortOrderConcatenation() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy( + sort(userNode.property("name")).descending(), + sort(userNode.property("age")).ascending() + ) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name DESC, u.age ASC"); + } + + @Test + void sortOrderDefaultExpression() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(userNode.property("name").ascending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); + } + + @Test + void sortOrderAscendingExpression() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(userNode.property("name").ascending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); + } + + @Test + void sortOrderDescendingExpression() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(userNode.property("name").descending()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name DESC"); + } + + @Test + void sortOrderConcatenationExpression() { + Statement statement = Cypher.match(userNode).returning(userNode) + .orderBy(userNode.property("name")).descending() + .and(userNode.property("age")).ascending() + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u ORDER BY u.name DESC, u.age ASC"); + } + + @Test + void skip() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(1).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u SKIP 1"); + } + + @Test + void nullSkip() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + + @Test + void limit() { + Statement statement = Cypher.match(userNode).returning(userNode).limit(1).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u LIMIT 1"); + } + + @Test + void nullLimit() { + Statement statement = Cypher.match(userNode).returning(userNode).limit(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + + @Test + void skipAndLimit() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(1).limit(1).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u SKIP 1 LIMIT 1"); + } + + @Test + void nullskipAndLimit() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(null).limit(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + + @Test + void distinct() { + Statement statement = Cypher.match(userNode).returningDistinct(userNode).skip(1).limit(1).build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN DISTINCT u SKIP 1 LIMIT 1"); + } + } + } + + @Nested + class SingleQueryMultiPart { + @Test + void simpleWith() { + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .where(userNode.property("a").isNull()) + .with(bikeNode, userNode) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u RETURN b"); + } + + @Test + void shouldRenderLeadingWith() { + Statement statement = Cypher + .with(Cypher.parameter("listOfPropertyMaps").as("p")) + .unwind("p").as("item") + .returning("item") + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("WITH $listOfPropertyMaps AS p UNWIND p AS item RETURN item"); + } + + @Test + void simpleWithChained() { + + Node tripNode = Cypher.node("Trip").named("t"); + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .where(userNode.property("a").isNull()) + .with(bikeNode, userNode) + .match(tripNode) + .where(tripNode.property("name").isEqualTo(literalOf("Festive500"))) + .with(tripNode) + .returning(bikeNode, userNode, tripNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u MATCH (t:`Trip`) WHERE t.name = 'Festive500' WITH t RETURN b, u, t"); + } + + @Test + void deletingSimpleWith() { + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .where(userNode.property("a").isNull()) + .delete(userNode) + .with(bikeNode, userNode) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL DELETE u WITH b, u RETURN b, u"); + } + + @Test + void deletingSimpleWithReverse() { + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .where(userNode.property("a").isNull()) + .with(bikeNode, userNode) + .delete(userNode) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u DELETE u RETURN b, u"); + } + + @Test + void mixedClausesWithWith() { + + Node tripNode = Cypher.node("Trip").named("t"); + Statement statement = Cypher + .match(userNode.relationshipTo(bikeNode, "OWNS")) + .match(tripNode) + .delete(tripNode) + .with(bikeNode, tripNode) + .match(userNode) + .with(bikeNode, userNode) + .returning(bikeNode, userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) MATCH (t:`Trip`) DELETE t WITH b, t MATCH (u) WITH b, u RETURN b, u"); + } + } + + @Nested + class MultipleMatches { + @Test + void simple() { + Statement statement = Cypher + .match(bikeNode) + .match(userNode, Cypher.node("U").named("o")) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) RETURN b"); + } + + @Test + void simpleWhere() { + Statement statement = Cypher + .match(bikeNode) + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull()) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); + } + + @Test + void multiWhere() { + Statement statement = Cypher + .match(bikeNode) + .where(bikeNode.property("a").isNotNull()) + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull()) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (b:`Bike`) WHERE b.a IS NOT NULL MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); + } + + @Test + void multiWhereMultiConditions() { + Statement statement = Cypher + .match(bikeNode) + .where(bikeNode.property("a").isNotNull()) + .and(bikeNode.property("b").isNull()) + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull().or(userNode.internalId().isEqualTo(literalOf(4711)))) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (b:`Bike`) WHERE (b.a IS NOT NULL AND b.b IS NULL) MATCH (u:`User`), (o:`U`) WHERE (u.a IS NULL OR id(u) = 4711) RETURN b"); + } + + @Test + void optional() { + Statement statement = Cypher + .optionalMatch(bikeNode) + .match(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull()) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("OPTIONAL MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); + } + + @Test + void optionalNext() { + Statement statement = Cypher + .match(bikeNode) + .optionalMatch(userNode, Cypher.node("U").named("o")) + .where(userNode.property("a").isNull()) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) OPTIONAL MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); + } + } + + @Nested + class FunctionRendering { + @Test + void inWhereClause() { + Statement statement = Cypher.match(userNode).where(userNode.internalId().isEqualTo(literalOf(1L))) + .returning(userNode).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE id(u) = 1 RETURN u"); + } + + @Test + void inReturnClause() { + Statement statement = Cypher.match(userNode).returning(Functions.count(userNode)).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN count(u)"); + } + + @Test + void aliasedInReturnClause() { + Statement statement = Cypher.match(userNode).returning(Functions.count(userNode).as("cnt")).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN count(u) AS cnt"); + } + + @Test + void shouldSupportMoreThanOneArgument() { + Statement statement = Cypher.match(userNode) + .returning(coalesce(userNode.property("a"), userNode.property("b"), literalOf("¯\\_(ツ)_/¯"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN coalesce(u.a, u.b, '¯\\\\_(ツ)_/¯')"); + } + + @Test + void literalsShouldDealWithNull() { + Statement statement = Cypher.match(userNode) + .returning(Functions.coalesce(literalOf(null), userNode.property("field")).as("p")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN coalesce(NULL, u.field) AS p"); + } + } + + @Nested + class ComparisonRendering { + + @Test + void equalsWithStringLiteral() { + Statement statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(literalOf("Test"))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.name = 'Test' RETURN u"); + } + + @Test + void equalsWithNumberLiteral() { + Statement statement = Cypher.match(userNode) + .where(userNode.property("age").isEqualTo(literalOf(21))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.age = 21 RETURN u"); + } + } + + @Nested + class Conditions { + @Test + void conditionsChainingAnd() { + Statement statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(literalOf("Test")) + .and(userNode.property("age").isEqualTo(literalOf(21)))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' AND u.age = 21) RETURN u"); + } + + @Test + void conditionsChainingOr() { + Statement statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(literalOf("Test")) + .or(userNode.property("age").isEqualTo(literalOf(21)))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' OR u.age = 21) RETURN u"); + } + + @Test + void conditionsChainingXor() { + Statement statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(literalOf("Test")) + .xor(userNode.property("age").isEqualTo(literalOf(21)))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' XOR u.age = 21) RETURN u"); + } + + @Test + void chainingOnWhere() { + Statement statement; + + Literal test = literalOf("Test"); + Literal foobar = literalOf("foobar"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE u.name = 'Test' RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'Test' AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'Test' OR u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'Test') OR u.name = 'foobar') AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(test)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((u.name = 'Test' OR u.name = 'foobar') AND u.name = 'Test' AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(test)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(test)) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((((u.name = 'Test' OR u.name = 'foobar') AND u.name = 'Test') OR u.name = 'foobar') AND u.name = 'Test') RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isNotNull()) + .and(userNode.property("name").isEqualTo(test)) + .or(userNode.property("age").isEqualTo(literalOf(21))) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((u.name IS NOT NULL AND u.name = 'Test') OR u.age = 21) RETURN u"); + } + + @Test + void chainingOnConditions() { + Statement statement; + + Literal test = literalOf("Test"); + Literal foobar = literalOf("foobar"); + Literal bazbar = literalOf("bazbar"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .and(userNode.property("name").isEqualTo(bazbar)) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test)) + .and( + userNode.property("name").isEqualTo(bazbar) + .and(userNode.property("name").isEqualTo(foobar)) + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'bazbar' AND u.name = 'foobar') RETURN u"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .and(userNode.property("name").isEqualTo(bazbar)) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE ((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .and(userNode.property("name").isEqualTo(bazbar)) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + .and(userNode.property("name").isEqualTo(bazbar)) + ) + .returning(userNode) + .build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') AND u.name = 'bazbar') RETURN u"); + } + + @Test + void chainingCombined() { + Statement statement; + + Literal test = literalOf("Test"); + Literal foobar = literalOf("foobar"); + Literal bazbar = literalOf("bazbar"); + + statement = Cypher.match(userNode) + .where( + userNode.property("name").isEqualTo(test) + .and(userNode.property("name").isEqualTo(bazbar)) + .or(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(foobar)) + ) + .and( + userNode.property("name").isEqualTo(bazbar) + .and(userNode.property("name").isEqualTo(foobar)) + .or(userNode.property("name").isEqualTo(test)) + .not() + ) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') AND NOT (((u.name = 'bazbar' AND u.name = 'foobar') OR u.name = 'Test'))) RETURN u"); + + } + + @Test + void negatedConditions() { + Statement statement = Cypher.match(userNode) + .where(userNode.property("name").isNotNull().not()) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE NOT (u.name IS NOT NULL) RETURN u"); + } + + @Test + void noConditionShouldNotBeRendered() { + Statement statement; + statement = Cypher.match(userNode) + .where(noCondition()) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("name").isEqualTo(literalOf("test"))) + .and(noCondition()).or(noCondition()) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.name = 'test' RETURN u"); + } + } + + @Nested + class RemoveClause { + @Test + void shouldRenderRemoveOnNodes() { + Statement statement; + + statement = Cypher.match(userNode) + .remove(userNode, "A", "B") + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) REMOVE u:`A`:`B` RETURN u"); + + statement = Cypher.match(userNode) + .with(userNode) + .set(userNode, "A", "B") + .remove(userNode, "C", "D") + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WITH u SET u:`A`:`B` REMOVE u:`C`:`D` RETURN u"); + } + + @Test + void shouldRenderRemoveOfProperties() { + Statement statement; + + statement = Cypher.match(userNode) + .remove(userNode.property("a"), userNode.property("b")) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) REMOVE u.a, u.b RETURN u"); + + statement = Cypher.match(userNode) + .with(userNode) + .remove(userNode.property("a")) + .remove(userNode.property("b")) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WITH u REMOVE u.a REMOVE u.b RETURN u"); + } + } + + @Nested + class SetClause { + + @Test + void shouldRenderSet() { + + Statement statement; + + statement = Cypher.match(userNode) + .set(userNode.property("p").to(literalOf("Hallo, Welt"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) SET u.p = 'Hallo, Welt'"); + + statement = Cypher.match(userNode) + .set(userNode.property("p").to(literalOf("Hallo, Welt"))) + .set(userNode.property("a").to(literalOf("Selber hallo."))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) SET u.p = 'Hallo, Welt' SET u.a = 'Selber hallo.'"); + + statement = Cypher.match(userNode) + .set( + userNode.property("p").to(literalOf("Hallo")), + userNode.property("g").to(literalOf("Welt")) + ) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) SET u.p = 'Hallo', u.g = 'Welt'"); + + } + + @Test + void shouldRenderSetOnNodes() { + Statement statement; + + statement = Cypher.match(userNode) + .set(userNode, "A", "B") + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) SET u:`A`:`B` RETURN u"); + + statement = Cypher.match(userNode) + .with(userNode) + .set(userNode, "A", "B") + .set(userNode, "C", "D") + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WITH u SET u:`A`:`B` SET u:`C`:`D` RETURN u"); + } + + @Test + void shouldRenderSetFromAListOfExpression() { + Statement statement; + + statement = Cypher.match(userNode) + .set(userNode.property("p"), literalOf("Hallo, Welt")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) SET u.p = 'Hallo, Welt'"); + + statement = Cypher.match(userNode) + .set(userNode.property("p"), literalOf("Hallo"), + userNode.property("g"), literalOf("Welt")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) SET u.p = 'Hallo', u.g = 'Welt'"); + + statement = Cypher.match(userNode) + .set(userNode.property("p"), literalOf("Hallo, Welt")) + .set(userNode.property("p"), literalOf("Hallo"), + userNode.property("g"), literalOf("Welt")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) SET u.p = 'Hallo, Welt' SET u.p = 'Hallo', u.g = 'Welt'"); + + assertThatIllegalArgumentException().isThrownBy(() -> { + Cypher.match(userNode).set(userNode.property("g")); + }).withMessage("The list of expression to set must be even."); + } + + @Test + void shouldRenderMixedSet() { + Statement statement; + + statement = Cypher.match(userNode) + .set(userNode.property("p1"), literalOf("Two expressions")) + .set(userNode.property("p2").to(literalOf("A set expression"))) + .set( + userNode.property("p3").to(literalOf("One of two set expression")), + userNode.property("p4").to(literalOf("Two of two set expression")) + ) + .set( + userNode.property("p5"), literalOf("Pair one of 2 expressions"), + userNode.property("p6"), literalOf("Pair two of 4 expressions") + ) + .returning(asterisk()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) SET u.p1 = 'Two expressions' SET u.p2 = 'A set expression' SET u.p3 = 'One of two set expression', u.p4 = 'Two of two set expression' SET u.p5 = 'Pair one of 2 expressions', u.p6 = 'Pair two of 4 expressions' RETURN *"); + } + } + + @Nested + class MergeClause { + + @Test + void shouldRenderMergeWithoutReturn() { + Statement statement; + statement = Cypher.merge(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`)"); + + statement = Cypher.merge(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`)-[o:`OWNS`]->(b:`Bike`)"); + } + + @Test + void shouldRenderMultipleMergesWithoutReturn() { + Statement statement; + statement = Cypher.merge(userNode) + .merge(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`) MERGE (b:`Bike`)"); + + statement = Cypher + .merge(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .merge(Cypher.node("Other")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`)-[o:`OWNS`]->(b:`Bike`) MERGE (:`Other`)"); + } + + @Test + void shouldRenderMergeReturn() { + Statement statement; + statement = Cypher.merge(userNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`) RETURN u"); + + Relationship r = userNode.relationshipTo(bikeNode, "OWNS").named("o"); + statement = Cypher.merge(r) + .returning(userNode, r) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`)-[o:`OWNS`]->(b:`Bike`) RETURN u, o"); + + statement = Cypher.merge(userNode) + .returning(userNode) + .orderBy(userNode.property("name")) + .skip(23) + .limit(42) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`) RETURN u ORDER BY u.name SKIP 23 LIMIT 42"); + } + + @Test + void shouldRenderMultipleMergesReturn() { + Statement statement; + statement = Cypher.merge(userNode) + .merge(bikeNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`) MERGE (b:`Bike`) RETURN u"); + + Relationship r = userNode.relationshipTo(bikeNode, "OWNS").named("o"); + statement = Cypher + .merge(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .merge(Cypher.node("Other")) + .returning(userNode, r) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`)-[o:`OWNS`]->(b:`Bike`) MERGE (:`Other`) RETURN u, o"); + } + + @Test + void shouldRenderMergeWithWith() { + Statement statement; + statement = Cypher.merge(userNode) + .with(userNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`) WITH u RETURN u"); + + Relationship r = userNode.relationshipTo(bikeNode, "OWNS").named("o"); + statement = Cypher.merge(userNode) + .with(userNode) + .set(userNode.property("x").to(literalOf("y"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`) WITH u SET u.x = 'y'"); + } + + @Test + void matchShouldExposeMerge() { + Statement statement; + statement = Cypher.match(userNode) + .merge(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) MERGE (u)-[o:`OWNS`]->(b:`Bike`)"); + } + + @Test + void withShouldExposeMerge() { + Statement statement; + statement = Cypher.match(userNode) + .withDistinct(userNode) + .merge(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WITH DISTINCT u MERGE (u)-[o:`OWNS`]->(b:`Bike`)"); + } + + @Test + void mixedCreateAndMerge() { + Statement statement; + + Node tripNode = Cypher.node("Trip").named("t"); + + statement = Cypher.create(userNode) + .merge(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .withDistinct(bikeNode) + .merge(tripNode.relationshipFrom(bikeNode, "USED_ON")) + .returning(Cypher.asterisk()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) MERGE (u)-[o:`OWNS`]->(b:`Bike`) WITH DISTINCT b MERGE (t:`Trip`)<-[:`USED_ON`]-(b) RETURN *"); + } + } + + @Nested + class CreateClause { + + @Test + void shouldRenderCreateWithoutReturn() { + Statement statement; + statement = Cypher.create(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`)"); + + statement = Cypher.create(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`)-[o:`OWNS`]->(b:`Bike`)"); + } + + @Test + void shouldRenderMultipleCreatesWithoutReturn() { + Statement statement; + statement = Cypher.create(userNode) + .create(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) CREATE (b:`Bike`)"); + + statement = Cypher + .create(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .create(Cypher.node("Other")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`)-[o:`OWNS`]->(b:`Bike`) CREATE (:`Other`)"); + } + + @Test + void shouldRenderCreateReturn() { + Statement statement; + statement = Cypher.create(userNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) RETURN u"); + + Relationship r = userNode.relationshipTo(bikeNode, "OWNS").named("o"); + statement = Cypher.create(r) + .returning(userNode, r) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`)-[o:`OWNS`]->(b:`Bike`) RETURN u, o"); + + statement = Cypher.create(userNode) + .returning(userNode) + .orderBy(userNode.property("name")) + .skip(23) + .limit(42) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) RETURN u ORDER BY u.name SKIP 23 LIMIT 42"); + } + + @Test + void shouldRenderMultipleCreatesReturn() { + Statement statement; + statement = Cypher.create(userNode) + .create(bikeNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) CREATE (b:`Bike`) RETURN u"); + + Relationship r = userNode.relationshipTo(bikeNode, "OWNS").named("o"); + statement = Cypher + .create(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .create(Cypher.node("Other")) + .returning(userNode, r) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`)-[o:`OWNS`]->(b:`Bike`) CREATE (:`Other`) RETURN u, o"); + } + + @Test + void shouldRenderCreateWithWith() { + Statement statement; + statement = Cypher.create(userNode) + .with(userNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) WITH u RETURN u"); + + Relationship r = userNode.relationshipTo(bikeNode, "OWNS").named("o"); + statement = Cypher.create(userNode) + .with(userNode) + .set(userNode.property("x").to(literalOf("y"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) WITH u SET u.x = 'y'"); + } + + @Test + void matchShouldExposeCreate() { + Statement statement; + statement = Cypher.match(userNode) + .create(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) CREATE (u)-[o:`OWNS`]->(b:`Bike`)"); + } + + @Test + void withShouldExposeCreate() { + Statement statement; + statement = Cypher.match(userNode) + .withDistinct(userNode) + .create(userNode.relationshipTo(bikeNode, "OWNS").named("o")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WITH DISTINCT u CREATE (u)-[o:`OWNS`]->(b:`Bike`)"); + } + } + + @Nested + class DeleteClause { + + @Test + void shouldRenderDeleteWithoutReturn() { + + Statement statement; + statement = Cypher.match(userNode) + .detachDelete(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) DETACH DELETE u"); + + statement = Cypher.match(userNode) + .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) + .delete(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DELETE u"); + + statement = Cypher.match(userNode, bikeNode) + .delete(userNode, bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`), (b:`Bike`) DELETE u, b"); + } + + @Test + void shouldRenderDeleteWithReturn() { + + Statement statement; + statement = Cypher.match(userNode) + .detachDelete(userNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) DETACH DELETE u RETURN u"); + + statement = Cypher.match(userNode) + .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) + .detachDelete(userNode) + .returning(userNode).orderBy(userNode.property("a").ascending()).skip(2).limit(1) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DETACH DELETE u RETURN u ORDER BY u.a ASC SKIP 2 LIMIT 1"); + + statement = Cypher.match(userNode) + .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) + .detachDelete(userNode) + .returningDistinct(userNode).orderBy(userNode.property("a").ascending()).skip(2).limit(1) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DETACH DELETE u RETURN DISTINCT u ORDER BY u.a ASC SKIP 2 LIMIT 1"); + } + + @Test + void shouldRenderNodeDelete() { + Node n = anyNode("n"); + Relationship r = n.relationshipBetween(anyNode()).named("r0"); + Statement statement = Cypher + .match(n).where(n.internalId().isEqualTo(literalOf(4711))) + .optionalMatch(r) + .delete(r, n) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) WHERE id(n) = 4711 OPTIONAL MATCH (n)-[r0]-() DELETE r0, n"); + } + + @Test + void shouldRenderChainedDeletes() { + Node n = anyNode("n"); + Relationship r = n.relationshipBetween(anyNode()).named("r0"); + Statement statement = Cypher + .match(n).where(n.internalId().isEqualTo(literalOf(4711))) + .optionalMatch(r) + .delete(r, n) + .delete(bikeNode) + .detachDelete(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) WHERE id(n) = 4711 OPTIONAL MATCH (n)-[r0]-() DELETE r0, n DELETE b DETACH DELETE u"); + } + } + + @Nested + class Expressions { + @Test + void shouldRenderParameters() { + Statement statement; + statement = Cypher.match(userNode) + .where(userNode.property("a").isEqualTo(parameter("aParameter"))) + .detachDelete(userNode) + .returning(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WHERE u.a = $aParameter DETACH DELETE u RETURN u"); + } + } + + @Nested + class OperationsAndComparisions { + + @Test + void shouldRenderOperations() { + Statement statement; + statement = Cypher.match(Cypher.anyNode("n")) + .returning(literalOf(1).plus(literalOf(2))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN (1 + 2)"); + } + + @Test + void shouldRenderComparision() { + Statement statement; + statement = Cypher.match(Cypher.anyNode("n")) + .returning(literalOf(1).gt(literalOf(2))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN 1 > 2"); + + statement = Cypher.match(Cypher.anyNode("n")) + .returning(literalOf(1).gt(literalOf(2)).isTrue()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN (1 > 2) = true"); + + statement = Cypher.match(Cypher.anyNode("n")) + .returning(literalOf(1).gt(literalOf(2)).isTrue().isFalse()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN ((1 > 2) = true) = false"); + } + } + + @Nested + class ExpressionsRendering { + @Test + void shouldRenderMap() { + Statement statement; + statement = Cypher.match(Cypher.anyNode("n")) + .returning( + Functions.point( + mapOf( + "latitude", Cypher.parameter("latitude"), + "longitude", Cypher.parameter("longitude"), + "crs", literalOf(4326) + ) + ) + ) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (n) RETURN point({latitude: $latitude, longitude: $longitude, crs: 4326})"); + } + } + + @Nested + class PropertyRendering { + @Test + void shouldRenderNodeProperties() { + + for (Node nodeWithProperties : new Node[] { + Cypher.node("Test", mapOf("a", literalOf("b"))), + Cypher.node("Test").properties(mapOf("a", literalOf("b"))), + Cypher.node("Test").properties("a", literalOf("b")) + }) { + + Statement statement; + statement = Cypher.match(nodeWithProperties) + .returning(Cypher.asterisk()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (:`Test` {a: 'b'}) RETURN *"); + + statement = Cypher.merge(nodeWithProperties) + .returning(Cypher.asterisk()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (:`Test` {a: 'b'}) RETURN *"); + } + } + + @Test + void nestedProperties() { + + Node nodeWithProperties = Cypher.node("Test").properties("outer", mapOf("a", literalOf("b"))); + + Statement statement; + statement = Cypher.match(nodeWithProperties) + .returning(Cypher.asterisk()) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (:`Test` {outer: {a: 'b'}}) RETURN *"); + } + + @Test + void shouldNotRenderPropertiesInReturn() { + + Node nodeWithProperties = bikeNode.properties("a", literalOf("b")); + + Statement statement; + statement = Cypher.match(nodeWithProperties, nodeWithProperties.relationshipFrom(userNode, "OWNS")) + .returning(nodeWithProperties) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (b:`Bike` {a: 'b'}), (b)<-[:`OWNS`]-(u:`User`) RETURN b"); + } + } + + @Nested + class UnwindRendering { + + @Test + void shouldRenderLeadingUnwind() { + + Statement statement; + statement = Cypher.unwind(Cypher.literalOf(1), Cypher.literalTrue(), Cypher.literalFalse()) + .as("n").returning(symbolicName("n")) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "UNWIND [1, true, false] AS n RETURN n"); + } + + @Test + void shouldRenderUnwind() { + + Statement statement; + + AliasedExpression collected = collect(bikeNode).as("collected"); + statement = Cypher.match(bikeNode) + .with(collected) + .unwind(collected).as("x") + .with("x") + .delete(Cypher.symbolicName("x")) + .returning("x") + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (b:`Bike`) WITH collect(b) AS collected UNWIND collected AS x WITH x DELETE x RETURN x"); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 6df72815f..d08196331 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -19,999 +19,48 @@ package org.springframework.data.neo4j.core.cypher; import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.neo4j.core.cypher.Conditions.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.core.cypher.Functions.*; -import org.junit.jupiter.api.Nested; +import java.util.ArrayList; +import java.util.List; + import org.junit.jupiter.api.Test; -import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; -import org.springframework.data.neo4j.core.cypher.renderer.Renderer; /** * @author Michael J. Simons - * @author Gerrit Meier */ -public class CypherTest { +class CypherTest { - private final Renderer cypherRenderer = CypherRenderer.create(); - private final Node bikeNode = Cypher.node("Bike").named("b"); - private final Node userNode = Cypher.node("User").named("u"); - - @Nested - class SingleQuerySinglePart { - - @Nested - class ReadingAndReturn { - - @Test - void unrelatedNodes() { - Statement statement = Cypher.match(bikeNode, userNode, Cypher.node("U").named("o")) - .returning(bikeNode, userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (b:`Bike`), (u:`User`), (o:`U`) RETURN b, u"); - } - - @Test - void asterikShouldWork() { - Statement statement = Cypher.match(bikeNode, userNode, Cypher.node("U").named("o")) - .returning(Cypher.asterisk()) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (b:`Bike`), (u:`User`), (o:`U`) RETURN *"); - } - - @Test - void aliasedExpressionsInReturn() { - Node unnamedNode = Cypher.node("ANode"); - Node namedNode = Cypher.node("AnotherNode").named("o"); - Statement statement = Cypher.match(unnamedNode, namedNode) - .returning(unnamedNode.as("theNode"), namedNode.as("theOtherNode")) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (:`ANode`), (o:`AnotherNode`) RETURN (:`ANode`) AS theNode, o AS theOtherNode"); - } - - @Test - void simpleRelationship() { - Statement statement = Cypher - .match(userNode.relationshipTo(bikeNode, "OWNS")) - .returning(bikeNode, userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) RETURN b, u"); - } - - @Test - void simpleRelationshipWithReturn() { - Relationship owns = userNode - .relationshipTo(bikeNode, "OWNS").named("o"); - - Statement statement = Cypher - .match(owns) - .returning(bikeNode, userNode, owns) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`)-[o:`OWNS`]->(b:`Bike`) RETURN b, u, o"); - } - - @Test - void chainedRelations() { - Node tripNode = Cypher.node("Trip").named("t"); - Statement statement = Cypher - .match(userNode - .relationshipTo(bikeNode, "OWNS").named("r1") - .relationshipTo(tripNode, "USED_ON").named("r2") - ) - .where(userNode.property("name").matches(".*aName")) - .returning(bikeNode, userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`)-[r1:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); - - statement = Cypher - .match(userNode - .relationshipTo(bikeNode, "OWNS") - .relationshipTo(tripNode, "USED_ON").named("r2") - ) - .where(userNode.property("name").matches(".*aName")) - .returning(bikeNode, userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`) WHERE u.name =~ '.*aName' RETURN b, u"); - - - statement = Cypher - .match(userNode - .relationshipTo(bikeNode, "OWNS") - .relationshipTo(tripNode, "USED_ON").named("r2") - .relationshipFrom(userNode, "WAS_ON").named("x") - .relationshipBetween(Cypher.node("SOMETHING")).named("y") - ) - .where(userNode.property("name").matches(".*aName")) - .returning(bikeNode, userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`)-[r2:`USED_ON`]->(t:`Trip`)<-[x:`WAS_ON`]-(u)-[y]-(:`SOMETHING`) WHERE u.name =~ '.*aName' RETURN b, u"); - } - - @Test - void sortOrderDefault() { - Statement statement = Cypher.match(userNode).returning(userNode).orderBy(sort(userNode.property("name"))).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u ORDER BY u.name"); - } - - @Test - void sortOrderAscending() { - Statement statement = Cypher.match(userNode).returning(userNode) - .orderBy(Cypher.sort(userNode.property("name")).ascending()).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); - } - - @Test - void sortOrderDescending() { - Statement statement = Cypher.match(userNode).returning(userNode) - .orderBy(Cypher.sort(userNode.property("name")).descending()).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u ORDER BY u.name DESC"); - } - - @Test - void sortOrderConcatenation() { - Statement statement = Cypher.match(userNode).returning(userNode) - .orderBy( - sort(userNode.property("name")).descending(), - sort(userNode.property("age")).ascending() - ) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u ORDER BY u.name DESC, u.age ASC"); - } - - @Test - void sortOrderDefaultExpression() { - Statement statement = Cypher.match(userNode).returning(userNode) - .orderBy(userNode.property("name").ascending()).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); - } - - @Test - void sortOrderAscendingExpression() { - Statement statement = Cypher.match(userNode).returning(userNode) - .orderBy(userNode.property("name").ascending()).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u ORDER BY u.name ASC"); - } - - @Test - void sortOrderDescendingExpression() { - Statement statement = Cypher.match(userNode).returning(userNode) - .orderBy(userNode.property("name").descending()).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u ORDER BY u.name DESC"); - } - - @Test - void sortOrderConcatenationExpression() { - Statement statement = Cypher.match(userNode).returning(userNode) - .orderBy(userNode.property("name")).descending() - .and(userNode.property("age")).ascending() - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u ORDER BY u.name DESC, u.age ASC"); - } - - @Test - void skip() { - Statement statement = Cypher.match(userNode).returning(userNode).skip(1).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u SKIP 1"); - } - - @Test - void nullSkip() { - Statement statement = Cypher.match(userNode).returning(userNode).skip(null).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u"); - } - - @Test - void limit() { - Statement statement = Cypher.match(userNode).returning(userNode).limit(1).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u LIMIT 1"); - } - - @Test - void nullLimit() { - Statement statement = Cypher.match(userNode).returning(userNode).limit(null).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u"); - } - - @Test - void skipAndLimit() { - Statement statement = Cypher.match(userNode).returning(userNode).skip(1).limit(1).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u SKIP 1 LIMIT 1"); - } - - @Test - void nullskipAndLimit() { - Statement statement = Cypher.match(userNode).returning(userNode).skip(null).limit(null).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u"); - } - - - @Test - void distinct() { - Statement statement = Cypher.match(userNode).returningDistinct(userNode).skip(1).limit(1).build(); - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN DISTINCT u SKIP 1 LIMIT 1"); - } - } + @Test + void shouldNotCreateIllegalLiterals() { + assertThatIllegalArgumentException().isThrownBy(() -> Cypher.literalOf(new CypherTest())) + .withMessageStartingWith("Unsupported literal type: "); } - @Nested - class SingleQueryMultiPart { - @Test - void simpleWith() { - Statement statement = Cypher - .match(userNode.relationshipTo(bikeNode, "OWNS")) - .where(userNode.property("a").isNull()) - .with(bikeNode, userNode) - .returning(bikeNode) - .build(); + @Test + void shouldCreateListLiterals() { - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u RETURN b"); - } + List> params = new ArrayList<>(); + params.add(Cypher.literalFalse()); + params.add(Cypher.literalTrue()); - @Test - void simpleWithChained() { + Literal listLiteral = Cypher.literalOf(params); - Node tripNode = Cypher.node("Trip").named("t"); - Statement statement = Cypher - .match(userNode.relationshipTo(bikeNode, "OWNS")) - .where(userNode.property("a").isNull()) - .with(bikeNode, userNode) - .match(tripNode) - .where(tripNode.property("name").isEqualTo(literalOf("Festive500"))) - .with(tripNode) - .returning(bikeNode, userNode, tripNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u MATCH (t:`Trip`) WHERE t.name = 'Festive500' WITH t RETURN b, u, t"); - } - - @Test - void deletingSimpleWith() { - Statement statement = Cypher - .match(userNode.relationshipTo(bikeNode, "OWNS")) - .where(userNode.property("a").isNull()) - .delete(userNode) - .with(bikeNode, userNode) - .returning(bikeNode, userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL DELETE u WITH b, u RETURN b, u"); - } - - @Test - void deletingSimpleWithReverse() { - Statement statement = Cypher - .match(userNode.relationshipTo(bikeNode, "OWNS")) - .where(userNode.property("a").isNull()) - .with(bikeNode, userNode) - .delete(userNode) - .returning(bikeNode, userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) WHERE u.a IS NULL WITH b, u DELETE u RETURN b, u"); - } - - @Test - void mixedClausesWithWith() { - - Node tripNode = Cypher.node("Trip").named("t"); - Statement statement = Cypher - .match(userNode.relationshipTo(bikeNode, "OWNS")) - .match(tripNode) - .delete(tripNode) - .with(bikeNode, tripNode) - .match(userNode) - .with(bikeNode, userNode) - .returning(bikeNode, userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`)-[:`OWNS`]->(b:`Bike`) MATCH (t:`Trip`) DELETE t WITH b, t MATCH (u) WITH b, u RETURN b, u"); - } + assertThat(listLiteral).isInstanceOf(ListLiteral.class) + .returns("[false, true]", v -> v.asString()); } - @Nested - class MultipleMatches { - @Test - void simple() { - Statement statement = Cypher - .match(bikeNode) - .match(userNode, Cypher.node("U").named("o")) - .returning(bikeNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) RETURN b"); - } - - @Test - void simpleWhere() { - Statement statement = Cypher - .match(bikeNode) - .match(userNode, Cypher.node("U").named("o")) - .where(userNode.property("a").isNull()) - .returning(bikeNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); - } - - @Test - void multiWhere() { - Statement statement = Cypher - .match(bikeNode) - .where(bikeNode.property("a").isNotNull()) - .match(userNode, Cypher.node("U").named("o")) - .where(userNode.property("a").isNull()) - .returning(bikeNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (b:`Bike`) WHERE b.a IS NOT NULL MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); - } - - @Test - void multiWhereMultiConditions() { - Statement statement = Cypher - .match(bikeNode) - .where(bikeNode.property("a").isNotNull()) - .and(bikeNode.property("b").isNull()) - .match(userNode, Cypher.node("U").named("o")) - .where(userNode.property("a").isNull().or(userNode.internalId().isEqualTo(literalOf(4711)))) - .returning(bikeNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (b:`Bike`) WHERE (b.a IS NOT NULL AND b.b IS NULL) MATCH (u:`User`), (o:`U`) WHERE (u.a IS NULL OR id(u) = 4711) RETURN b"); - } - - @Test - void optional() { - Statement statement = Cypher - .optionalMatch(bikeNode) - .match(userNode, Cypher.node("U").named("o")) - .where(userNode.property("a").isNull()) - .returning(bikeNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("OPTIONAL MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); - } - - @Test - void optionalNext() { - Statement statement = Cypher - .match(bikeNode) - .optionalMatch(userNode, Cypher.node("U").named("o")) - .where(userNode.property("a").isNull()) - .returning(bikeNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (b:`Bike`) OPTIONAL MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); - } + @Test + void shouldCreatePropertyPointingToSymbolicName() { + Property property = Cypher.property("a", "b"); + property.accept(segment -> { + if (segment instanceof Property) { + assertThat(segment).extracting("name").containsOnly("b"); + } else if (segment instanceof SymbolicName) { + assertThat(segment).extracting("name").containsOnly("a"); + } else { + fail("Unexpected segment"); + } + }); } - @Nested - class FunctionRendering { - @Test - void inWhereClause() { - Statement statement = Cypher.match(userNode).where(userNode.internalId().isEqualTo(literalOf(1L))) - .returning(userNode).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE id(u) = 1 RETURN u"); - } - - @Test - void inReturnClause() { - Statement statement = Cypher.match(userNode).returning(Functions.count(userNode)).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN count(u)"); - } - - @Test - void aliasedInReturnClause() { - Statement statement = Cypher.match(userNode).returning(Functions.count(userNode).as("cnt")).build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN count(u) AS cnt"); - } - - @Test - void shouldSupportMoreThanOneArgument() { - Statement statement = Cypher.match(userNode) - .returning(coalesce(userNode.property("a"), userNode.property("b"), Cypher.literalOf("¯\\_(ツ)_/¯"))) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN coalesce(u.a, u.b, '¯\\\\_(ツ)_/¯')"); - } - - @Test - void literalsShouldDealWithNull() { - Statement statement = Cypher.match(userNode) - .returning(Functions.coalesce(Cypher.literalOf(null), userNode.property("field")).as("p")) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN coalesce(NULL, u.field) AS p"); - } - } - - @Nested - class ComparisonRendering { - - @Test - void equalsWithStringLiteral() { - Statement statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(Cypher.literalOf("Test"))) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE u.name = 'Test' RETURN u"); - } - - @Test - void equalsWithNumberLiteral() { - Statement statement = Cypher.match(userNode) - .where(userNode.property("age").isEqualTo(Cypher.literalOf(21))) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE u.age = 21 RETURN u"); - } - } - - @Nested - class Conditions { - @Test - void conditionsChainingAnd() { - Statement statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(Cypher.literalOf("Test")) - .and(userNode.property("age").isEqualTo(Cypher.literalOf(21)))) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (u.name = 'Test' AND u.age = 21) RETURN u"); - } - - @Test - void conditionsChainingOr() { - Statement statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(Cypher.literalOf("Test")) - .or(userNode.property("age").isEqualTo(Cypher.literalOf(21)))) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (u.name = 'Test' OR u.age = 21) RETURN u"); - } - - @Test - void conditionsChainingXor() { - Statement statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(Cypher.literalOf("Test")) - .xor(userNode.property("age").isEqualTo(Cypher.literalOf(21)))) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (u.name = 'Test' XOR u.age = 21) RETURN u"); - } - - @Test - void chainingOnWhere() { - Statement statement; - - Literal test = literalOf("Test"); - Literal foobar = literalOf("foobar"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(test)) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`) WHERE u.name = 'Test' RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(test)) - .and(userNode.property("name").isEqualTo(test)) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'Test') RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(test)) - .and(userNode.property("name").isEqualTo(test)) - .and(userNode.property("name").isEqualTo(test)) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'Test' AND u.name = 'Test') RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(test)) - .or(userNode.property("name").isEqualTo(test)) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'Test') RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(test)) - .or(userNode.property("name").isEqualTo(test)) - .or(userNode.property("name").isEqualTo(test)) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo("MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'Test' OR u.name = 'Test') RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(test)) - .and(userNode.property("name").isEqualTo(test)) - .or(userNode.property("name").isEqualTo(foobar)) - .and(userNode.property("name").isEqualTo(test)) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'Test') OR u.name = 'foobar') AND u.name = 'Test') RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(test)) - .or(userNode.property("name").isEqualTo(foobar)) - .and(userNode.property("name").isEqualTo(test)) - .and(userNode.property("name").isEqualTo(test)) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE ((u.name = 'Test' OR u.name = 'foobar') AND u.name = 'Test' AND u.name = 'Test') RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(test)) - .or(userNode.property("name").isEqualTo(foobar)) - .and(userNode.property("name").isEqualTo(test)) - .or(userNode.property("name").isEqualTo(foobar)) - .and(userNode.property("name").isEqualTo(test)) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE ((((u.name = 'Test' OR u.name = 'foobar') AND u.name = 'Test') OR u.name = 'foobar') AND u.name = 'Test') RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isNotNull()) - .and(userNode.property("name").isEqualTo(test)) - .or(userNode.property("age").isEqualTo(Cypher.literalOf(21))) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE ((u.name IS NOT NULL AND u.name = 'Test') OR u.age = 21) RETURN u"); - } - - @Test - void chainingOnConditions() { - Statement statement; - - Literal test = literalOf("Test"); - Literal foobar = literalOf("foobar"); - Literal bazbar = literalOf("bazbar"); - - statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(test) - .or(userNode.property("name").isEqualTo(foobar)) - .or(userNode.property("name").isEqualTo(foobar)) - ) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (u.name = 'Test' OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); - - statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(test) - .and(userNode.property("name").isEqualTo(bazbar)) - .or(userNode.property("name").isEqualTo(foobar)) - .or(userNode.property("name").isEqualTo(foobar)) - ) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE ((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); - - statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(test)) - .and( - userNode.property("name").isEqualTo(bazbar) - .and(userNode.property("name").isEqualTo(foobar)) - ) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (u.name = 'Test' AND u.name = 'bazbar' AND u.name = 'foobar') RETURN u"); - - statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(test) - .and(userNode.property("name").isEqualTo(bazbar)) - .or(userNode.property("name").isEqualTo(foobar)) - .or(userNode.property("name").isEqualTo(foobar)) - ) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE ((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') RETURN u"); - - statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(test) - .and(userNode.property("name").isEqualTo(bazbar)) - .or(userNode.property("name").isEqualTo(foobar)) - .or(userNode.property("name").isEqualTo(foobar)) - .and(userNode.property("name").isEqualTo(bazbar)) - ) - .returning(userNode) - .build(); - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') AND u.name = 'bazbar') RETURN u"); - } - - @Test - void chainingCombined() { - Statement statement; - - Literal test = literalOf("Test"); - Literal foobar = literalOf("foobar"); - Literal bazbar = literalOf("bazbar"); - - statement = Cypher.match(userNode) - .where( - userNode.property("name").isEqualTo(test) - .and(userNode.property("name").isEqualTo(bazbar)) - .or(userNode.property("name").isEqualTo(foobar)) - .or(userNode.property("name").isEqualTo(foobar)) - ) - .and( - userNode.property("name").isEqualTo(bazbar) - .and(userNode.property("name").isEqualTo(foobar)) - .or(userNode.property("name").isEqualTo(test)) - .not() - ) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (((u.name = 'Test' AND u.name = 'bazbar') OR u.name = 'foobar' OR u.name = 'foobar') AND NOT (((u.name = 'bazbar' AND u.name = 'foobar') OR u.name = 'Test'))) RETURN u"); - - } - - @Test - void negatedConditions() { - Statement statement = Cypher.match(userNode) - .where(userNode.property("name").isNotNull().not()) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE NOT (u.name IS NOT NULL) RETURN u"); - } - - @Test - void noConditionShouldNotBeRendered() { - Statement statement; - statement = Cypher.match(userNode) - .where(noCondition()) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("name").isEqualTo(Cypher.literalOf("test"))) - .and(noCondition()).or(noCondition()) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE u.name = 'test' RETURN u"); - } - } - - @Nested - class DeleteClause { - - @Test - void shouldRenderDeleteWithoutReturn() { - - Statement statement; - statement = Cypher.match(userNode) - .detachDelete(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) DETACH DELETE u"); - - statement = Cypher.match(userNode) - .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) - .delete(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DELETE u"); - - statement = Cypher.match(userNode, bikeNode) - .delete(userNode, bikeNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`), (b:`Bike`) DELETE u, b"); - } - - @Test - void shouldRenderDeleteWithReturn() { - - Statement statement; - statement = Cypher.match(userNode) - .detachDelete(userNode) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) DETACH DELETE u RETURN u"); - - statement = Cypher.match(userNode) - .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) - .detachDelete(userNode) - .returning(userNode).orderBy(userNode.property("a").ascending()).skip(2).limit(1) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DETACH DELETE u RETURN u ORDER BY u.a ASC SKIP 2 LIMIT 1"); - - statement = Cypher.match(userNode) - .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) - .detachDelete(userNode) - .returningDistinct(userNode).orderBy(userNode.property("a").ascending()).skip(2).limit(1) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE (u.a IS NOT NULL AND u.b IS NULL) DETACH DELETE u RETURN DISTINCT u ORDER BY u.a ASC SKIP 2 LIMIT 1"); - } - - @Test - void shouldRenderNodeDelete() { - Node n = anyNode("n"); - Relationship r = n.relationshipBetween(anyNode()).named("r0"); - Statement statement = Cypher - .match(n).where(n.internalId().isEqualTo(literalOf(4711))) - .optionalMatch(r) - .delete(r, n) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (n) WHERE id(n) = 4711 OPTIONAL MATCH (n)-[r0]-() DELETE r0, n"); - } - - @Test - void shouldRenderChainedDeletes() { - Node n = anyNode("n"); - Relationship r = n.relationshipBetween(anyNode()).named("r0"); - Statement statement = Cypher - .match(n).where(n.internalId().isEqualTo(literalOf(4711))) - .optionalMatch(r) - .delete(r, n) - .delete(bikeNode) - .detachDelete(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (n) WHERE id(n) = 4711 OPTIONAL MATCH (n)-[r0]-() DELETE r0, n DELETE b DETACH DELETE u"); - } - } - - @Nested - class Expressions { - @Test - void shouldRenderParameters() { - Statement statement; - statement = Cypher.match(userNode) - .where(userNode.property("a").isEqualTo(parameter("aParameter"))) - .detachDelete(userNode) - .returning(userNode) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (u:`User`) WHERE u.a = $aParameter DETACH DELETE u RETURN u"); - } - } - - @Nested - class OperationsAndComparisions { - - @Test - void shouldRenderOperations() { - Statement statement; - statement = Cypher.match(Cypher.anyNode("n")) - .returning(literalOf(1).plus(literalOf(2))) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (n) RETURN (1 + 2)"); - } - - @Test - void shouldRenderComparision() { - Statement statement; - statement = Cypher.match(Cypher.anyNode("n")) - .returning(literalOf(1).gt(literalOf(2))) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (n) RETURN 1 > 2"); - - statement = Cypher.match(Cypher.anyNode("n")) - .returning(literalOf(1).gt(literalOf(2)).isTrue()) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (n) RETURN (1 > 2) = true"); - - statement = Cypher.match(Cypher.anyNode("n")) - .returning(literalOf(1).gt(literalOf(2)).isTrue().isFalse()) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (n) RETURN ((1 > 2) = true) = false"); - } - } - - @Nested - class ExpressionsRendering { - @Test - void shouldRenderMap() { - Statement statement; - statement = Cypher.match(Cypher.anyNode("n")) - .returning( - Functions.point( - Cypher.mapOf( - "latitude", Cypher.parameter("latitude"), - "longitude", Cypher.parameter("longitude"), - "crs", Cypher.literalOf(4326) - ) - ) - ) - .build(); - - assertThat(cypherRenderer.render(statement)) - .isEqualTo( - "MATCH (n) RETURN point({latitude: $latitude, longitude: $longitude, crs: 4326})"); - } - } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java index eb1a868f1..eba3a948c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java @@ -19,6 +19,8 @@ package org.springframework.data.neo4j.core.cypher; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.neo4j.core.cypher.Cypher.*; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -28,6 +30,8 @@ import org.junit.jupiter.api.Test; */ class FunctionsTest { + private static final String FUNCTION_NAME_FIELD = "functionName"; + @Nested class id { @@ -40,6 +44,13 @@ class FunctionsTest { assertThatIllegalArgumentException().isThrownBy(() -> Functions.id((Relationship) null)) .withMessage("The relationship parameter is required."); } + + @Test + void shouldCreateCorrectInvocation() { + + FunctionInvocation invocation = Functions.id(node("Test")); + assertThat(invocation).hasFieldOrPropertyWithValue(FUNCTION_NAME_FIELD, "id"); + } } @Nested @@ -51,6 +62,13 @@ class FunctionsTest { assertThatIllegalArgumentException().isThrownBy(() -> Functions.count(null)) .withMessage("The expression to count is required."); } + + @Test + void shouldCreateCorrectInvocation() { + + FunctionInvocation invocation = Functions.count(mock(Expression.class)); + assertThat(invocation).hasFieldOrPropertyWithValue(FUNCTION_NAME_FIELD, "count"); + } } @Nested @@ -68,5 +86,30 @@ class FunctionsTest { assertThatIllegalArgumentException().isThrownBy(() -> Functions.coalesce(new Expression[] { null })) .withMessage("At least one expression is required."); } + + @Test + void shouldCreateCorrectInvocation() { + + FunctionInvocation invocation = Functions.coalesce(mock(Expression.class)); + assertThat(invocation).hasFieldOrPropertyWithValue(FUNCTION_NAME_FIELD, "coalesce"); + } + } + + @Nested + class collect { + + @Test + void preconditionsShouldBeAsserted() { + + assertThatIllegalArgumentException().isThrownBy(() -> Functions.collect(null)) + .withMessage("The expression to collect is required."); + } + + @Test + void shouldCreateCorrectInvocation() { + + FunctionInvocation invocation = Functions.collect(mock(Expression.class)); + assertThat(invocation).hasFieldOrPropertyWithValue(FUNCTION_NAME_FIELD, "collect"); + } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java index aeb2f46ce..44fbdf424 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java @@ -20,7 +20,12 @@ package org.springframework.data.neo4j.core.cypher; import static org.assertj.core.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; + import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; /** * @author Michael J. Simons @@ -46,6 +51,17 @@ class NodeTest { void shouldCreateNodes() { Node node = Node.create("primary", "secondary"); - assertThat(node.getLabels()).contains("primary", "secondary"); + List labels = new ArrayList<>(); + node.accept(new Visitor() { + @Override + public void enter(Visitable segment) { + + if (segment instanceof NodeLabel) { + labels.add(((NodeLabel) segment).getValue()); + } + + } + }); + assertThat(labels).contains("primary", "secondary"); } } From da5c115a96248983a56342a294c81f5f6ecf4662 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 28 May 2019 17:31:54 +0200 Subject: [PATCH 100/342] Improve performance of CypherRenderer. The CypherRenderer now keeps rendered statemens around for a while and the reflective visitor support keeps the method handles cached. Also, the entry to the renderer is now inside the interface and not on the concrete class. --- .../core/cypher/renderer/CypherRenderer.java | 62 ++++++++++++++++--- .../neo4j/core/cypher/renderer/Renderer.java | 9 +++ .../cypher/support/ReflectiveVisitor.java | 27 +++++--- .../repository/query/CypherQueryCreator.java | 4 +- .../support/SimpleNeo4jRepository.java | 3 +- .../support/SimpleQueryByExampleExecutor.java | 3 +- .../SimpleReactiveNeo4jRepository.java | 3 +- .../SimpleReactiveQueryByExampleExecutor.java | 3 +- .../data/neo4j/core/cypher/CypherIT.java | 3 +- 9 files changed, 87 insertions(+), 30 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java index 4d6145fcf..0d069ccf5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java @@ -18,6 +18,13 @@ */ package org.springframework.data.neo4j.core.cypher.renderer; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + import org.springframework.data.neo4j.core.cypher.Statement; /** @@ -25,21 +32,58 @@ import org.springframework.data.neo4j.core.cypher.Statement; * @author Gerrit Meier * @since 1.0 */ -public class CypherRenderer implements Renderer { +enum CypherRenderer implements Renderer { - private CypherRenderer() { - } + INSTANCE; - public static Renderer create() { - return new CypherRenderer(); - } + private final int STATEMENT_CACHE_SIZE = 128; + private final LinkedHashMap renderedStatementCache = new LRUCache<>(STATEMENT_CACHE_SIZE); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock read = lock.readLock(); + private final Lock write = lock.writeLock(); @Override public String render(Statement statement) { - RenderingVisitor renderingVisitor = new RenderingVisitor(); - statement.accept(renderingVisitor); + int key = Objects.hashCode(statement); - return renderingVisitor.getRenderedContent().trim(); + String renderedContent; + try { + read.lock(); + renderedContent = renderedStatementCache.get(key); + } finally { + read.unlock(); + } + + if (renderedContent == null) { + try { + write.lock(); + + RenderingVisitor renderingVisitor = new RenderingVisitor(); + statement.accept(renderingVisitor); + renderedContent = renderingVisitor.getRenderedContent().trim(); + + renderedStatementCache.put(key, renderedContent); + } finally { + write.unlock(); + } + } + + return renderedContent; + } + + private static class LRUCache extends LinkedHashMap { + private int cacheSize; + + LRUCache(int cacheSize) { + super(cacheSize / 4, 0.75f, true); + this.cacheSize = cacheSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= cacheSize; + } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java index e6ba3a572..7543aa998 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java @@ -33,4 +33,13 @@ public interface Renderer { * @return */ String render(Statement statement); + + /** + * Provides the default renderer. This method may or may not provide shared instances of the renderer. + * + * @return The default renderer + */ + static Renderer getDefaultRenderer() { + return CypherRenderer.INSTANCE; + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java index da0a12d72..593da6856 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java @@ -61,7 +61,11 @@ public abstract class ReflectiveVisitor implements Visitor { } } - private final Map> cachedHandles = new ConcurrentHashMap<>(); + /** + * A shared cache of unbound method handles for entering and leaving phases. + * The key is the concrete class of the visitor as well as the hierarchy of the visitable. + */ + private static final Map> VISITING_METHODS_CACHE = new ConcurrentHashMap<>(); /** Keeps track of the ASTs current level. */ private Deque currentVisitedElements = new LinkedList<>(); @@ -90,7 +94,7 @@ public abstract class ReflectiveVisitor implements Visitor { if (preEnter(visitable)) { currentVisitedElements.push(visitable); - executeConcreteMethodIn(new TargetAndPhase(visitable.getClass(), Phase.ENTER), visitable); + executeConcreteMethodIn(new TargetAndPhase(this, visitable.getClass(), Phase.ENTER), visitable); } } @@ -98,32 +102,33 @@ public abstract class ReflectiveVisitor implements Visitor { public final void leave(Visitable visitable) { if (currentVisitedElements.peek() == visitable) { - executeConcreteMethodIn(new TargetAndPhase(visitable.getClass(), Phase.LEAVE), visitable); + executeConcreteMethodIn(new TargetAndPhase(this, visitable.getClass(), Phase.LEAVE), visitable); postLeave(visitable); currentVisitedElements.pop(); } } private void executeConcreteMethodIn(TargetAndPhase targetAndPhase, Visitable onVisitable) { - Optional optionalHandle = this.cachedHandles.computeIfAbsent(targetAndPhase, this::findHandleFor); + Optional optionalHandle = VISITING_METHODS_CACHE + .computeIfAbsent(targetAndPhase, ReflectiveVisitor::findHandleFor); optionalHandle.ifPresent(handle -> { try { - handle.invoke(onVisitable); + handle.invoke(this, onVisitable); } catch (Throwable throwable) { throwable.printStackTrace(); } }); } - private Optional findHandleFor(TargetAndPhase targetAndPhase) { + private static Optional findHandleFor(TargetAndPhase targetAndPhase) { for (Class clazz : targetAndPhase.classHierarchyOfVisitable) { try { // Using MethodHandles.lookup().findVirtual() doesn't allow to make a protected method accessible. - Method method = this.getClass() + Method method = targetAndPhase.visitorClass .getDeclaredMethod(targetAndPhase.phase.methodName, clazz); method.setAccessible(true); - return Optional.of(MethodHandles.lookup().in(this.getClass()).unreflect(method).bindTo(this)); + return Optional.of(MethodHandles.lookup().in(targetAndPhase.visitorClass).unreflect(method)); } catch (IllegalAccessException | NoSuchMethodException e) { // We don't do anything if the method doesn't exists // Try the next parameter type in the hierarchy @@ -134,11 +139,15 @@ public abstract class ReflectiveVisitor implements Visitor { @EqualsAndHashCode private static class TargetAndPhase { + private final Class visitorClass; + private final List> classHierarchyOfVisitable; private final Phase phase; - TargetAndPhase(Class concreteVisitableClass, Phase phase) { + TargetAndPhase(T visitor, Class concreteVisitableClass, + Phase phase) { + this.visitorClass = visitor.getClass(); this.phase = phase; this.classHierarchyOfVisitable = new ArrayList<>(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index f71afe07e..0eb30770e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -49,7 +49,7 @@ import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.SortItem; import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; +import org.springframework.data.neo4j.core.cypher.renderer.Renderer; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.core.schema.NodeDescription; @@ -138,7 +138,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { .limit(maxResults) .build(); - return CypherRenderer.create().render(statement); + return Renderer.getDefaultRenderer().render(statement); } private Condition createImpl(Part part, Iterator actualParameters) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 786fba834..986eca28c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -44,7 +44,6 @@ import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndReturn; -import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.repository.PagingAndSortingRepository; @@ -64,7 +63,7 @@ import org.springframework.transaction.annotation.Transactional; @Slf4j class SimpleNeo4jRepository implements PagingAndSortingRepository { - private static final Renderer renderer = CypherRenderer.create(); + private static final Renderer renderer = Renderer.getDefaultRenderer(); private final Neo4jClient neo4jClient; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java index 2bcffc43a..15143b848 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java @@ -44,7 +44,6 @@ import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableStatement; -import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.query.QueryByExampleExecutor; @@ -61,7 +60,7 @@ import org.springframework.data.repository.support.PageableExecutionUtils; @RequiredArgsConstructor(access = PACKAGE) class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { - private static final Renderer renderer = CypherRenderer.create(); + private static final Renderer renderer = Renderer.getDefaultRenderer(); private final Neo4jClient neo4jClient; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java index 32191488b..f34be8fb5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java @@ -37,7 +37,6 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.core.ReactiveNeo4jClient.ExecutableQuery; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.repository.reactive.ReactiveSortingRepository; @@ -56,7 +55,7 @@ import org.springframework.transaction.annotation.Transactional; @Slf4j class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository { - private static final Renderer renderer = CypherRenderer.create(); + private static final Renderer renderer = Renderer.getDefaultRenderer(); private final ReactiveNeo4jClient neo4jClient; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java index b611b11c4..384d9fb5e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java @@ -39,7 +39,6 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jClient; import org.springframework.data.neo4j.core.ReactiveNeo4jClient.ExecutableQuery; import org.springframework.data.neo4j.core.cypher.Functions; import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; @@ -56,7 +55,7 @@ import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; @RequiredArgsConstructor(access = PACKAGE) class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleExecutor { - private static final Renderer renderer = CypherRenderer.create(); + private static final Renderer renderer = Renderer.getDefaultRenderer(); private final ReactiveNeo4jClient neo4jClient; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java index be0471064..51ade0536 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java @@ -25,7 +25,6 @@ import static org.springframework.data.neo4j.core.cypher.Functions.*; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.data.neo4j.core.cypher.renderer.CypherRenderer; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; /** @@ -34,7 +33,7 @@ import org.springframework.data.neo4j.core.cypher.renderer.Renderer; */ class CypherIT { - private final Renderer cypherRenderer = CypherRenderer.create(); + private static final Renderer cypherRenderer = Renderer.getDefaultRenderer(); private final Node bikeNode = Cypher.node("Bike").named("b"); private final Node userNode = Cypher.node("User").named("u"); From d1ed5a78e5b4ea06159abdec449f0c4dbf3a1b28 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 28 May 2019 21:22:07 +0200 Subject: [PATCH 101/342] Allow SET after CREATE. --- .../data/neo4j/core/cypher/Cypher.java | 3 ++- .../core/cypher/DefaultStatementBuilder.java | 7 +++++-- .../data/neo4j/core/cypher/StatementBuilder.java | 15 +++++++++------ .../data/neo4j/core/cypher/CypherIT.java | 12 ++++++++++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index 3f4e0029b..5aa4df0d1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -20,6 +20,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesMatch; +import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesSet; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndWith; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingUnwind; @@ -148,7 +149,7 @@ public final class Cypher { * @param pattern The patterns to create * @return An ongoing {@code CREATE} that can be used to specify {@code WITH} and {@code RETURNING} etc. */ - public static OngoingUpdate create(PatternElement... pattern) { + public static T create(PatternElement... pattern) { return Statement.builder().create(pattern); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index 5268561df..b3e2feb80 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -183,7 +183,10 @@ class DefaultStatementBuilder @Override public OngoingMatchAndUpdate set(Expression... expressions) { - + if (this.currentOngoingUpdate != null) { + this.currentSinglePartElements.add(this.currentOngoingUpdate.buildUpdatingClause()); + this.currentOngoingUpdate = null; + } return new DefaultStatementWithUpdateBuilder(SET, expressions); } @@ -435,7 +438,7 @@ class DefaultStatementBuilder return DefaultStatementBuilder.this .addWith(buildWith()) - .detachDelete(expressions); + .set(expressions); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index a2a73ab1e..eed063e9e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -49,7 +49,7 @@ public interface StatementBuilder { * @param pattern * @return */ - OngoingUpdate create(PatternElement... pattern); + T create(PatternElement... pattern); /** * @see Cypher#merge(PatternElement...) @@ -340,10 +340,7 @@ public interface StatementBuilder { T detachDelete(Expression... expressions); } - /** - * A step that exposes the set clause. - */ - interface ExposesSetAndRemove { + interface ExposesSet { /** * Adds a {@code SET} clause to the statement. The list of expressions must be even, each pair will be turned into @@ -353,6 +350,12 @@ public interface StatementBuilder { * @return An ongoing match and update */ T set(Expression... expressions); + } + + /** + * A step that exposes the set clause. + */ + interface ExposesSetAndRemove extends ExposesSet { T set(Node node, String... label); @@ -385,7 +388,7 @@ public interface StatementBuilder { interface ExposesCreate { - OngoingUpdate create(PatternElement... pattern); + T create(PatternElement... pattern); } interface ExposesMerge { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java index 51ade0536..57ed84e01 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java @@ -901,6 +901,18 @@ class CypherIT { @Nested class SetClause { + @Test + void shouldRenderSetAfterCreate() { + Statement statement; + statement = Cypher.create(userNode) + .set(userNode.property("p").to(literalOf("Hallo, Welt"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) SET u.p = 'Hallo, Welt'"); + } + @Test void shouldRenderSet() { From 743e4d6e406711b23f726b6060ddf47754569e43 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 29 May 2019 15:24:01 +0200 Subject: [PATCH 102/342] Add additional features to Cypher DSL. * Cypher.union for creating UNION and UNION ALL queries * Allow SET after MERGE like SET after CREATE * Allow updates directly after UNWIND --- .../data/neo4j/core/cypher/Cypher.java | 41 +++++- .../core/cypher/DefaultStatementBuilder.java | 9 +- .../neo4j/core/cypher/StatementBuilder.java | 6 +- .../{RegularQuery.java => UnionPart.java} | 32 ++++- .../data/neo4j/core/cypher/UnionQuery.java | 87 +++++++++++++ .../cypher/renderer/RenderingVisitor.java | 6 + .../data/neo4j/core/cypher/CypherIT.java | 117 +++++++++++++++++- 7 files changed, 289 insertions(+), 9 deletions(-) rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/{RegularQuery.java => UnionPart.java} (56%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index 5aa4df0d1..94df05820 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -18,7 +18,11 @@ */ package org.springframework.data.neo4j.core.cypher; +import java.util.ArrayList; +import java.util.List; + import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesSet; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndWith; @@ -26,6 +30,7 @@ import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadin import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingUnwind; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingUpdate; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * The main entry point into the Cypher DSL. @@ -173,7 +178,7 @@ public final class Cypher { * @param pattern The patterns to merge * @return An ongoing {@code MERGE} that can be used to specify {@code WITH} and {@code RETURNING} etc. */ - public static OngoingUpdate merge(PatternElement... pattern) { + public static T merge(PatternElement... pattern) { return Statement.builder().merge(pattern); } @@ -272,6 +277,40 @@ public final class Cypher { return BooleanLiteral.FALSE; } + public static Statement union(Statement... statement) { + return unionImpl(false, statement); + } + + public static Statement unionAll(Statement... statement) { + return unionImpl(true, statement); + } + + private static Statement unionImpl(boolean unionAll, Statement... statements) { + + Assert.notEmpty(statements, "At least two statements are required!"); + + int i = 0; + UnionQuery existingUnionQuery = null; + if (statements[0] instanceof UnionQuery) { + existingUnionQuery = (UnionQuery) statements[0]; + Assert.state(existingUnionQuery.isAll() == unionAll, "Cannot mix union and union all!"); + i = 1; + } + + List listOfQueries = new ArrayList<>(); + while (i < statements.length) { + Assert.isInstanceOf(SingleQuery.class, statements[i], "Can only union single queries!"); + listOfQueries.add((SingleQuery) statements[i]); + ++i; + } + + if (existingUnionQuery == null) { + return UnionQuery.create(unionAll, listOfQueries); + } else { + return existingUnionQuery.addAdditionalQueries(listOfQueries); + } + } + /** * Not to be instantiated. */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index b3e2feb80..0fa170dd8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -435,10 +435,10 @@ class DefaultStatementBuilder @Override public OngoingMatchAndUpdate detachDelete(Expression... expressions) { - return DefaultStatementBuilder.this + .addWith(buildWith()) - .set(expressions); + .detachDelete(expressions); } @Override @@ -653,6 +653,11 @@ class DefaultStatementBuilder return delete(true, deletedExpressions); } + @Override + public T merge(PatternElement... pattern) { + throw new UnsupportedOperationException("Not supported yet"); + } + private OngoingMatchAndUpdate delete(boolean nextDetach, Expression... deletedExpressions) { DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); return DefaultStatementBuilder.this.new DefaultStatementWithUpdateBuilder( diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index eed063e9e..57a5c66d9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -56,7 +56,7 @@ public interface StatementBuilder { * @param pattern * @return */ - OngoingUpdate merge(PatternElement... pattern); + T merge(PatternElement... pattern); OngoingUnwind unwind(Expression expression); @@ -307,7 +307,7 @@ public interface StatementBuilder { /** * A step providing all the supported updating clauses (DELETE, SET) */ - interface ExposesUpdatingClause extends ExposesDelete, ExposesSetAndRemove { + interface ExposesUpdatingClause extends ExposesDelete, ExposesMerge, ExposesSetAndRemove { } /** @@ -393,7 +393,7 @@ public interface StatementBuilder { interface ExposesMerge { - OngoingUpdate merge(PatternElement... pattern); + T merge(PatternElement... pattern); } interface ExposesUnwind { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionPart.java similarity index 56% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionPart.java index e62cab0df..91564d340 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RegularQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionPart.java @@ -19,13 +19,41 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; +import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.data.neo4j.core.cypher.support.Visitor; /** - * See RegularQuery. + * Represents a part of an union. * * @author Michael J. Simons * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -public interface RegularQuery { +public final class UnionPart implements Visitable { + + private final boolean all; + + private final SingleQuery query; + + UnionPart(boolean all, SingleQuery query) { + this.all = all; + this.query = query; + } + + public boolean isAll() { + return all; + } + + SingleQuery getQuery() { + return query; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + query.accept(visitor); + visitor.leave(this); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java new file mode 100644 index 000000000..9045c15e1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.cypher; + +import static java.util.stream.Collectors.*; + +import java.util.ArrayList; +import java.util.List; + +import org.apiguardian.api.API; +import org.springframework.data.neo4j.core.cypher.Statement.RegularQuery; +import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; + +/** + * @author Michael J. Simons + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class UnionQuery implements RegularQuery { + + static UnionQuery create(boolean unionAll, List queries) { + + Assert.notNull(queries, "Queries are needed."); + Assert.state(queries.size() >= 2, "At least two queries are needed."); + + List unionParts = queries.stream().skip(1).map(q -> new UnionPart(unionAll, q)).collect(toList()); + return new UnionQuery(unionAll, queries.get(0), unionParts); + } + + private final boolean all; + + private final SingleQuery firstQuery; + + private final List additionalQueries; + + private UnionQuery(boolean all, SingleQuery firstQuery, List additionalQueries) { + this.all = all; + this.firstQuery = firstQuery; + this.additionalQueries = additionalQueries; + } + + /** + * Creates a new union query by appending more parts + * + * @param newAdditionalQueries more additional queries + * @return A new union query + */ + UnionQuery addAdditionalQueries(List newAdditionalQueries) { + + List queries = new ArrayList<>(); + queries.add(firstQuery); + queries.addAll(additionalQueries.stream().map(UnionPart::getQuery).collect(toList())); + queries.addAll(newAdditionalQueries); + + return create(this.isAll(), queries); + } + + boolean isAll() { + return all; + } + + @Override + public void accept(Visitor visitor) { + + visitor.enter(this); + this.firstQuery.accept(visitor); + this.additionalQueries.forEach(q -> q.accept(visitor)); + visitor.leave(this); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java index 08872a48c..d8c05671d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java @@ -378,7 +378,13 @@ class RenderingVisitor extends ReflectiveVisitor { .append(" "); } + void enter(UnionPart unionPart) { + builder.append(" UNION "); + if (unionPart.isAll()) { + builder.append("ALL "); + } + } void enter(Set set) { builder.append("SET "); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java index 57ed84e01..e43f26add 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java @@ -1606,6 +1606,21 @@ class CypherIT { "UNWIND [1, true, false] AS n RETURN n"); } + @Test + void shouldRenderLeadingUnwindWithUpdate() { + + Statement statement; + statement = Cypher.unwind(Cypher.literalOf(1), Cypher.literalTrue(), Cypher.literalFalse()) + .as("n") + .merge(bikeNode.properties("b", symbolicName("n"))) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "UNWIND [1, true, false] AS n MERGE (b:`Bike` {b: n}) RETURN b"); + } + @Test void shouldRenderUnwind() { @@ -1616,7 +1631,7 @@ class CypherIT { .with(collected) .unwind(collected).as("x") .with("x") - .delete(Cypher.symbolicName("x")) + .delete(symbolicName("x")) .returning("x") .build(); @@ -1625,4 +1640,104 @@ class CypherIT { "MATCH (b:`Bike`) WITH collect(b) AS collected UNWIND collected AS x WITH x DELETE x RETURN x"); } } + + @Nested + class Unions { + + @Test + void shouldRenderUnions() { + + Statement statement1 = Cypher.match(bikeNode) + .where(bikeNode.property("a").isEqualTo(literalOf("A"))) + .returning(bikeNode) + .build(); + + Statement statement2 = Cypher.match(bikeNode) + .where(bikeNode.property("b").isEqualTo(literalOf("B"))) + .returning(bikeNode) + .build(); + + Statement statement; + statement = Cypher.union(statement1, statement2); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (b:`Bike`) WHERE b.a = 'A' RETURN b UNION MATCH (b) WHERE b.b = 'B' RETURN b"); + } + + @Test + void shouldRenderAllUnions() { + + Statement statement1 = Cypher.match(bikeNode) + .where(bikeNode.property("a").isEqualTo(literalOf("A"))) + .returning(bikeNode) + .build(); + + Statement statement2 = Cypher.match(bikeNode) + .where(bikeNode.property("b").isEqualTo(literalOf("B"))) + .returning(bikeNode) + .build(); + + Statement statement; + statement = Cypher.unionAll(statement1, statement2); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (b:`Bike`) WHERE b.a = 'A' RETURN b UNION ALL MATCH (b) WHERE b.b = 'B' RETURN b"); + } + + @Test + void shouldAppendToExistingUnions() { + + Statement statement1 = Cypher.match(bikeNode) + .where(bikeNode.property("a").isEqualTo(literalOf("A"))) + .returning(bikeNode) + .build(); + + Statement statement2 = Cypher.match(bikeNode) + .where(bikeNode.property("b").isEqualTo(literalOf("B"))) + .returning(bikeNode) + .build(); + + Statement statement; + statement = Cypher.unionAll(statement1, statement2); + + Statement statement3 = Cypher.match(bikeNode) + .where(bikeNode.property("c").isEqualTo(literalOf("C"))) + .returning(bikeNode) + .build(); + + statement = Cypher.unionAll(statement, statement3); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (b:`Bike`) WHERE b.a = 'A' RETURN b UNION ALL MATCH (b) WHERE b.b = 'B' RETURN b UNION ALL MATCH (b) WHERE b.c = 'C' RETURN b"); + } + + @Test + void shouldNotMix() { + + Statement statement1 = Cypher.match(bikeNode) + .where(bikeNode.property("a").isEqualTo(literalOf("A"))) + .returning(bikeNode) + .build(); + + Statement statement2 = Cypher.match(bikeNode) + .where(bikeNode.property("b").isEqualTo(literalOf("B"))) + .returning(bikeNode) + .build(); + + Statement statement; + statement = Cypher.unionAll(statement1, statement2); + + Statement statement3 = Cypher.match(bikeNode) + .where(bikeNode.property("c").isEqualTo(literalOf("C"))) + .returning(bikeNode) + .build(); + + assertThatIllegalStateException().isThrownBy(() -> + Cypher.union(statement, statement3)).withMessage("Cannot mix union and union all!"); + + } + } } From 40df45e539c8706741f02bb2eca7e9ede682d803 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 28 May 2019 21:22:41 +0200 Subject: [PATCH 103/342] Implement save(Entity). This commits adds * A Neo4j specific IsNewStrategy * The ability to save single entities. The save statement works for both entities with internal generated and external assigned ids. In both scenarios, an upsert is performed. With external ids, this is done by a MERGE statement, with internal ids by a union statement. The IsNewStrategy provides interoberability with Spring Data REST. --- .../data/neo4j/core/DefaultNeo4jClient.java | 4 + .../core/DefaultReactiveNeo4jClient.java | 4 + .../data/neo4j/core/cypher/Comparison.java | 10 +- .../data/neo4j/core/cypher/Cypher.java | 7 +- .../neo4j/core/cypher/StatementBuilder.java | 2 +- .../mapping/DefaultNeo4jBinderFunction.java | 70 +++++++ .../mapping/DefaultNeo4jIsNewStrategy.java | 127 ++++++++++++ .../mapping/DefaultNeo4jMappingFunction.java | 2 +- .../mapping/DefaultNeo4jPersistentEntity.java | 35 +++- .../DefaultNeo4jPersistentProperty.java | 2 +- .../core/mapping/Neo4jMappingContext.java | 12 ++ .../data/neo4j/core/schema/Id.java | 23 ++- .../data/neo4j/core/schema/IdDescription.java | 4 +- .../data/neo4j/core/schema/IdGenerator.java | 5 +- .../neo4j/core/schema/NodeDescription.java | 6 +- .../data/neo4j/core/schema/Schema.java | 9 + .../repository/query/CypherAdapterUtils.java | 65 ++++-- .../DefaultNeo4jEntityInformation.java | 40 +++- .../support/Neo4jEntityInformation.java | 8 +- .../support/Neo4jRepositoryFactory.java | 5 +- .../neo4j/repository/support/Predicate.java | 6 +- .../ReactiveNeo4jRepositoryFactory.java | 2 +- .../support/SimpleNeo4jRepository.java | 46 +++-- .../SimpleReactiveNeo4jRepository.java | 16 +- .../data/neo4j/core/cypher/CypherIT.java | 53 ++++- .../DefaultNeo4jIsNewStrategyTest.java | 192 ++++++++++++++++++ .../core/mapping/Neo4jMappingContextTest.java | 2 +- .../integration/imperative/RepositoryIT.java | 90 ++++++++ .../shared/PersonWithAllConstructor.java | 6 +- 29 files changed, 769 insertions(+), 84 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jBinderFunction.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index 61cfec1d5..a93e55fb8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -50,6 +50,7 @@ import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.repository.NoResultException; +import org.springframework.util.Assert; /** * Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to and interact with the database. @@ -220,6 +221,9 @@ class DefaultNeo4jClient implements Neo4jClient { @Override public RunnableSpecTightToDatabase with(Function> binder) { + + Assert.notNull(binder, "Binder is required."); + return bindAll(binder.apply(value)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index b38033df0..4afd95e46 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -48,6 +48,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; import org.springframework.data.neo4j.core.Neo4jClient.OngoingBindSpec; import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; +import org.springframework.util.Assert; /** * Reactive variant of the {@link Neo4jClient}. @@ -177,6 +178,9 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { @Override public ReactiveRunnableSpecTightToDatabase with(Function> binder) { + + Assert.notNull(binder, "Binder is required."); + return bindAll(binder.apply(value)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java index 9591ad683..b3931c10c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java @@ -18,9 +18,9 @@ */ package org.springframework.data.neo4j.core.cypher; +import static org.springframework.data.neo4j.core.cypher.Expressions.*; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -78,9 +78,13 @@ public final class Comparison implements Condition { public void accept(Visitor visitor) { visitor.enter(this); - Visitable.visitIfNotNull(left, visitor); + if (left != null) { + nameOrExpression(left).accept(visitor); + } comparator.accept(visitor); - Visitable.visitIfNotNull(right, visitor); + if (right != null) { + nameOrExpression(right).accept(visitor); + } visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index 94df05820..5e2bd5302 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -23,7 +23,6 @@ import java.util.List; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesSet; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndWith; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; @@ -91,7 +90,7 @@ public final class Cypher { * @return A new property */ public static Property property(String containerName, String name) { - return property(symbolicName(containerName), name); + return property(name(containerName), name); } /** @@ -111,7 +110,7 @@ public final class Cypher { * @param value The value of the symbolic name * @return A new symoblic name */ - public static SymbolicName symbolicName(String value) { + public static SymbolicName name(String value) { return new SymbolicName(value); } @@ -131,7 +130,7 @@ public final class Cypher { * @param pattern The patterns to match * @return An ongoing match that is used to specify an optional where and a required return clause */ - public static ExposesMatch optionalMatch(PatternElement... pattern) { + public static OngoingReadingWithoutWhere optionalMatch(PatternElement... pattern) { return Statement.builder().optionalMatch(pattern); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index 57a5c66d9..6fbb32cc6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -403,7 +403,7 @@ public interface StatementBuilder { } default OngoingUnwind unwind(String variable) { - return unwind(Cypher.symbolicName(variable)); + return unwind(Cypher.name(variable)); } OngoingUnwind unwind(Expression expression); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jBinderFunction.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jBinderFunction.java new file mode 100644 index 000000000..399e6d373 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jBinderFunction.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.mapping; + +import static org.springframework.data.neo4j.core.schema.NodeDescription.*; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.data.mapping.PersistentPropertyAccessor; + +/** + * @param + * @author Michael J. Simons + * @since 1.0 + */ +@RequiredArgsConstructor +@Slf4j +final class DefaultNeo4jBinderFunction implements Function> { + + private final Neo4jPersistentEntity nodeDescription; + + @Override + public Map apply(T entity) { + Map properties = new HashMap<>(); + + PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(entity); + nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> { + + // Skip the internal properties, we don't want them to end up stored as properties + if (p.isInternalIdProperty()) { + return; + } + + final Object value = propertyAccessor.getProperty(p); + // Don't add null values, shave off some bytes for transport + if (value == null) { + return; + } + + properties.put(p.getPropertyName(), value); + }); + + Map parameters = new HashMap<>(); + parameters.put(NAME_OF_PROPERTIES_PARAM, properties); + parameters.put(NAME_OF_ID_PARAM, propertyAccessor.getProperty(nodeDescription.getRequiredIdProperty())); + + return parameters; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java new file mode 100644 index 000000000..5ac9fd556 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.mapping; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Function; + +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.support.IsNewStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of a {@link IsNewStrategy} that follows our supported identifiers and generators. + * Entities will be treated as new: + *
    + *
  • when using internally generated (database) ids and the id property is {@literal null} or of a numeric primitive less than or equal {@literal 0},
  • + *
  • when using externally generated values and the id is {@literal null},
  • + *
  • when using assigned values without a version property or with a version property that is {@literal null}.
  • + *
+ *

+ * An entity will not be treated as new + *

    + *
  • when using internally generated (database) ids and the id property has a non-null value greater than {@literal 0},
  • + *
  • when using externally generated values and the id property is not {@literal null},
  • + *
  • when using assigned values together with {@link org.springframework.data.annotation.Version @Version} which has already a value not equal to {@literal null} or {@literal 0}.
  • + *
+ * + * @author Michael J. Simons + * @since 1.0 + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j +class DefaultNeo4jIsNewStrategy implements IsNewStrategy { + + static IsNewStrategy basedOn(Neo4jPersistentEntity entityMetaData) { + + Assert.notNull(entityMetaData, "Entity meta data must not be null."); + + Id.Strategy idStrategy = entityMetaData.getIdDescription().getIdStrategy(); + Class valueType = entityMetaData.getRequiredIdProperty().getType(); + + if (idStrategy == Id.Strategy.EXTERNALLY_GENERATED && valueType.isPrimitive()) { + throw new IllegalArgumentException(String.format("Cannot use %s with externally generated, primitive ids.", + DefaultNeo4jIsNewStrategy.class.getName())); + } + + Function valueLookup; + Neo4jPersistentProperty versionProperty = entityMetaData.getVersionProperty(); + if (idStrategy == Id.Strategy.ASSIGNED) { + if (versionProperty == null) { + log.warn("Instances of " + entityMetaData.getType() + + " with an assigned id will always be treated as new without version property!"); + valueType = Void.class; + valueLookup = source -> null; + } else { + valueType = versionProperty.getType(); + valueLookup = source -> entityMetaData.getPropertyAccessor(source).getProperty(versionProperty); + } + } else { + valueLookup = source -> entityMetaData.getIdentifierAccessor(source).getIdentifier(); + } + + return new DefaultNeo4jIsNewStrategy(idStrategy, valueType, valueLookup); + } + + private final Id.Strategy strategy; + + private final Class valueType; + + private @Nullable final Function valueLookup; + + /* + * (non-Javadoc) + * @see IsNewStrategy#isNew(Object) + */ + @Override + public boolean isNew(Object entity) { + + Object value = valueLookup.apply(entity); + if (strategy.isInternal()) { + + boolean isNew = false; + if (value != null && valueType.isPrimitive() && Number.class.isInstance(value)) { + isNew = ((Number) value).longValue() <= 0; + } else { + isNew = value == null; + } + + return isNew; + } else if (strategy == Id.Strategy.EXTERNALLY_GENERATED) { + return value == null; + } else if (strategy == Id.Strategy.ASSIGNED) { + if (valueType != null && !valueType.isPrimitive()) { + return value == null; + } + + if (Number.class.isInstance(value)) { + return ((Number) value).longValue() == 0; + } + } + + throw new IllegalArgumentException( + String + .format("Could not determine whether %s is new! Unsupported identifier or version property!", entity)); + + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java index 22ce6774c..75deaef12 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java @@ -51,7 +51,7 @@ import org.springframework.data.neo4j.core.schema.NodeDescription; */ @RequiredArgsConstructor @Slf4j -class DefaultNeo4jMappingFunction implements BiFunction { +final class DefaultNeo4jMappingFunction implements BiFunction { private final EntityInstantiators instantiators; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index c9ac9503e..b43baffdb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -31,6 +31,7 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Property; +import org.springframework.data.support.IsNewStrategy; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -60,31 +61,60 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity computeGraphProperties()); } + /* + * (non-Javadoc) + * @see NodeDescription#getPrimaryLabel() + */ @Override public String getPrimaryLabel() { return primaryLabel; } + /* + * (non-Javadoc) + * @see NodeDescription#getUnderlyingClass() + */ @Override public Class getUnderlyingClass() { return getType(); } + /* + * (non-Javadoc) + * @see NodeDescription#getIdDescription() + */ @Override public IdDescription getIdDescription() { return this.idDescription.get(); } + /* + * (non-Javadoc) + * @see NodeDescription#getGraphProperties() + */ @Override public Collection getGraphProperties() { return this.graphProperties.get(); } + /* + * (non-Javadoc) + * @see NodeDescription#getGraphProperty(String) + */ @Override public Optional getGraphProperty(String fieldName) { return Optional.ofNullable(this.getPersistentProperty(fieldName)); } + /* + * (non-Javadoc) + * @see BasicPersistentEntity#getFallbackIsNewStrategy() + */ + @Override + protected IsNewStrategy getFallbackIsNewStrategy() { + return DefaultNeo4jIsNewStrategy.basedOn(this); + } + @Override public void verify() { @@ -110,7 +140,7 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity { - if (idAnnotation.strategy() == Id.Strategy.INTERNAL + if (idAnnotation.strategy() == Id.Strategy.INTERNALLY_GENERATED && idProperty.findAnnotation(Property.class) != null) { throw new IllegalArgumentException( "Cannot use internal id strategy with custom property " + idProperty.getPropertyName() @@ -118,7 +148,7 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity new IdDescription()); } @@ -132,4 +162,5 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity new DefaultNeo4jMappingFunction<>(instantiators, neo4jPersistentEntity)); } + @Override + public Optional>> getBinderFunctionFor(Class sourceClass) { + if (!this.hasPersistentEntityFor(sourceClass)) { + return Optional.empty(); + } + + return this.getNodeDescription(sourceClass) + .map(Neo4jPersistentEntity.class::cast) + .map(neo4jPersistentEntity -> new DefaultNeo4jBinderFunction<>(neo4jPersistentEntity)); + } + private Collection computeRelationshipsOf(String primaryLabel) { NodeDescription nodeDescription = getRequiredNodeDescription(primaryLabel); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java index 0f31626ef..54cc66155 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java @@ -24,6 +24,7 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.EnumSet; import org.apiguardian.api.API; @@ -49,7 +50,7 @@ public @interface Id { /** * The default, use Neo4js internal ids. */ - INTERNAL, + INTERNALLY_GENERATED, /** * Use assigned values. @@ -57,9 +58,23 @@ public @interface Id { ASSIGNED, /** - * Use generated values, a generator class is required. + * Use externally generated values, a generator class is required. */ - GENERATED + EXTERNALLY_GENERATED; + + /** + * @return True, if the database generated the ID. + */ + public boolean isInternal() { + return this == INTERNALLY_GENERATED; + } + + /** + * @return True, if the ID is assigned to the entity before the entity hits the database + */ + public boolean isExternal() { + return EnumSet.of(ASSIGNED, EXTERNALLY_GENERATED).contains(this); + } } /** @@ -67,7 +82,7 @@ public @interface Id { * * @return The strategy applied for generating ids for new entities. Defaults to use Neo4j internal database identifiers. */ - Strategy strategy() default Strategy.INTERNAL; + Strategy strategy() default Strategy.INTERNALLY_GENERATED; /** * diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java index dac803d6b..8079dfa9c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java @@ -48,7 +48,7 @@ public final class IdDescription { private @Nullable final String graphPropertyName; public IdDescription() { - this(Id.Strategy.INTERNAL, null, null); + this(Id.Strategy.INTERNALLY_GENERATED, null, null); } public IdDescription(Id.Strategy idStrategy, @@ -68,7 +68,7 @@ public final class IdDescription { /** * An ID description has only a corresponding graph property name when it's based on either {@link Id.Strategy#ASSIGNED} - * or {@link Id.Strategy#GENERATED}. An internal id has no corresponding graph property and therefor this method + * or {@link Id.Strategy#EXTERNALLY_GENERATED}. An internal id has no corresponding graph property and therefor this method * will return an empty {@link Optional} in such cases. * * @return The name of an optional graph property. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java index bb34dfc6f..97602bc43 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java @@ -23,12 +23,13 @@ import org.apiguardian.api.API; /** * Interface for generating ids for entities. * + * @param Type of the id to generate * @author Michael J. Simons * @since 1.0 */ @FunctionalInterface @API(status = API.Status.STABLE, since = "1.0") -public interface IdGenerator { +public interface IdGenerator { /** * Generates a new id for given entity. @@ -36,5 +37,5 @@ public interface IdGenerator { * @param entity the entity to be saved * @return id to be assigned to the entity */ - Object generateId(Object entity); + T generateId(Object entity); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java index 710e7993c..212ecc9e0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -36,6 +36,8 @@ public interface NodeDescription { String NAME_OF_ROOT_NODE = "n"; String NAME_OF_INTERNAL_ID = "__internalNeo4jId__"; + String NAME_OF_ID_PARAM = "__id__"; + String NAME_OF_PROPERTIES_PARAM = "__properties__"; /** * @return The primary label of this entity inside Neo4j. @@ -68,7 +70,7 @@ public interface NodeDescription { /** * @return True if entities for this node use Neo4j internal ids. */ - default boolean useInternalIds() { - return this.getIdDescription().getIdStrategy() == Id.Strategy.INTERNAL; + default boolean isUsingInternalIds() { + return this.getIdDescription().getIdStrategy() == Id.Strategy.INTERNALLY_GENERATED; } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java index 0e2875601..0c85d8024 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -19,9 +19,11 @@ package org.springframework.data.neo4j.core.schema; import java.util.Collection; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Function; import org.apiguardian.api.API; import org.neo4j.driver.Record; @@ -109,4 +111,11 @@ public interface Schema { return getMappingFunctionFor(targetClass).orElseThrow(() -> new UnknownEntityException(targetClass)); } + + Optional>> getBinderFunctionFor(Class sourceClass); + + default Function> getRequiredBinderFunctionFor(Class sourceClass) { + + return getBinderFunctionFor(sourceClass).orElseThrow(() -> new UnknownEntityException(sourceClass)); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java index 6c25eabdf..1b3e2adde 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java @@ -29,17 +29,10 @@ import java.util.function.Function; import org.apiguardian.api.API; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.core.cypher.Condition; -import org.springframework.data.neo4j.core.cypher.Conditions; -import org.springframework.data.neo4j.core.cypher.Cypher; -import org.springframework.data.neo4j.core.cypher.Expression; -import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.Node; -import org.springframework.data.neo4j.core.cypher.SortItem; -import org.springframework.data.neo4j.core.cypher.StatementBuilder; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.neo4j.core.cypher.*; import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableStatement; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndWith; -import org.springframework.data.neo4j.core.cypher.SymbolicName; import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.IdDescription; @@ -66,7 +59,7 @@ public final class CypherAdapterUtils { * @return A stream if sort items. Will be empty when sort is unsorted. */ public static Function sortAdapterFor(NodeDescription nodeDescription) { - SymbolicName rootNode = Cypher.symbolicName(NodeDescription.NAME_OF_ROOT_NODE); + SymbolicName rootNode = Cypher.name(NodeDescription.NAME_OF_ROOT_NODE); return order -> { String property = nodeDescription.getGraphProperty(order.getProperty()) @@ -139,23 +132,59 @@ public final class CypherAdapterUtils { * @return An ongoing match */ public OngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition) { - Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); + Node rootNode = node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); IdDescription idDescription = nodeDescription.getIdDescription(); List expressions = new ArrayList<>(); expressions.add(rootNode); - if (idDescription.getIdStrategy() == Id.Strategy.INTERNAL) { + if (idDescription.getIdStrategy() == Id.Strategy.INTERNALLY_GENERATED) { expressions.add(Functions.id(rootNode).as(NAME_OF_INTERNAL_ID)); } return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())) .with(expressions.toArray(new Expression[expressions.size()])); } - public BuildableStatement prepareDeleteOf(NodeDescription nodeDescription, + public Statement prepareDeleteOf(NodeDescription nodeDescription, Optional condition) { - Node rootNode = Cypher.node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); - return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())).delete(rootNode); + Node rootNode = node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); + return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())).delete(rootNode).build(); + } + + public Statement prepareSaveOf(NodeDescription nodeDescription) { + + Node rootNode = node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); + IdDescription idDescription = nodeDescription.getIdDescription(); + Parameter idParameter = parameter(NAME_OF_ID_PARAM); + if (idDescription.getIdStrategy().isExternal()) { + String nameOfIdProperty = idDescription.getOptionalGraphPropertyName() + .orElseThrow(() -> new MappingException("External id does not correspond to a graph property!")); + + return Cypher.merge(rootNode.properties(nameOfIdProperty, idParameter)) + .set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)) + .returning(rootNode.internalId()) + .build(); + } else { + Node possibleExistingNode = node(nodeDescription.getPrimaryLabel()).named("hlp"); + + Statement createIfNew = Cypher + .optionalMatch(possibleExistingNode) + .where(possibleExistingNode.internalId().isEqualTo(idParameter)) + .with(possibleExistingNode).where(possibleExistingNode.isNull()) + .create(rootNode) + .set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)) + .returning(rootNode.internalId()) + .build(); + + Statement updateIfExists = Cypher + .match(rootNode) + .where(rootNode.internalId().isEqualTo(idParameter)) + .set(rootNode, parameter(NAME_OF_PROPERTIES_PARAM)) + .returning(rootNode.internalId()) + .build(); + + return Cypher.union(createIfNew, updateIfExists); + } } } @@ -180,15 +209,15 @@ public final class CypherAdapterUtils { */ public static Expression createIdExpression(final NodeDescription nodeDescription) { - final SymbolicName rootNode = Cypher.symbolicName(NAME_OF_ROOT_NODE); + final SymbolicName rootNode = Cypher.name(NAME_OF_ROOT_NODE); final IdDescription idDescription = nodeDescription.getIdDescription(); Expression idExpression; switch (idDescription.getIdStrategy()) { - case INTERNAL: + case INTERNALLY_GENERATED: idExpression = Functions.id(rootNode); break; case ASSIGNED: - case GENERATED: + case EXTERNALLY_GENERATED: idExpression = idDescription.getOptionalGraphPropertyName() .map(propertyName -> property(rootNode.getName(), propertyName)).get(); break; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java index 77a2ddc6f..ed7734146 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java @@ -18,13 +18,14 @@ */ package org.springframework.data.neo4j.repository.support; +import java.util.Map; import java.util.function.BiFunction; +import java.util.function.Function; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.cypher.Expression; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.repository.query.CypherAdapterUtils; import org.springframework.data.repository.core.support.PersistentEntityInformation; @@ -38,34 +39,59 @@ import org.springframework.data.repository.core.support.PersistentEntityInformat final class DefaultNeo4jEntityInformation extends PersistentEntityInformation implements Neo4jEntityInformation { - private final NodeDescription nodeDescription; + private final Neo4jPersistentEntity entityMetaData; private final BiFunction mappingFunction; + private final Function> binderFunction; + private final Expression idExpression; DefaultNeo4jEntityInformation( Neo4jPersistentEntity entityMetaData, - BiFunction mappingFunction + BiFunction mappingFunction, + Function> binderFunction ) { super(entityMetaData); - this.nodeDescription = entityMetaData; + this.entityMetaData = entityMetaData; this.mappingFunction = mappingFunction; + this.binderFunction = binderFunction; - this.idExpression = CypherAdapterUtils.createIdExpression(nodeDescription); + this.idExpression = CypherAdapterUtils.createIdExpression(entityMetaData); } + /* + * (non-Javadoc) + * @see Neo4jEntityInformation#getIdExpression() + */ @Override public Expression getIdExpression() { return this.idExpression; } + /* + * (non-Javadoc) + * @see Neo4jEntityInformation#getEntityMetaData() + */ @Override - public NodeDescription getNodeDescription() { - return this.nodeDescription; + public Neo4jPersistentEntity getEntityMetaData() { + return this.entityMetaData; } + /* + * (non-Javadoc) + * @see Neo4jEntityInformation#getMappingFunction() + */ @Override public BiFunction getMappingFunction() { return mappingFunction; } + + /* + * (non-Javadoc) + * @see Neo4jEntityInformation#getBinderFunction() + */ + @Override + public Function> getBinderFunction() { + return binderFunction; + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java index b48f4548d..5774af456 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java @@ -18,12 +18,14 @@ */ package org.springframework.data.neo4j.repository.support; +import java.util.Map; import java.util.function.BiFunction; +import java.util.function.Function; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.cypher.Expression; -import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.core.EntityInformation; /** @@ -48,11 +50,13 @@ public interface Neo4jEntityInformation extends EntityInformation /** * @return The full schema based description for the underlying entity. */ - NodeDescription getNodeDescription(); + Neo4jPersistentEntity getEntityMetaData(); /** * @return A mapping function that creates instances of {@code T} for the given record. * @see org.springframework.data.neo4j.core.schema.Schema#getMappingFunctionFor(Class) */ BiFunction getMappingFunction(); + + Function> getBinderFunction(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index bb3562c2e..107488c79 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -20,8 +20,10 @@ package org.springframework.data.neo4j.repository.support; import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; +import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; +import java.util.function.Function; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; @@ -67,8 +69,9 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); BiFunction mappingFunction = mappingContext.getRequiredMappingFunctionFor(domainClass); + Function> binderFUnction = mappingContext.getRequiredBinderFunctionFor(domainClass); - return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction); + return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction, binderFUnction); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java index 6c516f54f..84464570a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java @@ -64,7 +64,7 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); - SymbolicName rootNode = Cypher.symbolicName(NAME_OF_ROOT_NODE); + SymbolicName rootNode = Cypher.name(NAME_OF_ROOT_NODE); Collection graphProperties = probeNodeDescription.getGraphProperties(); DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); ExampleMatcher matcher = example.getMatcher(); @@ -81,7 +81,7 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; continue; } - boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.useInternalIds(); + boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.isUsingInternalIds(); String propertyName = graphProperty.getPropertyName(); ExampleMatcher.PropertyValueTransformer transformer = matcherAccessor @@ -98,7 +98,7 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; if (graphProperty.isRelationship()) { log.error("Querying by example does not support traversing of relationships."); - } else if (graphProperty.isIdProperty() && probeNodeDescription.useInternalIds()) { + } else if (graphProperty.isIdProperty() && probeNodeDescription.isUsingInternalIds()) { predicate .add(mode, createIdExpression(predicate.nodeDescription).isEqualTo(literalOf(optionalValue.get()))); } else { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java index 4141a77b5..5fa1734ae 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -67,7 +67,7 @@ final class ReactiveNeo4jRepositoryFactory extends RepositoryFactorySupport { Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); BiFunction mappingFunction = mappingContext.getRequiredMappingFunctionFor(domainClass); - return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction); + return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction, null); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 986eca28c..81f3e3716 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -36,6 +36,7 @@ import org.neo4j.driver.summary.ResultSummary; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.Neo4jClient.ExecutableQuery; import org.springframework.data.neo4j.core.PreparedQuery; @@ -45,7 +46,7 @@ import org.springframework.data.neo4j.core.cypher.Statement; import org.springframework.data.neo4j.core.cypher.StatementBuilder; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndReturn; import org.springframework.data.neo4j.core.cypher.renderer.Renderer; -import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; @@ -69,7 +70,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository private final Neo4jEntityInformation entityInformation; - private final NodeDescription nodeDescription; + private final Neo4jPersistentEntity entityMetaData; private final SchemaBasedStatementBuilder statementBuilder; @@ -77,16 +78,16 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository SchemaBasedStatementBuilder statementBuilder) { this.neo4jClient = neo4jClient; this.entityInformation = entityInformation; - this.nodeDescription = this.entityInformation.getNodeDescription(); + this.entityMetaData = this.entityInformation.getEntityMetaData(); this.statementBuilder = statementBuilder; } @Override public Iterable findAll(Sort sort) { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData) .returning(asterisk()) - .orderBy(toSortItems(nodeDescription, sort)) + .orderBy(toSortItems(entityMetaData, sort)) .build(); return createExecutableQuery(statement).getResults(); @@ -95,10 +96,10 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Override public Page findAll(Pageable pageable) { - OngoingReadingAndReturn returning = statementBuilder.prepareMatchOf(nodeDescription) + OngoingReadingAndReturn returning = statementBuilder.prepareMatchOf(entityMetaData) .returning(asterisk()); - StatementBuilder.BuildableStatement returningWithPaging = addPagingParameter(nodeDescription, pageable, + StatementBuilder.BuildableStatement returningWithPaging = addPagingParameter(entityMetaData, pageable, returning); Statement statement = returningWithPaging.build(); @@ -111,7 +112,20 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Override @Transactional public S save(S entity) { - throw new UnsupportedOperationException("Not there yet."); + + Long internalId = neo4jClient + .newQuery(() -> renderer.render(statementBuilder.prepareSaveOf(entityMetaData))) + .bind((T) entity) + .with(entityInformation.getBinderFunction()) + .fetchAs(Long.class).one().get(); + + if (!entityMetaData.isUsingInternalIds()) { + return entity; + } else { + PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entity); + propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId); + return (S) propertyAccessor.getBean(); + } } @Override @@ -124,7 +138,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository public Optional findById(ID id) { Statement statement = statementBuilder - .prepareMatchOf(nodeDescription, + .prepareMatchOf(entityMetaData, Optional.of(entityInformation.getIdExpression().isEqualTo(literalOf(id)))) .returning(asterisk()) .build(); @@ -139,7 +153,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Override public Iterable findAll() { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData) .returning(asterisk()).build(); return createExecutableQuery(statement).getResults(); } @@ -148,7 +162,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository public Iterable findAllById(Iterable ids) { Statement statement = statementBuilder - .prepareMatchOf(nodeDescription, Optional.of(entityInformation.getIdExpression().in((parameter("ids"))))) + .prepareMatchOf(entityMetaData, Optional.of(entityInformation.getIdExpression().in((parameter("ids"))))) .returning(asterisk()) .build(); @@ -158,7 +172,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Override public long count() { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData) .returning(Functions.count(asterisk())).build(); PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) @@ -178,7 +192,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository log.debug("Deleting entity with id {} ", id); Statement statement = statementBuilder - .prepareDeleteOf(nodeDescription, Optional.of(condition)).build(); + .prepareDeleteOf(entityMetaData, Optional.of(condition)); ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)) .bind(id).to(nameOfParameter) .run(); @@ -206,7 +220,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository log.debug("Deleting all entities with the following ids: {} ", ids); - Statement statement = statementBuilder.prepareDeleteOf(nodeDescription, Optional.of(condition)).build(); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)) .bind(ids).to(nameOfParameter) .run(); @@ -218,9 +232,9 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Transactional public void deleteAll() { - log.debug("Deleting all nodes with primary label {}", nodeDescription.getPrimaryLabel()); + log.debug("Deleting all nodes with primary label {}", entityMetaData.getPrimaryLabel()); - Statement statement = statementBuilder.prepareDeleteOf(nodeDescription, Optional.empty()).build(); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.empty()); ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)).run(); log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java index f34be8fb5..81f96a38c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java @@ -61,7 +61,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< private final Neo4jEntityInformation entityInformation; - private final NodeDescription nodeDescription; + private final NodeDescription entityMetaData; private final SchemaBasedStatementBuilder statementBuilder; @@ -69,14 +69,14 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< SchemaBasedStatementBuilder statementBuilder) { this.neo4jClient = neo4jClient; this.entityInformation = entityInformation; - this.nodeDescription = this.entityInformation.getNodeDescription(); + this.entityMetaData = this.entityInformation.getEntityMetaData(); this.statementBuilder = statementBuilder; } @Override public Mono findById(ID id) { Statement statement = statementBuilder - .prepareMatchOf(nodeDescription, Optional.of(entityInformation.getIdExpression().isEqualTo(literalOf(id)))) + .prepareMatchOf(entityMetaData, Optional.of(entityInformation.getIdExpression().isEqualTo(literalOf(id)))) .returning(asterisk()) .build(); return createExecutableQuery(statement).getSingleResult(); @@ -89,9 +89,9 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Flux findAll(Sort sort) { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData, Optional.empty()) .returning(asterisk()) - .orderBy(toSortItems(nodeDescription, sort)) + .orderBy(toSortItems(entityMetaData, sort)) .build(); return createExecutableQuery(statement).getResults(); @@ -109,7 +109,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Flux findAll() { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData, Optional.empty()) .returning(asterisk()).build(); return createExecutableQuery(statement).getResults(); } @@ -117,7 +117,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Flux findAllById(Iterable ids) { Statement statement = statementBuilder - .prepareMatchOf(nodeDescription, Optional.of(entityInformation.getIdExpression().in((parameter("ids"))))) + .prepareMatchOf(entityMetaData, Optional.of(entityInformation.getIdExpression().in((parameter("ids"))))) .returning(asterisk()) .build(); @@ -131,7 +131,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Mono count() { - Statement statement = statementBuilder.prepareMatchOf(nodeDescription, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData, Optional.empty()) .returning(Functions.count(asterisk())).build(); PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java index e43f26add..53cb02336 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java @@ -913,6 +913,44 @@ class CypherIT { "CREATE (u:`User`) SET u.p = 'Hallo, Welt'"); } + @Test + void shouldRenderSetAfterMerge() { + Statement statement; + statement = Cypher.merge(userNode) + .set(userNode.property("p").to(literalOf("Hallo, Welt"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`) SET u.p = 'Hallo, Welt'"); + } + + @Test + void shouldRenderSetAfterCreateAndWith() { + Statement statement; + statement = Cypher.create(userNode) + .with(userNode) + .set(userNode.property("p").to(literalOf("Hallo, Welt"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "CREATE (u:`User`) WITH u SET u.p = 'Hallo, Welt'"); + } + + @Test + void shouldRenderSetAfterMergeAndWith() { + Statement statement; + statement = Cypher.merge(userNode) + .with(userNode) + .set(userNode.property("p").to(literalOf("Hallo, Welt"))) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MERGE (u:`User`) WITH u SET u.p = 'Hallo, Welt'"); + } + @Test void shouldRenderSet() { @@ -1360,6 +1398,15 @@ class CypherIT { .isEqualTo( "MATCH (u:`User`) DETACH DELETE u"); + statement = Cypher.match(userNode) + .with(userNode) + .detachDelete(userNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) WITH u DETACH DELETE u"); + statement = Cypher.match(userNode) .where(userNode.property("a").isNotNull()).and(userNode.property("b").isNull()) .delete(userNode) @@ -1598,7 +1645,7 @@ class CypherIT { Statement statement; statement = Cypher.unwind(Cypher.literalOf(1), Cypher.literalTrue(), Cypher.literalFalse()) - .as("n").returning(symbolicName("n")) + .as("n").returning(name("n")) .build(); assertThat(cypherRenderer.render(statement)) @@ -1612,7 +1659,7 @@ class CypherIT { Statement statement; statement = Cypher.unwind(Cypher.literalOf(1), Cypher.literalTrue(), Cypher.literalFalse()) .as("n") - .merge(bikeNode.properties("b", symbolicName("n"))) + .merge(bikeNode.properties("b", name("n"))) .returning(bikeNode) .build(); @@ -1631,7 +1678,7 @@ class CypherIT { .with(collected) .unwind(collected).as("x") .with("x") - .delete(symbolicName("x")) + .delete(name("x")) .returning("x") .build(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java new file mode 100644 index 000000000..f96dddc91 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.mapping; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdDescription; +import org.springframework.data.support.IsNewStrategy; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class DefaultNeo4jIsNewStrategyTest { + + @Mock + Neo4jPersistentEntity entityMetaData; + + @Mock + Neo4jPersistentProperty idProperty; + + @Mock + Neo4jPersistentProperty versionProperty; + + @Nested + class InternallyGenerated { + @Test + void shouldDealWithNonPrimitives() { + Object a = new Object(); + Object b = new Object(); + + IdDescription idDescription = new IdDescription(); + doReturn(Long.class).when(idProperty).getType(); + doReturn(idDescription).when(entityMetaData).getIdDescription(); + doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); + doReturn((IdentifierAccessor) () -> null).when(entityMetaData).getIdentifierAccessor(a); + doReturn((IdentifierAccessor) () -> Long.valueOf(1)).when(entityMetaData).getIdentifierAccessor(b); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + } + + @Test + void shouldDealWithPrimitives() { + Object a = new Object(); + Object b = new Object(); + Object c = new Object(); + + IdDescription idDescription = new IdDescription(); + doReturn(long.class).when(idProperty).getType(); + doReturn(idDescription).when(entityMetaData).getIdDescription(); + doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); + doReturn((IdentifierAccessor) () -> -1L).when(entityMetaData).getIdentifierAccessor(a); + doReturn((IdentifierAccessor) () -> 0L).when(entityMetaData).getIdentifierAccessor(b); + doReturn((IdentifierAccessor) () -> 1L).when(entityMetaData).getIdentifierAccessor(c); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isTrue(); + assertThat(strategy.isNew(c)).isFalse(); + } + } + + @Nested + class ExternallyGenerated { + @Test + void shouldDealWithNonPrimitives() { + + Object a = new Object(); + Object b = new Object(); + IdDescription idDescription = new IdDescription(Id.Strategy.EXTERNALLY_GENERATED, null, null); + doReturn(String.class).when(idProperty).getType(); + doReturn(idDescription).when(entityMetaData).getIdDescription(); + doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); + doReturn((IdentifierAccessor) () -> null).when(entityMetaData).getIdentifierAccessor(a); + doReturn((IdentifierAccessor) () -> "4711").when(entityMetaData).getIdentifierAccessor(b); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + } + + @Test + void doesntNeedToDealWithPrimitives() { + + IdDescription idDescription = new IdDescription(Id.Strategy.EXTERNALLY_GENERATED, null, null); + doReturn(long.class).when(idProperty).getType(); + doReturn(idDescription).when(entityMetaData).getIdDescription(); + doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); + + assertThatIllegalArgumentException().isThrownBy(() -> DefaultNeo4jIsNewStrategy.basedOn(entityMetaData)) + .withMessage( + "Cannot use org.springframework.data.neo4j.core.mapping.DefaultNeo4jIsNewStrategy with externally generated, primitive ids."); + } + } + + @Nested + class Assigned { + + @Test + void shouldAlwaysTreatEntitiesAsNewWithoutVersion() { + Object a = new Object(); + IdDescription idDescription = new IdDescription(Id.Strategy.ASSIGNED, null, null); + doReturn(String.class).when(idProperty).getType(); + doReturn(idDescription).when(entityMetaData).getIdDescription(); + doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); + assertThat(strategy.isNew(a)).isTrue(); + } + + @Test + void shouldDealWithVersion() { + Object a = new Object(); + Object b = new Object(); + IdDescription idDescription = new IdDescription(Id.Strategy.ASSIGNED, null, null); + + doReturn(String.class).when(idProperty).getType(); + doReturn(String.class).when(versionProperty).getType(); + + doReturn(idDescription).when(entityMetaData).getIdDescription(); + doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); + doReturn(versionProperty).when(entityMetaData).getVersionProperty(); + + PersistentPropertyAccessor aa = mock(PersistentPropertyAccessor.class); + doReturn(null).when(aa).getProperty(versionProperty); + doReturn(aa).when(entityMetaData).getPropertyAccessor(a); + + PersistentPropertyAccessor ab = mock(PersistentPropertyAccessor.class); + doReturn("A version").when(ab).getProperty(versionProperty); + doReturn(ab).when(entityMetaData).getPropertyAccessor(b); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); + + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + } + + @Test + void shouldDealWithPrimitiveVersion() { + Object a = new Object(); + Object b = new Object(); + IdDescription idDescription = new IdDescription(Id.Strategy.ASSIGNED, null, null); + + doReturn(String.class).when(idProperty).getType(); + doReturn(int.class).when(versionProperty).getType(); + + doReturn(idDescription).when(entityMetaData).getIdDescription(); + doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); + doReturn(versionProperty).when(entityMetaData).getVersionProperty(); + + PersistentPropertyAccessor aa = mock(PersistentPropertyAccessor.class); + doReturn(0).when(aa).getProperty(versionProperty); + doReturn(aa).when(entityMetaData).getPropertyAccessor(a); + + PersistentPropertyAccessor ab = mock(PersistentPropertyAccessor.class); + doReturn(1).when(ab).getProperty(versionProperty); + doReturn(ab).when(entityMetaData).getPropertyAccessor(b); + + IsNewStrategy strategy = DefaultNeo4jIsNewStrategy.basedOn(entityMetaData); + + assertThat(strategy.isNew(a)).isTrue(); + assertThat(strategy.isNew(b)).isFalse(); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index 34f06a39d..0425fe721 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -56,7 +56,7 @@ public class Neo4jMappingContextTest { assertThat(description.getUnderlyingClass()).isEqualTo(UserNode.class); assertThat(description.getIdDescription().getIdStrategy()) - .isEqualTo(Id.Strategy.INTERNAL); + .isEqualTo(Id.Strategy.INTERNALLY_GENERATED); assertThat(description.getGraphProperties()) .extracting(GraphPropertyDescription::getFieldName) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index 2d4e92637..b16740b7b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -34,8 +34,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; +import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Point; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -163,6 +166,46 @@ class RepositoryIT { assertThat(exists).isTrue(); } + @Test + void saveSingleEntity() { + + PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, + LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null); + PersonWithAllConstructor savedPerson = repository.save(person); + try (Session session = driver.session()) { + Record record = session.run("MATCH (n:PersonWithAllConstructor) WHERE n.first_name = $first_name RETURN n", + Values.parameters("first_name", "Freddie")).single(); + + assertThat(record.containsKey("n")).isTrue(); + Node node = record.get("n").asNode(); + assertThat(savedPerson.getId()).isEqualTo(node.id()); + } + } + + @Test + void updateSingleEntity() { + + PersonWithAllConstructor originalPerson = repository.findById(id1).get(); + originalPerson.setFirstName("Updated first name"); + originalPerson.setNullable("Updated nullable field"); + + PersonWithAllConstructor savedPerson = repository.save(originalPerson); + try (Session session = driver.session()) { + session.readTransaction(tx -> { + Record record = tx.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", + Values.parameters("id", originalPerson.getId())).single(); + assertThat(record.containsKey("n")).isTrue(); + Node node = record.get("n").asNode(); + assertThat(savedPerson.getId()).isEqualTo(node.id()); + + assertThat(savedPerson.getId()).isEqualTo(originalPerson.getId()); + assertThat(savedPerson.getFirstName()).isEqualTo(originalPerson.getFirstName()); + assertThat(savedPerson.getNullable()).isEqualTo(originalPerson.getNullable()); + return null; + }); + } + } + @Test void delete() { @@ -211,6 +254,53 @@ class RepositoryIT { assertThat(optionalThing).map(ThingWithAssignedId::getName).contains("Homer"); } + @Test + void saveWithAssignedId() { + + assertThat(thingRepository.count()).isEqualTo(21); + + ThingWithAssignedId thing = new ThingWithAssignedId("aaBB"); + thing.setName("That's the thing."); + thing = thingRepository.save(thing); + + try (Session session = driver.session()) { + Record record = session + .run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", Values.parameters("id", thing.getTheId())) + .single(); + + assertThat(record.containsKey("n")).isTrue(); + Node node = record.get("n").asNode(); + assertThat(node.get("theId").asString()).isEqualTo(thing.getTheId()); + assertThat(node.get("name").asString()).isEqualTo(thing.getName()); + + assertThat(thingRepository.count()).isEqualTo(22); + } + } + + @Test + void updateWithAssignedId() { + + assertThat(thingRepository.count()).isEqualTo(21); + + ThingWithAssignedId thing = new ThingWithAssignedId("id07"); + thing.setName("An updated thing"); + thingRepository.save(thing); + + thing = thingRepository.findById("id15").get(); + thing.setName("Another updated thing"); + thingRepository.save(thing); + + try (Session session = driver.session()) { + List records = session + .run("MATCH (n:Thing) WHERE n.theId in $ids RETURN n ORDER BY n.name ASC", + Values.parameters("ids", Arrays.asList("id07", "id15"))) + .list(r -> r.get("n").get("name").asString()); + + assertThat(records).hasSize(2).containsExactly("An updated thing", "Another updated thing"); + assertThat(thingRepository.count()).isEqualTo(21); + } + } + @Test void count() { assertThat(repository.count()).isEqualTo(2); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java index 1390cf1b2..16faf095e 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java @@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import lombok.experimental.Wither; import java.time.LocalDate; import java.util.List; @@ -45,12 +46,13 @@ import org.springframework.data.neo4j.core.schema.Property; public class PersonWithAllConstructor { @Id + @Wither private final Long id; private final String name; @Property("first_name") - private final String firstName; + private String firstName; private final String sameValue; @@ -60,7 +62,7 @@ public class PersonWithAllConstructor { private final LocalDate bornOn; - private final String nullable; + private String nullable; private final List things; From 05f2b3a7a716e1b90b80ca7a2d413de8e3c07570 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 3 Jun 2019 10:34:03 +0200 Subject: [PATCH 104/342] Use Assert.isTrue instead of Assert.state. Previously, state has been used for arguments, which is not appropriate. --- .../data/neo4j/core/cypher/Comparison.java | 2 +- .../neo4j/core/cypher/CompoundCondition.java | 2 +- .../data/neo4j/core/cypher/Cypher.java | 9 ++++----- .../data/neo4j/core/cypher/MapExpression.java | 4 ++-- .../data/neo4j/core/cypher/Operation.java | 4 ++-- .../data/neo4j/core/cypher/Property.java | 2 +- .../data/neo4j/core/cypher/UnionQuery.java | 3 +-- .../transaction/Neo4jConnectionHolder.java | 2 +- .../repository/query/PartTreeNeo4jQuery.java | 8 ++++---- .../query/ReactivePartTreeNeo4jQuery.java | 8 ++++---- .../data/neo4j/core/cypher/CypherIT.java | 19 ++++++++++++++----- .../data/neo4j/core/cypher/PropertyTest.java | 2 +- 12 files changed, 36 insertions(+), 29 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java index b3931c10c..b80b6e07a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java @@ -37,7 +37,7 @@ public final class Comparison implements Condition { static Comparison create(Operator operator, Expression expression) { - Assert.state(operator.isUnary(), "Operator must be unary."); + Assert.isTrue(operator.isUnary(), "Operator must be unary."); Assert.notNull(expression, "Expression must not be null."); switch (operator.getType()) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java index 827b8277f..19c9f38b4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java @@ -48,7 +48,7 @@ public final class CompoundCondition implements Condition { static CompoundCondition create(Condition left, Operator operator, Condition right) { - Assert.state(VALID_OPERATORS.contains(operator), + Assert.isTrue(VALID_OPERATORS.contains(operator), "Operator " + operator + " is not a valid operator for a compound condition."); Assert.notNull(left, "Left hand side condition is required."); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java index 5e2bd5302..ff0a065d0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java @@ -286,22 +286,21 @@ public final class Cypher { private static Statement unionImpl(boolean unionAll, Statement... statements) { - Assert.notEmpty(statements, "At least two statements are required!"); + Assert.isTrue(statements != null && statements.length >= 2, "At least two statements are required!"); int i = 0; UnionQuery existingUnionQuery = null; if (statements[0] instanceof UnionQuery) { existingUnionQuery = (UnionQuery) statements[0]; - Assert.state(existingUnionQuery.isAll() == unionAll, "Cannot mix union and union all!"); + Assert.isTrue(existingUnionQuery.isAll() == unionAll, "Cannot mix union and union all!"); i = 1; } List listOfQueries = new ArrayList<>(); - while (i < statements.length) { + do { Assert.isInstanceOf(SingleQuery.class, statements[i], "Can only union single queries!"); listOfQueries.add((SingleQuery) statements[i]); - ++i; - } + } while (++i < statements.length); if (existingUnionQuery == null) { return UnionQuery.create(unionAll, listOfQueries); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java index aa9209b64..a256de7b2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java @@ -42,14 +42,14 @@ public final class MapExpression> extends TypedSubtre static MapExpression create(Object... input) { - Assert.state(input.length % 2 == 0, "Need an even number of input parameters"); + Assert.isTrue(input.length % 2 == 0, "Need an even number of input parameters"); List newContent = new ArrayList<>(input.length / 2); Set knownKeys = new HashSet<>(); for (int i = 0; i < input.length; i += 2) { Assert.isInstanceOf(String.class, input[i], "Key needs to be of type String."); Assert.isInstanceOf(Expression.class, input[i + 1], "Value needs to be of type Expression."); - Assert.state(!knownKeys.contains(input[i]), "Duplicate key '" + input[i] + "'"); + Assert.isTrue(!knownKeys.contains(input[i]), "Duplicate key '" + input[i] + "'"); final MapEntry entry = new MapEntry((String) input[i], (Expression) input[i + 1]); newContent.add(entry); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java index 07a2263f9..491b3d3da 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java @@ -57,8 +57,8 @@ public final class Operation implements Expression { static Operation create(Node op1, Operator operator, String... nodeLabels) { Assert.notNull(op1, "The first operand must not be null."); - Assert.state(op1.getSymbolicName().isPresent(), "The node must have a name."); - Assert.state(LABEL_OPERATORS.contains(operator), + Assert.isTrue(op1.getSymbolicName().isPresent(), "The node must have a name."); + Assert.isTrue(LABEL_OPERATORS.contains(operator), () -> String.format("Only operators %s can be used to modify labels", LABEL_OPERATORS)); Assert.notEmpty(nodeLabels, "The labels cannot be empty."); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java index 2169450c4..1727c5e76 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java @@ -33,7 +33,7 @@ public final class Property implements Expression { static Property create(Node parentContainer, String name) { - Assert.state(parentContainer.getSymbolicName().isPresent(), + Assert.isTrue(parentContainer.getSymbolicName().isPresent(), "A property derived from a node needs a parent with a symbolic name."); Assert.hasText(name, "The properties name is required."); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java index 9045c15e1..abea662a8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java @@ -37,8 +37,7 @@ public final class UnionQuery implements RegularQuery { static UnionQuery create(boolean unionAll, List queries) { - Assert.notNull(queries, "Queries are needed."); - Assert.state(queries.size() >= 2, "At least two queries are needed."); + Assert.isTrue(queries != null && queries.size() >= 2, "At least two queries are needed."); List unionParts = queries.stream().skip(1).map(q -> new UnionPart(unionAll, q)).collect(toList()); return new UnionQuery(unionAll, queries.get(0), unionParts); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java index 00aa2df29..a1cd8641a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java @@ -74,7 +74,7 @@ class Neo4jConnectionHolder extends ResourceHolderSupport { void commit() { Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed."); - Assert.state(!isRollbackOnly(), "Resource msut not be marked as rollback only."); + Assert.state(!isRollbackOnly(), "Resource must not be marked as rollback only."); transaction.success(); transaction.close(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 162820cb6..c49016940 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -169,7 +169,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { private void validateIgnoreCase(Part part) { - Assert.state(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), + Assert.isTrue(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), () -> String.format( "Can not derive query for '%s': Only the case of String based properties can be ignored within the following keywords: %s", queryMethod, @@ -178,7 +178,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { private void validateTemporalProperty(Part part) { - Assert.state(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String + Assert.isTrue(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String .format( "Can not derive query for '%s': The keywords %s work only with properties with one of the following types: %s", queryMethod, formatTypes(Collections.singletonList(part.getType())), @@ -187,7 +187,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { private void validateCollectionProperty(Part part) { - Assert.state(part.getProperty().getLeafProperty().isCollection(), () -> String + Assert.isTrue(part.getProperty().getLeafProperty().isCollection(), () -> String .format("Can not derive query for '%s': The keywords %s work only with collection properties", queryMethod, formatTypes(Collections.singletonList(part.getType())))); @@ -195,7 +195,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { private void validatePointProperty(Part part) { - Assert.state(ClassTypeInformation.from(Point.class) + Assert.isTrue(ClassTypeInformation.from(Point.class) .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), () -> String .format("Can not derive query for '%s': %s works only with spatial properties", queryMethod, part.getType())); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java index 57e02a5f4..e61c164c1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java @@ -168,7 +168,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { private void validateIgnoreCase(Part part) { - Assert.state(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), + Assert.isTrue(part.shouldIgnoreCase() != Part.IgnoreCaseType.ALWAYS || canIgnoreCase(part), () -> String.format( "Can not derive query for '%s': Only the case of String based properties can be ignored within the following keywords: %s", queryMethod, @@ -177,7 +177,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { private void validateTemporalProperty(Part part) { - Assert.state(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String + Assert.isTrue(COMPARABLE_TEMPORAL_TYPES.contains(part.getProperty().getLeafType()), () -> String .format( "Can not derive query for '%s': The keywords %s work only with properties with one of the following types: %s", queryMethod, formatTypes(Collections.singletonList(part.getType())), @@ -186,7 +186,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { private void validateCollectionProperty(Part part) { - Assert.state(part.getProperty().getLeafProperty().isCollection(), () -> String + Assert.isTrue(part.getProperty().getLeafProperty().isCollection(), () -> String .format("Can not derive query for '%s': The keywords %s work only with collection properties", queryMethod, formatTypes(Collections.singletonList(part.getType())))); @@ -194,7 +194,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { private void validatePointProperty(Part part) { - Assert.state(ClassTypeInformation.from(Point.class) + Assert.isTrue(ClassTypeInformation.from(Point.class) .isAssignableFrom(part.getProperty().getLeafProperty().getTypeInformation()), () -> String .format("Can not derive query for '%s': %s works only with spatial properties", queryMethod, part.getType())); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java index 53cb02336..567d3ac6c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java @@ -1704,12 +1704,16 @@ class CypherIT { .returning(bikeNode) .build(); + Statement statement3 = Cypher.match(bikeNode) + .where(bikeNode.property("c").isEqualTo(literalOf("C"))) + .returning(bikeNode) + .build(); Statement statement; - statement = Cypher.union(statement1, statement2); + statement = Cypher.union(statement1, statement2, statement3); assertThat(cypherRenderer.render(statement)) .isEqualTo( - "MATCH (b:`Bike`) WHERE b.a = 'A' RETURN b UNION MATCH (b) WHERE b.b = 'B' RETURN b"); + "MATCH (b:`Bike`) WHERE b.a = 'A' RETURN b UNION MATCH (b) WHERE b.b = 'B' RETURN b UNION MATCH (b) WHERE b.c = 'C' RETURN b"); } @Test @@ -1754,11 +1758,16 @@ class CypherIT { .returning(bikeNode) .build(); - statement = Cypher.unionAll(statement, statement3); + Statement statement4 = Cypher.match(bikeNode) + .where(bikeNode.property("d").isEqualTo(literalOf("D"))) + .returning(bikeNode) + .build(); + + statement = Cypher.unionAll(statement, statement3, statement4); assertThat(cypherRenderer.render(statement)) .isEqualTo( - "MATCH (b:`Bike`) WHERE b.a = 'A' RETURN b UNION ALL MATCH (b) WHERE b.b = 'B' RETURN b UNION ALL MATCH (b) WHERE b.c = 'C' RETURN b"); + "MATCH (b:`Bike`) WHERE b.a = 'A' RETURN b UNION ALL MATCH (b) WHERE b.b = 'B' RETURN b UNION ALL MATCH (b) WHERE b.c = 'C' RETURN b UNION ALL MATCH (b) WHERE b.d = 'D' RETURN b"); } @Test @@ -1782,7 +1791,7 @@ class CypherIT { .returning(bikeNode) .build(); - assertThatIllegalStateException().isThrownBy(() -> + assertThatIllegalArgumentException().isThrownBy(() -> Cypher.union(statement, statement3)).withMessage("Cannot mix union and union all!"); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java index 6424ccbd1..386e02749 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java @@ -30,7 +30,7 @@ class PropertyTest { @Test void preconditionsShouldBeAsserted() { - assertThatIllegalStateException().isThrownBy(() -> Property.create(Node.create("a"), "")) + assertThatIllegalArgumentException().isThrownBy(() -> Property.create(Node.create("a"), "")) .withMessage("A property derived from a node needs a parent with a symbolic name."); String expectedMessage = "The properties name is required."; assertThatIllegalArgumentException().isThrownBy(() -> Property.create(Node.create("a").named("s"), null)) From bca87cd1389eb2945a4a6463cd4871d907e2af3f Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 3 Jun 2019 12:06:22 +0200 Subject: [PATCH 105/342] Update to Java-Driver 2.0.0-alpha02. --- pom.xml | 2 +- .../data/neo4j/core/DefaultNeo4jClient.java | 6 +-- .../core/DefaultReactiveNeo4jClient.java | 7 +-- .../transaction/Neo4jConnectionHolder.java | 9 ++-- .../transaction/Neo4jTransactionManager.java | 17 +++++-- .../transaction/Neo4jTransactionUtils.java | 14 +++--- .../data/neo4j/core/Neo4jClientTest.java | 48 +++++++++++-------- .../neo4j/core/ReactiveNeo4jClientTest.java | 35 +++++++------- .../neo4j/core/TransactionHandlingTest.java | 13 ++--- .../Neo4jConnectionHolderTest.java | 4 -- .../Neo4jTransactionManagerTest.java | 6 +-- .../Neo4jTransactionUtilsTest.java | 6 +-- 12 files changed, 85 insertions(+), 82 deletions(-) diff --git a/pom.xml b/pom.xml index aa91ab839..4884dfe50 100644 --- a/pom.xml +++ b/pom.xml @@ -99,7 +99,7 @@ ${java.version} ${java.version} 2.23.4 - 2.0.0-alpha01 + 2.0.0-alpha02 3.5.2 UTF-8 1.2.1 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index a93e55fb8..dd69cbf71 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -39,7 +39,6 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; -import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; @@ -67,10 +66,7 @@ class DefaultNeo4jClient implements Neo4jClient { DefaultNeo4jClient(Driver driver) { this.driver = driver; - // This will go away - try (Session session = this.driver.session(t -> t.withDefaultAccessMode(AccessMode.READ))) { - typeSystem = session.typeSystem(); - } + this.typeSystem = driver.defaultTypeSystem(); } AutoCloseableStatementRunner getStatementRunner(final String targetDatabase) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 4afd95e46..fbf5185e6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -33,10 +33,8 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; -import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; -import org.neo4j.driver.Session; import org.neo4j.driver.exceptions.NoSuchRecordException; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; @@ -68,10 +66,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { DefaultReactiveNeo4jClient(Driver driver) { this.driver = driver; - // This will go away - try (Session session = this.driver.session(t -> t.withDefaultAccessMode(AccessMode.READ))) { - typeSystem = session.typeSystem(); - } + this.typeSystem = driver.defaultTypeSystem(); } // Internal helper methods for managing transactional state diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java index a1cd8641a..a56319d33 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java @@ -18,11 +18,10 @@ */ package org.springframework.data.neo4j.core.transaction; -import java.util.Optional; - import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; +import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; @@ -64,11 +63,9 @@ class Neo4jConnectionHolder extends ResourceHolderSupport { return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? transaction : null; } - static boolean namesMapToTheSameDatabase(String name1, String name2) { - String d1 = Optional.ofNullable(name1).orElse(Neo4jTransactionUtils.DEFAULT_DATABASE_NAME); - String d2 = Optional.ofNullable(name2).orElse(Neo4jTransactionUtils.DEFAULT_DATABASE_NAME); + static boolean namesMapToTheSameDatabase(@Nullable String name1, @Nullable String name2) { - return d1.equals(d2); + return name1 == null && name2 == null || (name1 != null && name1.equals(name2)); } void commit() { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index f6459e6ec..05fd789ea 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -53,10 +53,22 @@ import org.springframework.util.Assert; @Slf4j public class Neo4jTransactionManager extends AbstractPlatformTransactionManager { + /** + * The underlying driver, which is also the synchronisation object. + */ private final Driver driver; + /** + * The name of the target database. + */ + private final String databaseName; public Neo4jTransactionManager(Driver driver) { + this(driver, null); + } + + public Neo4jTransactionManager(Driver driver, String databaseName) { this.driver = driver; + this.databaseName = databaseName; } @Override @@ -82,15 +94,14 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager boolean readOnly = definition.isReadOnly(); AccessMode accessMode = readOnly ? AccessMode.READ : AccessMode.WRITE; List bookmarks = Collections.emptyList(); // TODO Bookmarksupport - String database = ""; // TODO Database selection TransactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); try { Session session = this.driver - .session(t -> t.withDefaultAccessMode(accessMode).withBookmarks(bookmarks).withDatabase(database)); + .session(t -> t.withDefaultAccessMode(accessMode).withBookmarks(bookmarks).withDatabase(databaseName)); - Neo4jConnectionHolder connectionHolder = new Neo4jConnectionHolder(database, session, transactionConfig); + Neo4jConnectionHolder connectionHolder = new Neo4jConnectionHolder(databaseName, session, transactionConfig); connectionHolder.setSynchronizedWithTransaction(true); transactionObject.setResourceHolder(connectionHolder); TransactionSynchronizationManager.bindResource(this.driver, connectionHolder); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index 237c2abc9..3bbc7484f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -37,8 +37,6 @@ import org.springframework.transaction.support.TransactionSynchronizationManager */ public final class Neo4jTransactionUtils { - public static final String DEFAULT_DATABASE_NAME = ""; - /** * The default session uses {@link AccessMode#WRITE} and an empty list of bookmarks. * @@ -46,10 +44,14 @@ public final class Neo4jTransactionUtils { * @return Session parameters to configure the default session used */ public static Consumer defaultSessionParameters(@Nullable String databaseName) { - return t -> t - .withDefaultAccessMode(AccessMode.WRITE) - .withBookmarks(Collections.EMPTY_LIST) - .withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME)); + return (SessionParametersTemplate t) -> { + t.withDefaultAccessMode(AccessMode.WRITE) + .withBookmarks(Collections.EMPTY_LIST); + + if (databaseName != null) { + t.withDatabase(databaseName); + } + }; } /** diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java index 9c344a4bb..eeb2282be 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java @@ -21,7 +21,6 @@ package org.springframework.data.neo4j.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import static org.mockito.hamcrest.MockitoHamcrest.argThat; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; import java.time.LocalDate; import java.util.ArrayList; @@ -92,13 +91,12 @@ class Neo4jClientTest { @BeforeEach void prepareMocks() { - when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) .thenReturn(sessionParametersTemplate); when(driver.session(any(Consumer.class))).thenReturn(session); - when(session.typeSystem()).thenReturn(typeSystem); + when(driver.defaultTypeSystem()).thenReturn(typeSystem); } @AfterEach @@ -135,7 +133,7 @@ class Neo4jClientTest { assertThat(usedBikes).hasSize(2); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); Map expectedParameters = new HashMap<>(); expectedParameters.putAll(parameters); @@ -146,12 +144,13 @@ class Neo4jClientTest { verify(statementResult).stream(); verify(record1).asMap(); verify(record2).asMap(); - verify(session, times(2)).close(); + verify(session).close(); } @Test void databaseSelectionShouldBePossibleOnlyOnce() { + when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.stream()).thenReturn(Stream.of(record1, record2)); @@ -176,7 +175,7 @@ class Neo4jClientTest { verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).stream(); verify(record1).asMap(); - verify(session, times(2)).close(); + verify(session).close(); } @Nested @@ -193,14 +192,16 @@ class Neo4jClientTest { assertThat(result).isPresent().hasValue(42); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); - verify(session, times(2)).close(); + verify(session).close(); } @Test void withDatabase() { + when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); + Neo4jClient client = Neo4jClient.create(driver); Optional result = client .delegateTo(runner -> Optional.of(42)) @@ -211,7 +212,7 @@ class Neo4jClientTest { verifyDatabaseSelection("aDatabase"); - verify(session, times(2)).close(); + verify(session).close(); } } @@ -241,7 +242,7 @@ class Neo4jClientTest { assertThat(bikeOwners).hasSize(1).first() .hasFieldOrPropertyWithValue("name", "michael"); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "michael"); @@ -249,7 +250,7 @@ class Neo4jClientTest { verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).stream(); verify(record1).get("name"); - verify(session, times(2)).close(); + verify(session).close(); } @Test @@ -272,12 +273,12 @@ class Neo4jClientTest { }) .all()); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); verify(session).run(eq("MATCH (n) RETURN n"), argThat(new MapAssertionMatcher(Collections.emptyMap()))); verify(statementResult).stream(); verify(record1).get("name"); - verify(session, times(2)).close(); + verify(session).close(); } @Test @@ -298,14 +299,14 @@ class Neo4jClientTest { .bind(michael).with(new BikeOwnerBinder()) .run(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "Michael"); verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).consume(); - verify(session, times(2)).close(); + verify(session).close(); } @Test @@ -328,12 +329,12 @@ class Neo4jClientTest { assertThat(numberOfBikes).isPresent().hasValue(23L); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); verify(session).run(eq(cypher), anyMap()); verify(statementResult).hasNext(); verify(statementResult).single(); - verify(session, times(2)).close(); + verify(session).close(); } } @@ -353,14 +354,14 @@ class Neo4jClientTest { .bind("fixie").to("name") .run(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "fixie"); verify(session).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).consume(); - verify(session, times(2)).close(); + verify(session).close(); } static class BikeOwner { @@ -417,9 +418,14 @@ class Neo4jClientTest { } void verifyDatabaseSelection(String targetDatabase) { - verify(driver, times(2)).session(sessionTemplateCaptor.capture()); + + verify(driver).session(sessionTemplateCaptor.capture()); sessionTemplateCaptor.getValue().accept(sessionParametersTemplate); - verify(sessionParametersTemplate).withDatabase(targetDatabase); + if (targetDatabase != null) { + verify(sessionParametersTemplate).withDatabase(targetDatabase); + } else { + verify(sessionParametersTemplate, never()).withDatabase(any()); + } } static class MapAssertionMatcher extends AssertionMatcher> { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java index 632e14b2a..c0dad2ab2 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java @@ -21,7 +21,6 @@ package org.springframework.data.neo4j.core; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.mockito.hamcrest.MockitoHamcrest.argThat; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -46,11 +45,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; -import org.neo4j.driver.Session; import org.neo4j.driver.Values; import org.neo4j.driver.internal.SessionParameters; -import org.neo4j.driver.reactive.RxResult; import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxStatementResult; import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; @@ -69,9 +67,6 @@ class ReactiveNeo4jClientTest { @Mock private Driver driver; - @Mock - private Session goAwaySession; - @Mock private TypeSystem typeSystem; @@ -84,7 +79,7 @@ class ReactiveNeo4jClientTest { private RxSession session; @Mock - private RxResult statementResult; + private RxStatementResult statementResult; @Mock private RxTransaction transaction; @@ -101,10 +96,8 @@ class ReactiveNeo4jClientTest { @BeforeEach void prepareMocks() { - when(driver.session(any(Consumer.class))).thenReturn(goAwaySession); - when(goAwaySession.typeSystem()).thenReturn(typeSystem); + when(driver.defaultTypeSystem()).thenReturn(typeSystem); - when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) .thenReturn(sessionParametersTemplate); @@ -153,7 +146,7 @@ class ReactiveNeo4jClientTest { .expectNextCount(2L) .verifyComplete(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); Map expectedParameters = new HashMap<>(); expectedParameters.putAll(parameters); @@ -171,6 +164,7 @@ class ReactiveNeo4jClientTest { @Test void databaseSelectionShouldBePossibleOnlyOnce() { + when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); @@ -219,7 +213,7 @@ class ReactiveNeo4jClientTest { .expectNext(21) .verifyComplete(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); verify(transaction).commit(); verify(session).close(); @@ -228,6 +222,7 @@ class ReactiveNeo4jClientTest { @Test void withDatabase() { + when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(transaction.commit()).thenReturn(Mono.empty()); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -275,7 +270,7 @@ class ReactiveNeo4jClientTest { .expectNextMatches(o -> o.getName().equals("michael")) .verifyComplete(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "michael"); @@ -311,7 +306,7 @@ class ReactiveNeo4jClientTest { .expectNextCount(1) .verifyError(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); verify(transaction).run(eq("MATCH (n) RETURN n"), argThat(new MapAssertionMatcher(Collections.emptyMap()))); verify(statementResult).records(); @@ -344,7 +339,7 @@ class ReactiveNeo4jClientTest { .expectNext(resultSummary) .verifyComplete(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "Michael"); @@ -377,7 +372,7 @@ class ReactiveNeo4jClientTest { .expectNext(23L) .verifyComplete(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); verify(transaction).run(eq(cypher), anyMap()); verify(transaction).commit(); @@ -406,7 +401,7 @@ class ReactiveNeo4jClientTest { .expectNext(resultSummary) .verifyComplete(); - verifyDatabaseSelection(DEFAULT_DATABASE_NAME); + verifyDatabaseSelection(null); Map expectedParameters = new HashMap<>(); expectedParameters.put("name", "fixie"); @@ -420,6 +415,10 @@ class ReactiveNeo4jClientTest { void verifyDatabaseSelection(String targetDatabase) { verify(driver).rxSession(sessionTemplateCaptor.capture()); sessionTemplateCaptor.getValue().accept(sessionParametersTemplate); - verify(sessionParametersTemplate).withDatabase(targetDatabase); + if (targetDatabase != null) { + verify(sessionParametersTemplate).withDatabase(targetDatabase); + } else { + verify(sessionParametersTemplate, never()).withDatabase(any()); + } } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java index 4e8720df1..9101c8327 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java @@ -71,15 +71,14 @@ class TransactionHandlingTest { @BeforeEach void prepareMocks() { - when(driver.session(any(Consumer.class))).thenReturn(session); - when(session.typeSystem()).thenReturn(typeSystem); + when(driver.defaultTypeSystem()).thenReturn(typeSystem); } @AfterEach void verifyTypeSystemOnSession() { - verify(session).typeSystem(); + verify(driver).defaultTypeSystem(); } @Nested @@ -96,6 +95,7 @@ class TransactionHandlingTest { ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); + when(driver.session(any(Consumer.class))).thenReturn(session); when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) @@ -107,12 +107,12 @@ class TransactionHandlingTest { s.run("MATCH (n) RETURN n"); } - verify(driver, times(2)).session(consumerCaptor.capture()); + verify(driver).session(consumerCaptor.capture()); consumerCaptor.getValue().accept(sessionParametersTemplate); verify(sessionParametersTemplate).withDatabase("aDatabase"); verify(session).run(any(String.class)); - verify(session, times(2)).close(); + verify(session).close(); verifyNoMoreInteractions(driver, sessionParametersTemplate, session, transaction); } @@ -122,6 +122,7 @@ class TransactionHandlingTest { AtomicBoolean transactionIsOpen = new AtomicBoolean(true); + when(driver.session(any(Consumer.class))).thenReturn(session); when(session.isOpen()).thenReturn(true); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); // Mock closing of the transaction @@ -148,7 +149,7 @@ class TransactionHandlingTest { verify(transaction).success(); verify(transaction).close(); verify(session).isOpen(); - verify(session, times(2)).close(); + verify(session).close(); verifyNoMoreInteractions(driver, session, transaction); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java index 546fa759f..a93874494 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java @@ -19,7 +19,6 @@ package org.springframework.data.neo4j.core.transaction; import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; @@ -36,9 +35,6 @@ class Neo4jConnectionHolderTest { @ParameterizedTest @CsvSource({ ",", - "," + DEFAULT_DATABASE_NAME, - DEFAULT_DATABASE_NAME + ",", - DEFAULT_DATABASE_NAME + "," + DEFAULT_DATABASE_NAME, "a,a" }) void nameComparisionShouldWorkForNamesTargetingTheSame(String name1, String name2) { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java index 7c85318fa..96bb8c647 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java @@ -56,11 +56,11 @@ class Neo4jTransactionManagerTest { @Test public void triggerCommitCorrectly() { + when(driver.defaultTypeSystem()).thenReturn(typeSystem); when(driver.session(any(Consumer.class))).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(session.isOpen()).thenReturn(true); - when(session.typeSystem()).thenReturn(typeSystem); when(transaction.isOpen()).thenReturn(true, false); Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); @@ -71,7 +71,7 @@ class Neo4jTransactionManagerTest { txManager.commit(txStatus); - verify(driver, times(2)).session(any(Consumer.class)); + verify(driver).session(any(Consumer.class)); verify(session).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -80,6 +80,6 @@ class Neo4jTransactionManagerTest { verify(transaction).success(); verify(transaction).close(); - verify(session, times(2)).close(); + verify(session).close(); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java index 3f110f6a2..c5690a6f1 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java @@ -54,7 +54,7 @@ import org.springframework.transaction.support.TransactionTemplate; @MockitoSettings(strictness = Strictness.LENIENT) class Neo4jTransactionUtilsTest { - private String databaseName = Neo4jTransactionUtils.DEFAULT_DATABASE_NAME; + private String databaseName = "aDatabase"; @Mock private Driver driver; @@ -101,7 +101,7 @@ class Neo4jTransactionUtilsTest { @Test void shouldOpenNewTransaction() { - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver, databaseName); TransactionTemplate txTemplate = new TransactionTemplate(txManager); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -134,7 +134,7 @@ class Neo4jTransactionUtilsTest { @Test void shouldParticipateInOngoingTransaction() { - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver); + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver, databaseName); TransactionTemplate txTemplate = new TransactionTemplate(txManager); txTemplate.execute(new TransactionCallbackWithoutResult() { From 67bee606228998f66f70f7da3f3c6123061207b2 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 28 May 2019 17:22:49 +0200 Subject: [PATCH 106/342] Introduce reactive transaction manager. - Set dependency to Spring Data commons 2.2.0.M4. - Transaction synchronization with other transaction managers. - Shared Neo4jTransactionUtils for reactive and imperative. --- pom.xml | 13 +- spring-data-neo4j/pom.xml | 13 + .../config/AbstractReactiveNeo4jConfig.java | 2 +- .../ReactiveNeo4jConfigurationSupport.java | 132 +++++++++ .../data/neo4j/core/DefaultNeo4jClient.java | 15 +- .../core/DefaultReactiveNeo4jClient.java | 156 +++++------ .../data/neo4j/core/Neo4jClient.java | 7 +- .../data/neo4j/core/ReactiveNeo4jClient.java | 8 +- .../Neo4jSessionSynchronization.java | 10 +- ...older.java => Neo4jTransactionHolder.java} | 23 +- .../transaction/Neo4jTransactionManager.java | 121 ++++---- .../transaction/Neo4jTransactionUtils.java | 66 ++--- .../ReactiveNeo4jSessionSynchronization.java | 84 ++++++ .../ReactiveNeo4jTransactionHolder.java | 81 ++++++ .../ReactiveNeo4jTransactionManager.java | 261 ++++++++++++++++++ .../support/SimpleNeo4jRepository.java | 8 +- .../data/neo4j/core/Neo4jClientTest.java | 14 +- .../neo4j/core/ReactiveNeo4jClientTest.java | 24 +- .../neo4j/core/TransactionHandlingTest.java | 51 +--- .../Neo4jConnectionHolderTest.java | 56 ---- .../Neo4jTransactionManagerTest.java | 232 +++++++++++++++- .../Neo4jTransactionUtilsTest.java | 245 ++-------------- .../ReactiveNeo4jTransactionManagerTest.java | 206 ++++++++++++++ 23 files changed, 1284 insertions(+), 544 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jConfigurationSupport.java rename spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/{Neo4jConnectionHolder.java => Neo4jTransactionHolder.java} (80%) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jSessionSynchronization.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java delete mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java diff --git a/pom.xml b/pom.xml index 4884dfe50..6703afbed 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ spring-data-parent org.springframework.data.build - 2.1.5.RELEASE + 2.2.0.BUILD-SNAPSHOT org.springframework.data @@ -106,8 +106,8 @@ Californium-SR4 1.3.8 2.2.5 - 1.7.25 - 2.1.5.RELEASE + 1.7.26 + 2.2.0.M4 1.10.7 ${skipTests} @@ -384,4 +384,11 @@ + + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index bfab0f00b..31eae3069 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -64,6 +64,19 @@ 1.1 test + + org.springframework.data + spring-data-r2dbc + 1.0.0.M2 + test + + + io.r2dbc + r2dbc-h2 + 0.8.0.M8 + test + + org.apiguardian apiguardian-api diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java index 2ab9b6ba0..754f385d6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java @@ -33,7 +33,7 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jClient; */ @Configuration @API(status = API.Status.STABLE, since = "1.0") -public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupport { +public abstract class AbstractReactiveNeo4jConfig extends ReactiveNeo4jConfigurationSupport { /** * The driver used here should be the driver resulting from {@link #driver()}, which is the default. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jConfigurationSupport.java new file mode 100644 index 000000000..578c7373a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jConfigurationSupport.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.config; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.apiguardian.api.API; +import org.neo4j.driver.Driver; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Internal support class for basic configuration. The support infrastructure here is basically all around finding out about + * which classes are to be mapped and which not. The driver is part of the configuration support, as Neo4js Java driver + * contains both imperative and reactive components. + * + * @author Michael J. Simons + * @author Gerrit Meier + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public abstract class ReactiveNeo4jConfigurationSupport { + + /** + * The driver to be used for interacting with Neo4j. + * + * @return + */ + public abstract Driver driver(); + + /** + * Creates a {@link Neo4jMappingContext} equipped with entity classes + * scanned from the mapping base package. + * + * @return + * @see #getMappingBasePackages() + */ + @Bean + public Neo4jMappingContext neo4jMappingContext() throws ClassNotFoundException { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + mappingContext.setInitialEntitySet(getInitialEntitySet()); + + return mappingContext; + } + + /** + * Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}. + * + * @param driver The driver to synchronize against + * @return A platform transaction manager + */ + @Bean + public ReactiveTransactionManager transactionManager(Driver driver) { + + return new ReactiveNeo4jTransactionManager(driver); + } + + /** + * Returns the base packages to scan for Neo4j mapped entities at startup. Will return the package name of the + * configuration class' (the concrete class, not this one here) by default. So if you have a + * {@code com.acme.AppConfig} extending {@link ReactiveNeo4jConfigurationSupport} the base package will be considered + * {@code com.acme} unless the method is overridden to implement alternate behavior. + * + * @return the base packages to scan for mapped {@link Node} classes + * or an empty collection to not enable scanning for entities. + */ + protected Collection getMappingBasePackages() { + + Package mappingBasePackage = getClass().getPackage(); + return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName()); + } + + private Set> getInitialEntitySet() throws ClassNotFoundException { + + Set> initialEntitySet = new HashSet>(); + + for (String basePackage : getMappingBasePackages()) { + initialEntitySet.addAll(scanForEntities(basePackage)); + } + + return initialEntitySet; + } + + private Set> scanForEntities(String basePackage) throws ClassNotFoundException { + + if (!StringUtils.hasText(basePackage)) { + return Collections.emptySet(); + } + + Set> initialEntitySet = new HashSet>(); + + ClassPathScanningCandidateComponentProvider componentProvider = + new ClassPathScanningCandidateComponentProvider(false); + componentProvider.addIncludeFilter(new AnnotationTypeFilter(Node.class)); + + ClassLoader classLoader = ReactiveNeo4jConfigurationSupport.class.getClassLoader(); + for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) { + initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), classLoader)); + } + + return initialEntitySet; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index dd69cbf71..2f95c8524 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core; import static java.util.stream.Collectors.*; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager.*; import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; import lombok.AccessLevel; @@ -49,6 +50,7 @@ import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.repository.NoResultException; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -81,7 +83,7 @@ class DefaultNeo4jClient implements Neo4jClient { } /** - * Makes a statement runner autoclosable and aware wether it's session or a transaction + * Makes a statement runner automatically closeable and aware whether it's session or a transaction */ interface AutoCloseableStatementRunner extends StatementRunner, AutoCloseable { @@ -123,12 +125,12 @@ class DefaultNeo4jClient implements Neo4jClient { // Below are all the implementations (methods and classes) as defined by the contracts of Neo4jClient @Override - public RunnableSpec newQuery(String cypher) { - return newQuery(() -> cypher); + public RunnableSpec query(String cypher) { + return query(() -> cypher); } @Override - public RunnableSpec newQuery(Supplier cypherSupplier) { + public RunnableSpec query(Supplier cypherSupplier) { return new DefaultRunnableSpec(cypherSupplier); } @@ -141,7 +143,7 @@ class DefaultNeo4jClient implements Neo4jClient { public ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery) { Neo4jClient.MappingSpec, Collection, T> mappingSpec = this - .newQuery(preparedQuery.getCypherQuery()) + .query(preparedQuery.getCypherQuery()) .bindAll(preparedQuery.getParameters()) .fetchAs(preparedQuery.getResultType()); Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec = preparedQuery @@ -206,6 +208,7 @@ class DefaultNeo4jClient implements Neo4jClient { @RequiredArgsConstructor class DefaultOngoingBindSpec implements OngoingBindSpec { + @Nullable private final T value; @Override @@ -225,7 +228,7 @@ class DefaultNeo4jClient implements Neo4jClient { } @Override - public OngoingBindSpec bind(Object value) { + public OngoingBindSpec bind(@Nullable Object value) { return new DefaultOngoingBindSpec(value); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index fbf5185e6..857914440 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core; import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; +import static org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -38,7 +39,6 @@ import org.neo4j.driver.Record; import org.neo4j.driver.exceptions.NoSuchRecordException; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; -import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.reactivestreams.Publisher; @@ -46,14 +46,14 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; import org.springframework.data.neo4j.core.Neo4jClient.OngoingBindSpec; import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * Reactive variant of the {@link Neo4jClient}. * - * TODO Reactive transaction management is pretty obvious still open - * * @author Michael J. Simons + * @author Gerrit Meier * @soundtrack Die Toten Hosen - Im Auftrag des Herrn * @since 1.0 */ @@ -69,56 +69,41 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { this.typeSystem = driver.defaultTypeSystem(); } - // Internal helper methods for managing transactional state + Mono retrieveRxStatementRunnerHolder(String targetDatabase) { - Mono getStatementRunner(final String targetDatabase) { return retrieveReactiveTransaction(driver, targetDatabase) + .map(rxTransaction -> new RxStatementRunnerHolder(rxTransaction, Mono.empty(), Mono.empty())) // .switchIfEmpty( Mono.using(() -> driver.rxSession(defaultSessionParameters(targetDatabase)), - session -> Mono.from(session.beginTransaction()), RxSession::close) - ) - .map(RxStatementRunner.class::cast); + session -> Mono.from(session.beginTransaction()) + .map(tx -> new RxStatementRunnerHolder(tx, tx.commit(), tx.rollback())), RxSession::close) + ); } - static boolean isSpringTransactionManagementActive() { - // TODO Something along the lines of if TransactionSynchronizationManager#isSynchronizationActive but for reactive - return false; + Mono doInStatementRunnerForMono(final String targetDatabase, Function> func) { + + return Mono.usingWhen(retrieveRxStatementRunnerHolder(targetDatabase), + holder -> func.apply(holder.getRxStatementRunner()), + RxStatementRunnerHolder::getCommit, + RxStatementRunnerHolder::getRollback); + } - static Publisher asyncComplete(RxStatementRunner runner) { + Flux doInStatementRunnerForFlux(final String targetDatabase, Function> func) { - if (runner instanceof RxTransaction && !isSpringTransactionManagementActive()) { - return ((RxTransaction) runner).commit(); - } - - return Mono.empty(); - } - - static Publisher asyncCancel(RxStatementRunner runner) { - - log.debug("Request has been cancelled, ongoing transactions will be committed."); - - return asyncComplete(runner); - } - - static Publisher asyncError(RxStatementRunner runner) { - - if (runner instanceof RxTransaction && !isSpringTransactionManagementActive()) { - return ((RxTransaction) runner).rollback(); - } - - return Mono.empty(); - } - - // Below are all the implementations (methods and classes) as defined by the contracts of ReactiveNeo4jClient - - @Override - public ReactiveRunnableSpec newQuery(String cypher) { - return newQuery(() -> cypher); + return Flux.usingWhen(retrieveRxStatementRunnerHolder(targetDatabase), + holder -> func.apply(holder.getRxStatementRunner()), + RxStatementRunnerHolder::getCommit, + RxStatementRunnerHolder::getRollback); } @Override - public ReactiveRunnableSpec newQuery(Supplier cypherSupplier) { + public ReactiveRunnableSpec query(String cypher) { + return query(() -> cypher); + } + + @Override + public ReactiveRunnableSpec query(Supplier cypherSupplier) { return new DefaultReactiveRunnableSpec(cypherSupplier); } @@ -132,7 +117,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { Class resultType = preparedQuery.getResultType(); Neo4jClient.MappingSpec, Flux, T> mappingSpec = this - .newQuery(preparedQuery.getCypherQuery()) + .query(preparedQuery.getCypherQuery()) .bindAll(preparedQuery.getParameters()) .fetchAs(resultType); @@ -162,6 +147,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { @RequiredArgsConstructor class DefaultOngoingBindSpec implements OngoingBindSpec { + @Nullable private final T value; @Override @@ -181,7 +167,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } @Override - public OngoingBindSpec bind(Object value) { + public OngoingBindSpec bind(@Nullable Object value) { return new DefaultOngoingBindSpec(value); } @@ -236,7 +222,14 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } Mono>> prepareStatement() { + if (cypherLog.isDebugEnabled()) { + String cypher = cypherSupplier.get(); + cypherLog.debug("Executing:{}{}", System.lineSeparator(), cypher); + if (cypherLog.isTraceEnabled() && !parameters.isEmpty()) { + cypherLog.trace("with parameters:{}{}", System.lineSeparator(), parameters); + } + } return Mono.fromSupplier(cypherSupplier).zipWith(Mono.just(parameters.get())); } @@ -248,49 +241,33 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { @Override public Mono one() { - return Mono.usingWhen( - getStatementRunner(targetDatabase), - runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).singleOrEmpty(), - DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError, - DefaultReactiveNeo4jClient::asyncCancel - ); + return doInStatementRunnerForMono( + targetDatabase, + (runner) -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).singleOrEmpty()); } @Override public Mono first() { - return Mono.usingWhen( - getStatementRunner(targetDatabase), - runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).next(), - DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError, - DefaultReactiveNeo4jClient::asyncCancel - ); + return doInStatementRunnerForMono( + targetDatabase, + runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).next()); } @Override public Flux all() { - return Flux.usingWhen( - getStatementRunner(targetDatabase), - runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)), - DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError, - DefaultReactiveNeo4jClient::asyncCancel + return doInStatementRunnerForFlux( + targetDatabase, + runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)) ); } Mono run() { - return Mono - .usingWhen( - getStatementRunner(targetDatabase), - runner -> prepareStatement().flatMap(t -> Mono.from(runner.run(t.getT1(), t.getT2()).summary())), - DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError, - DefaultReactiveNeo4jClient::asyncCancel - ); + return doInStatementRunnerForMono( + targetDatabase, + runner -> prepareStatement().flatMap(t -> Mono.from(runner.run(t.getT1(), t.getT2()).summary()))); } } @@ -311,12 +288,11 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { @Override public Mono run() { - return Mono.usingWhen( - getStatementRunner(targetDatabase), - callback::apply, - DefaultReactiveNeo4jClient::asyncComplete, - DefaultReactiveNeo4jClient::asyncError, - DefaultReactiveNeo4jClient::asyncCancel); + return doInStatementRunnerForMono( + targetDatabase, + callback + ); + } } @@ -346,4 +322,30 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } } } + + final class RxStatementRunnerHolder { + private final RxStatementRunner rxStatementRunner; + + private final Publisher commit; + private final Publisher rollback; + + RxStatementRunnerHolder(RxStatementRunner rxStatementRunner, Publisher commit, Publisher rollback) { + this.rxStatementRunner = rxStatementRunner; + this.commit = commit; + this.rollback = rollback; + } + + public RxStatementRunner getRxStatementRunner() { + return rxStatementRunner; + } + + public Publisher getCommit() { + return commit; + } + + public Publisher getRollback() { + return rollback; + } + } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java index 65c91d040..3534e9195 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java @@ -34,6 +34,7 @@ import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; /** * Definition of a modern Neo4j client. @@ -62,7 +63,7 @@ public interface Neo4jClient { * @param cypher The cypher code that shall be executed * @return A new CypherSpec */ - RunnableSpec newQuery(String cypher); + RunnableSpec query(String cypher); /** * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at this point whether it's a match, @@ -72,7 +73,7 @@ public interface Neo4jClient { * @param cypherSupplier A supplier of arbitrary Cypher code * @return */ - RunnableSpec newQuery(Supplier cypherSupplier); + RunnableSpec query(Supplier cypherSupplier); /** * Delegates interaction with the default database to the given callback. @@ -147,7 +148,7 @@ public interface Neo4jClient { * @param value The value to bind to a query * @return An ongoing bind spec for specifying the name that {@code value} should be bound to or a binder function */ - OngoingBindSpec bind(T value); + OngoingBindSpec bind(@Nullable T value); S bindAll(Map parameters); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java index 4b417071c..15fedc8f3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java @@ -29,6 +29,8 @@ import org.apiguardian.api.API; import org.neo4j.driver.Driver; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.summary.ResultSummary; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.neo4j.core.Neo4jClient.BindSpec; import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; @@ -45,6 +47,8 @@ import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; @API(status = API.Status.STABLE, since = "1.0") public interface ReactiveNeo4jClient { + Logger cypherLog = LoggerFactory.getLogger("org.springframework.data.neo4j.cypher"); + static ReactiveNeo4jClient create(Driver driver) { return new DefaultReactiveNeo4jClient(driver); @@ -57,7 +61,7 @@ public interface ReactiveNeo4jClient { * @param cypher The cypher code that shall be executed * @return A new CypherSpec */ - ReactiveRunnableSpec newQuery(String cypher); + ReactiveRunnableSpec query(String cypher); /** * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at this point whether it's a match, @@ -67,7 +71,7 @@ public interface ReactiveNeo4jClient { * @param cypherSupplier A supplier of arbitrary Cypher code * @return */ - ReactiveRunnableSpec newQuery(Supplier cypherSupplier); + ReactiveRunnableSpec query(Supplier cypherSupplier); /** * Delegates interaction with the default database to the given callback. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java index 048f25579..d45a6fe50 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java @@ -27,11 +27,11 @@ import org.springframework.transaction.support.TransactionSynchronization; * participating in a non-native Neo4j transaction, such as a Jta transaction. */ class Neo4jSessionSynchronization - extends ResourceHolderSynchronization { + extends ResourceHolderSynchronization { - private final Neo4jConnectionHolder localConnectionHolder; + private final Neo4jTransactionHolder localConnectionHolder; - Neo4jSessionSynchronization(Neo4jConnectionHolder connectionHolder, Driver driver) { + Neo4jSessionSynchronization(Neo4jTransactionHolder connectionHolder, Driver driver) { super(connectionHolder, driver); this.localConnectionHolder = connectionHolder; @@ -51,7 +51,7 @@ class Neo4jSessionSynchronization * @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object) */ @Override - protected void processResourceAfterCommit(Neo4jConnectionHolder resourceHolder) { + protected void processResourceAfterCommit(Neo4jTransactionHolder resourceHolder) { super.processResourceAfterCommit(resourceHolder); @@ -79,7 +79,7 @@ class Neo4jSessionSynchronization * @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object) */ @Override - protected void releaseResource(Neo4jConnectionHolder resourceHolder, Object resourceKey) { + protected void releaseResource(Neo4jTransactionHolder resourceHolder, Object resourceKey) { if (resourceHolder.hasActiveSession()) { resourceHolder.close(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java similarity index 80% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java index a56319d33..5781e9195 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionHolder.java @@ -18,10 +18,10 @@ */ package org.springframework.data.neo4j.core.transaction; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; -import org.neo4j.driver.TransactionConfig; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; @@ -33,23 +33,17 @@ import org.springframework.util.Assert; * * @author Michael J. Simons */ -class Neo4jConnectionHolder extends ResourceHolderSupport { +class Neo4jTransactionHolder extends ResourceHolderSupport { private final String databaseName; - private final Session session; - private final Transaction transaction; - Neo4jConnectionHolder(String databaseName, Session session) { - this(databaseName, session, TransactionConfig.empty()); - } - - Neo4jConnectionHolder(String databaseName, Session session, TransactionConfig transactionConfig) { + Neo4jTransactionHolder(String databaseName, Session session, Transaction transaction) { this.databaseName = databaseName; this.session = session; - this.transaction = this.session.beginTransaction(transactionConfig); + this.transaction = transaction; } /** @@ -63,11 +57,6 @@ class Neo4jConnectionHolder extends ResourceHolderSupport { return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? transaction : null; } - static boolean namesMapToTheSameDatabase(@Nullable String name1, @Nullable String name2) { - - return name1 == null && name2 == null || (name1 != null && name1.equals(name2)); - } - void commit() { Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed."); @@ -97,7 +86,9 @@ class Neo4jConnectionHolder extends ResourceHolderSupport { @Override public void setRollbackOnly() { + super.setRollbackOnly(); + transaction.failure(); } @Override diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java index 05fd789ea..ac95080a2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -18,20 +18,21 @@ */ package org.springframework.data.neo4j.core.transaction; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + import lombok.extern.slf4j.Slf4j; -import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; import org.springframework.lang.Nullable; -import org.springframework.transaction.IllegalTransactionStateException; -import org.springframework.transaction.InvalidIsolationLevelException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionSystemException; @@ -44,8 +45,7 @@ import org.springframework.util.Assert; /** * Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. This - * transaction manager will synchronize a pair of a native Neo4j session/transaction and one instance of a - * {@link org.springframework.data.neo4j.core.NodeManager} with the transaction. + * transaction manager will synchronize a pair of a native Neo4j session/transaction with the transaction. * * @author Michael J. Simons */ @@ -71,10 +71,64 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager this.databaseName = databaseName; } + /** + * This methods provides a native Neo4j transaction to be used from within a {@link org.springframework.data.neo4j.core.Neo4jClient}. + * In most cases this the native transaction will be controlled from the Neo4j specific + * {@link org.springframework.transaction.PlatformTransactionManager}. However, SDN-RX provides support for other + * transaction managers as well. This methods registers a session synchronization in such cases on the foreign transaction manager. + * + * @param driver The driver that has been used as a synchronization object. + * @param targetDatabase The target database + * @return An optional containing a managed transaction or an empty optional if the method hasn't been called inside + * an ongoing Spring transaction + */ + public static Optional retrieveTransaction(final Driver driver, final String targetDatabase) { + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + return Optional.empty(); + } + + // Check whether we have a transaction managed by a Neo4j transaction manager + Neo4jTransactionHolder connectionHolder = (Neo4jTransactionHolder) TransactionSynchronizationManager + .getResource(driver); + + if (connectionHolder != null) { + + return Optional.of(connectionHolder.getTransaction(targetDatabase)); + } + + // Otherwise we open a session and synchronize it. + Session session = driver.session(defaultSessionParameters(targetDatabase)); + Transaction transaction = session.beginTransaction(TransactionConfig.empty()); + // Manually create a new synchronization + connectionHolder = new Neo4jTransactionHolder(targetDatabase, session, transaction); + connectionHolder.setSynchronizedWithTransaction(true); + + TransactionSynchronizationManager.registerSynchronization( + new Neo4jSessionSynchronization(connectionHolder, driver)); + + TransactionSynchronizationManager.bindResource(driver, connectionHolder); + return Optional.of(connectionHolder.getTransaction(targetDatabase)); + } + + private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) { + + Assert.isInstanceOf(Neo4jTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", Neo4jTransactionObject.class, + transaction.getClass())); + + return (Neo4jTransactionObject) transaction; + } + + private static Neo4jTransactionObject extractNeo4jTransaction(DefaultTransactionStatus status) { + + return extractNeo4jTransaction(status.getTransaction()); + } + @Override protected Object doGetTransaction() throws TransactionException { - Neo4jConnectionHolder resourceHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager + Neo4jTransactionHolder resourceHolder = (Neo4jTransactionHolder) TransactionSynchronizationManager .getResource(driver); return new Neo4jTransactionObject(resourceHolder); } @@ -100,15 +154,15 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager try { Session session = this.driver .session(t -> t.withDefaultAccessMode(accessMode).withBookmarks(bookmarks).withDatabase(databaseName)); + Transaction nativeTransaction = session.beginTransaction(transactionConfig); - Neo4jConnectionHolder connectionHolder = new Neo4jConnectionHolder(databaseName, session, transactionConfig); - connectionHolder.setSynchronizedWithTransaction(true); - transactionObject.setResourceHolder(connectionHolder); - TransactionSynchronizationManager.bindResource(this.driver, connectionHolder); + Neo4jTransactionHolder transactionHolder = + new Neo4jTransactionHolder(databaseName, session, nativeTransaction); + transactionHolder.setSynchronizedWithTransaction(true); + transactionObject.setResourceHolder(transactionHolder); + TransactionSynchronizationManager.bindResource(this.driver, transactionHolder); } catch (Exception ex) { - ex.printStackTrace(); throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage())); - } } @@ -157,39 +211,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager TransactionSynchronizationManager.unbindResource(driver); } - private static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) { - - if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { - throw new InvalidIsolationLevelException( - "Neo4jTransactionManager is not allowed to support custom isolation levels."); - } - - if (definition.getPropagationBehavior() != TransactionDefinition.PROPAGATION_REQUIRED) { - throw new IllegalTransactionStateException("Neo4jTransactionManager only supports 'required' propagation."); - } - - TransactionConfig.Builder builder = TransactionConfig.builder(); - if (definition.getTimeout() > 0) { - builder = builder.withTimeout(Duration.ofSeconds(definition.getTimeout())); - } - - return builder.build(); - } - - private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) { - - Assert.isInstanceOf(Neo4jTransactionObject.class, transaction, - () -> String.format("Expected to find a %s but it turned out to be %s.", Neo4jTransactionObject.class, - transaction.getClass())); - - return (Neo4jTransactionObject) transaction; - } - - private static Neo4jTransactionObject extractNeo4jTransaction(DefaultTransactionStatus status) { - - return extractNeo4jTransaction(status.getTransaction()); - } - static class Neo4jTransactionObject implements SmartTransactionObject { @@ -199,9 +220,9 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder. // If it is null, there's no existing session / transaction. @Nullable - private Neo4jConnectionHolder resourceHolder; + private Neo4jTransactionHolder resourceHolder; - Neo4jTransactionObject(@Nullable Neo4jConnectionHolder resourceHolder) { + Neo4jTransactionObject(@Nullable Neo4jTransactionHolder resourceHolder) { this.resourceHolder = resourceHolder; } @@ -211,18 +232,18 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager * * @param resourceHolder A newly created resource holder with a fresh drivers session, */ - void setResourceHolder(@Nullable Neo4jConnectionHolder resourceHolder) { + void setResourceHolder(@Nullable Neo4jTransactionHolder resourceHolder) { this.resourceHolder = resourceHolder; } /** - * @return {@literal true} if a {@link Neo4jConnectionHolder} is set. + * @return {@literal true} if a {@link Neo4jTransactionHolder} is set. */ boolean hasResourceHolder() { return resourceHolder != null; } - Neo4jConnectionHolder getRequiredResourceHolder() { + Neo4jTransactionHolder getRequiredResourceHolder() { Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); return resourceHolder; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java index 3bbc7484f..7cb765b99 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtils.java @@ -18,19 +18,17 @@ */ package org.springframework.data.neo4j.core.transaction; -import reactor.core.publisher.Mono; - +import java.time.Duration; import java.util.Collections; -import java.util.Optional; import java.util.function.Consumer; import org.neo4j.driver.AccessMode; -import org.neo4j.driver.Driver; import org.neo4j.driver.SessionParametersTemplate; -import org.neo4j.driver.Transaction; -import org.neo4j.driver.reactive.RxTransaction; +import org.neo4j.driver.TransactionConfig; import org.springframework.lang.Nullable; -import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; /** * Internal use only. @@ -55,54 +53,38 @@ public final class Neo4jTransactionUtils { } /** - * A hook into Springs transaction management to provide managed, native transactions. + * Maps a Spring {@link TransactionDefinition transaction definition} to a native Neo4j driver transaction. + * Only the default isolation leven ({@link TransactionDefinition#ISOLATION_DEFAULT}) and + * {@link TransactionDefinition#PROPAGATION_REQUIRED propagation required} behaviour are supported. * - * @param driver The driver that has been used as a synchronization object. - * @param targetDatabase The target database - * @return An optional containing a managed transaction or an empty optional if the method hasn't been called inside - * an ongoing Spring transaction + * @param definition The transaction definition passed to a Neo4j transaction manager + * @return A Neo4j native transaction configuration */ - public static Optional retrieveTransaction(final Driver driver, final String targetDatabase) { + static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) { - if (!TransactionSynchronizationManager.isSynchronizationActive()) { - return Optional.empty(); + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + throw new InvalidIsolationLevelException( + "Neo4jTransactionManager is not allowed to support custom isolation levels."); } - // Try existing transaction - Neo4jConnectionHolder connectionHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager - .getResource(driver); - - if (connectionHolder != null) { - - return Optional.of(connectionHolder.getTransaction(targetDatabase)); + if (definition.getPropagationBehavior() != TransactionDefinition.PROPAGATION_REQUIRED) { + throw new IllegalTransactionStateException("Neo4jTransactionManager only supports 'required' propagation."); } - // Manually create a new synchronization - connectionHolder = new Neo4jConnectionHolder(targetDatabase, - driver.session(defaultSessionParameters(targetDatabase))); - connectionHolder.setSynchronizedWithTransaction(true); + TransactionConfig.Builder builder = TransactionConfig.builder(); + if (definition.getTimeout() > 0) { + builder = builder.withTimeout(Duration.ofSeconds(definition.getTimeout())); + } - TransactionSynchronizationManager.registerSynchronization( - new Neo4jSessionSynchronization(connectionHolder, driver)); - - TransactionSynchronizationManager.bindResource(driver, connectionHolder); - return Optional.of(connectionHolder.getTransaction(targetDatabase)); + return builder.build(); } - /** - * A stub to retrieve a reactive transaction that may is already synchronized with Springs context. - * - * @param driver The driver that has been used as a synchronization object. - * @param targetDatabase The target database - * @return A Mono containing a managed transaction or an empty Mono if the method hasn't been called inside - * an ongoing Spring transaction - */ - public static Mono retrieveReactiveTransaction(final Driver driver, final String targetDatabase) { + static boolean namesMapToTheSameDatabase(@Nullable String name1, @Nullable String name2) { - // TODO Hook into Springs reactive transaction management (Coming 5.2 M2) - return Mono.empty(); + return name1 == null && name2 == null || (name1 != null && name1.equals(name2)); } + private Neo4jTransactionUtils() { } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jSessionSynchronization.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jSessionSynchronization.java new file mode 100644 index 000000000..784d27cef --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jSessionSynchronization.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.transaction; + +import reactor.core.publisher.Mono; + +import org.neo4j.driver.Driver; +import org.springframework.transaction.reactive.ReactiveResourceSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + */ +class ReactiveNeo4jSessionSynchronization extends ReactiveResourceSynchronization { + + private final ReactiveNeo4jTransactionHolder transactionHolder; + + ReactiveNeo4jSessionSynchronization(TransactionSynchronizationManager transactionSynchronizationManager, + ReactiveNeo4jTransactionHolder transactionHolder, Driver driver) { + + super(transactionHolder, driver, transactionSynchronizationManager); + + this.transactionHolder = transactionHolder; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#shouldReleaseBeforeCompletion() + */ + @Override + protected boolean shouldReleaseBeforeCompletion() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#processResourceAfterCommit(java.lang.Object) + */ + @Override + protected Mono processResourceAfterCommit(ReactiveNeo4jTransactionHolder resourceHolder) { + return Mono.defer(() -> super.processResourceAfterCommit(resourceHolder).then(resourceHolder.commit())); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#afterCompletion(int) + */ + @Override + public Mono afterCompletion(int status) { + return Mono.defer(() -> { + if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { + return transactionHolder.rollback().then(super.afterCompletion(status)); + } + return super.afterCompletion(status); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#releaseResource(java.lang.Object, java.lang.Object) + */ + @Override + protected Mono releaseResource(ReactiveNeo4jTransactionHolder resourceHolder, Object resourceKey) { + return Mono.defer(() -> Mono.from(resourceHolder.getSession().close())); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java new file mode 100644 index 000000000..f8244aad1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionHolder.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.transaction; + +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + +import reactor.core.publisher.Mono; + +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxTransaction; +import org.springframework.transaction.support.ResourceHolderSupport; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + */ +class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport { + + private final RxSession session; + private final String databaseName; + private final RxTransaction transaction; + + ReactiveNeo4jTransactionHolder(String databaseName, RxSession session, RxTransaction transaction) { + + this.session = session; + this.databaseName = databaseName; + this.transaction = transaction; + } + + RxSession getSession() { + return session; + } + + RxTransaction getTransaction(String inDatabase) { + + return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? transaction : null; + } + + Mono commit() { + + return Mono.from(transaction.commit()); + } + + Mono rollback() { + + return Mono.from(transaction.rollback()); + } + + Mono close() { + + return Mono.from(session.close()); + } + + @Override + public void setRollbackOnly() { + + super.setRollbackOnly(); + } + + @Override + public void resetRollbackOnly() { + + throw new UnsupportedOperationException(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java new file mode 100644 index 000000000..1290d2129 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManager.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.transaction; + +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; + +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.List; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxTransaction; +import org.springframework.lang.Nullable; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; +import org.springframework.transaction.reactive.GenericReactiveTransaction; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; + +/** + * @author Gerrit Meier + */ +public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransactionManager { + + /** + * The underlying driver, which is also the synchronisation object. + */ + private final Driver driver; + /** + * The name of the target database. + */ + private final String databaseName; + + public ReactiveNeo4jTransactionManager(Driver driver) { + this(driver, null); + } + + public ReactiveNeo4jTransactionManager(Driver driver, String databaseName) { + this.driver = driver; + this.databaseName = databaseName; + } + + public static Mono retrieveReactiveTransaction(final Driver driver, final String targetDatabase) { + + return TransactionSynchronizationManager.currentTransaction() // Do we have a Transaction context? + // Bail out early if synchronization between transaction managers is not active + .filter(TransactionSynchronizationManager::isSynchronizationActive) + .flatMap(tsm -> { + // Get an existing holder + ReactiveNeo4jTransactionHolder existingTxHolder = (ReactiveNeo4jTransactionHolder) tsm + .getResource(driver); + + // And use it if there is any + if (existingTxHolder != null) { + return Mono.just(existingTxHolder); + } + + // Otherwise open up a new native transaction + return Mono.defer(() -> { + RxSession session = driver.rxSession(defaultSessionParameters(targetDatabase)); + return Mono.from(session.beginTransaction(TransactionConfig.empty())).map(tx -> { + + ReactiveNeo4jTransactionHolder newConnectionHolder = new ReactiveNeo4jTransactionHolder( + targetDatabase, session, tx); + newConnectionHolder.setSynchronizedWithTransaction(true); + + tsm.registerSynchronization( + new ReactiveNeo4jSessionSynchronization(tsm, newConnectionHolder, driver)); + + tsm.bindResource(driver, newConnectionHolder); + return newConnectionHolder; + }); + }); + }) + .map(connectionHolder -> connectionHolder.getTransaction(targetDatabase)) + // If not, than just don't open a transaction + .onErrorResume(NoTransactionException.class, nte -> Mono.empty()); + } + + private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(Object transaction) { + + Assert.isInstanceOf(ReactiveNeo4jTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", ReactiveNeo4jTransactionObject.class, + transaction.getClass())); + + return (ReactiveNeo4jTransactionObject) transaction; + } + + private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(GenericReactiveTransaction status) { + return extractNeo4jTransaction(status.getTransaction()); + } + + @Override + protected Object doGetTransaction(TransactionSynchronizationManager transactionSynchronizationManager) + throws TransactionException { + + ReactiveNeo4jTransactionHolder resourceHolder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager + .getResource(driver); + return new ReactiveNeo4jTransactionObject(resourceHolder); + } + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#isExistingTransaction(Object) + */ + + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + + return extractNeo4jTransaction(transaction).hasResourceHolder(); + } + + @Override + protected Mono doBegin(TransactionSynchronizationManager transactionSynchronizationManager, Object transaction, + TransactionDefinition transactionDefinition) throws TransactionException { + + return Mono.defer(() -> { + + ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); + + TransactionConfig transactionConfig = createTransactionConfigFrom(transactionDefinition); + boolean readOnly = transactionDefinition.isReadOnly(); + AccessMode accessMode = readOnly ? AccessMode.READ : AccessMode.WRITE; + List bookmarks = Collections.emptyList(); // TODO Bookmarksupport + + transactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); + + RxSession session = this.driver + .rxSession(t -> t.withDefaultAccessMode(accessMode).withBookmarks(bookmarks).withDatabase(databaseName)); + + return Mono.from(session.beginTransaction(transactionConfig)).doOnNext(nativeTransaction -> { + + ReactiveNeo4jTransactionHolder transactionHolder = + new ReactiveNeo4jTransactionHolder(databaseName, session, nativeTransaction); + transactionHolder.setSynchronizedWithTransaction(true); + transactionObject.setResourceHolder(transactionHolder); + transactionSynchronizationManager.bindResource(this.driver, transactionHolder); + }); + + }).then(); + } + + @Override + protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager synchronizationManager, + Object transaction) { + + ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); + + return Mono.defer(() -> transactionObject.getRequiredResourceHolder().close() + .doOnNext(ignoreMe -> synchronizationManager.unbindResource(driver))); + } + + @Override + protected Mono doCommit(TransactionSynchronizationManager transactionSynchronizationManager, + GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { + + ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager + .getResource(driver); + return Mono.from(holder.getTransaction(databaseName).commit()).then(); + } + + @Override + protected Mono doRollback(TransactionSynchronizationManager transactionSynchronizationManager, + GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { + + ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager + .getResource(driver); + return Mono.from(holder.getTransaction(databaseName).rollback()).then(); + } + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + + @Override + protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction status) throws TransactionException { + + return Mono.fromRunnable(() -> { + ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(status); + transactionObject.getRequiredResourceHolder().setRollbackOnly(); + }); + } + + static class ReactiveNeo4jTransactionObject implements SmartTransactionObject { + + private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jConnectionHolder is required but not present. o_O"; + + // The resource holder is null when the call to TransactionSynchronizationManager.getResource + // in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder. + // If it is null, there's no existing session / transaction. + @Nullable private ReactiveNeo4jTransactionHolder resourceHolder; + + ReactiveNeo4jTransactionObject(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * Usually called in {@link #doBegin(TransactionSynchronizationManager, Object, TransactionDefinition)} which is + * called when there's no existing transaction. + * + * @param resourceHolder A newly created resource holder with a fresh drivers session, + */ + void setResourceHolder(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * @return {@literal true} if a {@link Neo4jTransactionHolder} is set. + */ + boolean hasResourceHolder() { + return resourceHolder != null; + } + + ReactiveNeo4jTransactionHolder getRequiredResourceHolder() { + + Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE); + return resourceHolder; + } + + void setRollbackOnly() { + + getRequiredResourceHolder().setRollbackOnly(); + } + + @Override + public boolean isRollbackOnly() { + return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly(); + } + + @Override + public void flush() { + + TransactionSynchronizationUtils.triggerFlush(); + } + } + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 81f3e3716..cb4160681 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -114,7 +114,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository public S save(S entity) { Long internalId = neo4jClient - .newQuery(() -> renderer.render(statementBuilder.prepareSaveOf(entityMetaData))) + .query(() -> renderer.render(statementBuilder.prepareSaveOf(entityMetaData))) .bind((T) entity) .with(entityInformation.getBinderFunction()) .fetchAs(Long.class).one().get(); @@ -193,7 +193,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository Statement statement = statementBuilder .prepareDeleteOf(entityMetaData, Optional.of(condition)); - ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)) + ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) .bind(id).to(nameOfParameter) .run(); @@ -221,7 +221,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository log.debug("Deleting all entities with the following ids: {} ", ids); Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); - ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)) + ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) .bind(ids).to(nameOfParameter) .run(); @@ -235,7 +235,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository log.debug("Deleting all nodes with primary label {}", entityMetaData.getPrimaryLabel()); Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.empty()); - ResultSummary summary = this.neo4jClient.newQuery(renderer.render(statement)).run(); + ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).run(); log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java index eeb2282be..9f480de43 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java @@ -124,7 +124,7 @@ class Neo4jClientTest { "RETURN b"; Collection> usedBikes = client - .newQuery(cypher) + .query(cypher) .bind("michael").to("name") .bindAll(parameters) .bind(LocalDate.of(2019, 1, 1)).to("aDate") @@ -159,7 +159,7 @@ class Neo4jClientTest { String cypher = "MATCH (u:User) WHERE u.name =~ $name"; Optional> firstMatchingUser = client - .newQuery(cypher) + .query(cypher) .in("bikingDatabase") .bind("Someone.*").to("name") .fetch() @@ -234,7 +234,7 @@ class Neo4jClientTest { BikeOwnerReader mappingFunction = new BikeOwnerReader(); Collection bikeOwners = client - .newQuery(cypher) + .query(cypher) .bind("michael").to("name") .fetchAs(BikeOwner.class).mappedBy(mappingFunction) .all(); @@ -263,7 +263,7 @@ class Neo4jClientTest { Neo4jClient client = Neo4jClient.create(driver); assertThatIllegalStateException().isThrownBy(() -> client - .newQuery("MATCH (n) RETURN n") + .query("MATCH (n) RETURN n") .fetchAs(BikeOwner.class).mappedBy((t, r) -> { if (r == record1) { return new BikeOwner(r.get("name").asString(), Collections.emptyList()); @@ -295,7 +295,7 @@ class Neo4jClientTest { + "MERGE (b:Bike {name: bike}) " + "MERGE (u) - [o:OWNS] -> (b) "; ResultSummary summary = client - .newQuery(cypher) + .query(cypher) .bind(michael).with(new BikeOwnerBinder()) .run(); @@ -323,7 +323,7 @@ class Neo4jClientTest { String cypher = "MATCH (b:Bike) RETURN count(b)"; Optional numberOfBikes = client - .newQuery(cypher) + .query(cypher) .fetchAs(Long.class) .one(); @@ -350,7 +350,7 @@ class Neo4jClientTest { String cypher = "DETACH DELETE (b) WHERE name = $name"; client - .newQuery(cypher) + .query(cypher) .bind("fixie").to("name") .run(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java index c0dad2ab2..a633c6a0d 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java @@ -113,7 +113,6 @@ class ReactiveNeo4jClientTest { verifyNoMoreInteractions(driver, session, transaction, statementResult, resultSummary, record1, record2); } - @Test @DisplayName("Creation of queries and binding parameters should feel natural") void queryCreationShouldFeelGood() { @@ -135,7 +134,7 @@ class ReactiveNeo4jClientTest { "RETURN b"; Flux> usedBikes = client - .newQuery(cypher) + .query(cypher) .bind("michael").to("name") .bindAll(parameters) .bind(LocalDate.of(2019, 1, 1)).to("aDate") @@ -158,6 +157,7 @@ class ReactiveNeo4jClientTest { verify(record1).asMap(); verify(record2).asMap(); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); } @@ -173,7 +173,7 @@ class ReactiveNeo4jClientTest { String cypher = "MATCH (u:User) WHERE u.name =~ $name"; Mono> firstMatchingUser = client - .newQuery(cypher) + .query(cypher) .in("bikingDatabase") .bind("Someone.*").to("name") .fetch() @@ -192,6 +192,7 @@ class ReactiveNeo4jClientTest { verify(statementResult).records(); verify(record1).asMap(); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); } @@ -216,6 +217,7 @@ class ReactiveNeo4jClientTest { verifyDatabaseSelection(null); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); } @@ -238,6 +240,7 @@ class ReactiveNeo4jClientTest { verifyDatabaseSelection("aDatabase"); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); } } @@ -261,7 +264,7 @@ class ReactiveNeo4jClientTest { BikeOwnerReader mappingFunction = new BikeOwnerReader(); Flux bikeOwners = client - .newQuery(cypher) + .query(cypher) .bind("michael").to("name") .fetchAs(BikeOwner.class).mappedBy(mappingFunction) .all(); @@ -279,6 +282,7 @@ class ReactiveNeo4jClientTest { verify(statementResult).records(); verify(record1).get("name"); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); } @@ -292,7 +296,7 @@ class ReactiveNeo4jClientTest { ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); Flux bikeOwners = client - .newQuery("MATCH (n) RETURN n") + .query("MATCH (n) RETURN n") .fetchAs(BikeOwner.class).mappedBy((t, r) -> { if (r == record1) { return new BikeOwner(r.get("name").asString(), Collections.emptyList()); @@ -311,6 +315,7 @@ class ReactiveNeo4jClientTest { verify(transaction).run(eq("MATCH (n) RETURN n"), argThat(new MapAssertionMatcher(Collections.emptyMap()))); verify(statementResult).records(); verify(record1).get("name"); + verify(transaction).commit(); verify(transaction).rollback(); verify(session).close(); } @@ -331,7 +336,7 @@ class ReactiveNeo4jClientTest { + "MERGE (u) - [o:OWNS] -> (b) "; Mono summary = client - .newQuery(cypher) + .query(cypher) .bind(michael).with(new BikeOwnerBinder()) .run(); @@ -347,6 +352,7 @@ class ReactiveNeo4jClientTest { verify(transaction).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).summary(); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); } @@ -364,7 +370,7 @@ class ReactiveNeo4jClientTest { String cypher = "MATCH (b:Bike) RETURN count(b)"; Mono numberOfBikes = client - .newQuery(cypher) + .query(cypher) .fetchAs(Long.class) .one(); @@ -376,6 +382,7 @@ class ReactiveNeo4jClientTest { verify(transaction).run(eq(cypher), anyMap()); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); } } @@ -393,7 +400,7 @@ class ReactiveNeo4jClientTest { String cypher = "DETACH DELETE (b) WHERE name = $name"; Mono deletionResult = client - .newQuery(cypher) + .query(cypher) .bind("fixie").to("name") .run(); @@ -409,6 +416,7 @@ class ReactiveNeo4jClientTest { verify(transaction).run(eq(cypher), argThat(new MapAssertionMatcher(expectedParameters))); verify(statementResult).summary(); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java index 9101c8327..e841ba737 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java @@ -29,6 +29,7 @@ import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,7 +43,6 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.internal.SessionParameters; import org.neo4j.driver.reactive.RxSession; -import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; @@ -165,39 +165,13 @@ class TransactionHandlingTest { private RxTransaction transaction; @Test + @Disabled public void shouldNotOpenTransactionsWithoutSubscription() { - DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); - - @SuppressWarnings("unused") - Mono runner = neo4jClient.getStatementRunner("aDatabase"); - verify(driver, never()).rxSession(any(Consumer.class)); verifyZeroInteractions(driver, session); } - // TODO The same like shouldNotOpenTransactionsWithoutSubscription but with managed transactions - - @Test - public void shouldFallbackToImplicitTransaction() { - - when(driver.rxSession(any(Consumer.class))).thenReturn(session); - when(session.beginTransaction()).thenReturn(Mono.just(transaction)); - - DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); - - @SuppressWarnings("unused") - Mono runner = neo4jClient.getStatementRunner("aDatabase"); - - StepVerifier.create(runner) - .expectNextCount(1L) - .verifyComplete(); - - verify(driver).rxSession(any(Consumer.class)); - verify(session).close(); - verifyZeroInteractions(driver, session, transaction); - } - @Test public void shouldCloseUnmanagedSessionOnComplete() { @@ -208,16 +182,16 @@ class TransactionHandlingTest { DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); - Mono sequence = Mono - .usingWhen(neo4jClient.getStatementRunner("aDatabase"), r -> Mono.just("A node"), - DefaultReactiveNeo4jClient::asyncComplete, DefaultReactiveNeo4jClient::asyncError); + Mono sequence = neo4jClient.doInStatementRunnerForMono("aDatabase", tx -> Mono.just("1")); StepVerifier.create(sequence) - .expectNext("A node") + .expectNext("1") .verifyComplete(); verify(driver).rxSession(any(Consumer.class)); + verify(session).beginTransaction(); verify(transaction).commit(); + verify(transaction).rollback(); verify(session).close(); verifyZeroInteractions(driver, session, transaction); } @@ -225,6 +199,7 @@ class TransactionHandlingTest { @Test public void shouldCloseUnmanagedSessionOnError() { + when(driver.rxSession(any(Consumer.class))).thenReturn(session); when(session.beginTransaction()).thenReturn(Mono.just(transaction)); when(transaction.rollback()).thenReturn(Mono.empty()); @@ -232,18 +207,22 @@ class TransactionHandlingTest { DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); - Mono sequence = Mono - .usingWhen(neo4jClient.getStatementRunner("aDatabase"), r -> Mono.error(new RuntimeException()), - DefaultReactiveNeo4jClient::asyncComplete, DefaultReactiveNeo4jClient::asyncError); + Mono sequence = neo4jClient + .doInStatementRunnerForMono("aDatabase", tx -> Mono.error(new SomeException())); StepVerifier.create(sequence) - .expectError() + .expectError(SomeException.class) .verify(); verify(driver).rxSession(any(Consumer.class)); + verify(session).beginTransaction(); + verify(transaction).commit(); verify(transaction).rollback(); verify(session).close(); verifyZeroInteractions(driver, session, transaction); } } + + static class SomeException extends RuntimeException { + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java deleted file mode 100644 index a93874494..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jConnectionHolderTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2019 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed 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 - * - * https://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 org.springframework.data.neo4j.core.transaction; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -/** - * @author Michael J. Simons - */ -class Neo4jConnectionHolderTest { - - @Nested - class DatabaseNameComparision { - - @ParameterizedTest - @CsvSource({ - ",", - "a,a" - }) - void nameComparisionShouldWorkForNamesTargetingTheSame(String name1, String name2) { - - assertThat(Neo4jConnectionHolder.namesMapToTheSameDatabase(name1, name2)).isTrue(); - } - - @ParameterizedTest - @CsvSource({ - "a,", - ",b", - "a,b" - }) - void nameComparisionShouldWorkForNamesTargetingOther(String name1, String name2) { - - assertThat(Neo4jConnectionHolder.namesMapToTheSameDatabase(name1, name2)).isFalse(); - } - } -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java index 96bb8c647..5673ee669 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java @@ -18,10 +18,20 @@ */ package org.springframework.data.neo4j.core.transaction; +import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager.*; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -34,7 +44,11 @@ import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; /** * @author Michael J. Simons @@ -42,6 +56,8 @@ import org.springframework.transaction.support.DefaultTransactionDefinition; @ExtendWith(MockitoExtension.class) class Neo4jTransactionManagerTest { + private String databaseName = "aDatabase"; + @Mock private Driver driver; @Mock @@ -52,9 +68,21 @@ class Neo4jTransactionManagerTest { private Transaction transaction; @Mock private StatementResult statementResult; + @Mock + private UserTransaction userTransaction; + @Test - public void triggerCommitCorrectly() { + void shouldWorkWithoutSynchronizations() { + Optional optionalTransaction = retrieveTransaction(driver, databaseName); + + assertThat(optionalTransaction).isEmpty(); + + verifyZeroInteractions(driver, session, transaction); + } + + @Test + void triggerCommitCorrectly() { when(driver.defaultTypeSystem()).thenReturn(typeSystem); when(driver.session(any(Consumer.class))).thenReturn(session); @@ -67,7 +95,7 @@ class Neo4jTransactionManagerTest { TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); Neo4jClient client = Neo4jClient.create(driver); - client.newQuery("RETURN 1").run(); + client.query("RETURN 1").run(); txManager.commit(txStatus); @@ -82,4 +110,204 @@ class Neo4jTransactionManagerTest { verify(session).close(); } + + @Nested + class TransactionParticipation { + + @BeforeEach + void setUp() { + + AtomicBoolean sessionIsOpen = new AtomicBoolean(true); + AtomicBoolean transactionIsOpen = new AtomicBoolean(true); + + when(driver.session(any(Consumer.class))).thenReturn(session); + + when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); + doAnswer(invocation -> { + sessionIsOpen.set(false); + return null; + }).when(session).close(); + when(session.isOpen()).thenAnswer(invocation -> sessionIsOpen.get()); + + doAnswer(invocation -> { + transactionIsOpen.set(false); + return null; + }).when(transaction).close(); + when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get()); + } + + @AfterEach + void verifyTransactionSynchronizationManagerState() { + + assertThat(TransactionSynchronizationManager.getResourceMap().isEmpty()).isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + assertThat(TransactionSynchronizationManager.getCurrentTransactionName()).isNull(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel()).isNull(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + + @Nested + class BasedOnNeo4jTransactions { + + @Test + void shouldUseTxFromNeo4jTxManager() { + + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver, databaseName); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + + Optional optionalTransaction = retrieveTransaction(driver, databaseName); + assertThat(optionalTransaction).isPresent(); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(driver).session(any(Consumer.class)); + + verify(session).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(2)).isOpen(); + verify(transaction).failure(); + verify(transaction).close(); + } + + @Test + void shouldParticipateInOngoingTransaction() { + + Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver, databaseName); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { + + Optional outerNativeTransaction = retrieveTransaction(driver, databaseName); + assertThat(outerNativeTransaction).isPresent(); + assertThat(outerStatus.isNewTransaction()).isTrue(); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus innerStatus) { + + assertThat(innerStatus.isNewTransaction()).isFalse(); + + Optional innerNativeTransaction = retrieveTransaction(driver, databaseName); + assertThat(innerNativeTransaction).isPresent(); + } + }); + + outerStatus.setRollbackOnly(); + } + }); + + verify(driver).session(any(Consumer.class)); + + verify(session).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(2)).isOpen(); + verify(transaction).failure(); + verify(transaction).close(); + } + + } + + @Nested + class BasedOnJtaTransactions { + + @Test + void shouldParticipateInOngoingTransactionWithCommit() throws Exception { + + when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); + + Optional nativeTransaction = retrieveTransaction(driver, databaseName); + + assertThat(nativeTransaction).isPresent(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + } + }); + + verify(userTransaction).begin(); + + verify(driver).session(any(Consumer.class)); + + verify(session, times(2)).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(3)).isOpen(); + verify(transaction).success(); + verify(transaction).close(); + } + + @Test + void shouldParticipateInOngoingTransactionWithRollback() throws Exception { + + when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); + + Optional nativeTransaction = retrieveTransaction(driver, databaseName); + + assertThat(nativeTransaction).isPresent(); + assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(userTransaction).begin(); + verify(userTransaction).rollback(); + + verify(driver).session(any(Consumer.class)); + + verify(session, times(2)).isOpen(); + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + + verify(transaction, times(3)).isOpen(); + verify(transaction).failure(); + verify(transaction).close(); + } + } + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java index c5690a6f1..508a4156b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java @@ -19,33 +19,15 @@ package org.springframework.data.neo4j.core.transaction; import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - -import javax.transaction.Status; -import javax.transaction.UserTransaction; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.Transaction; -import org.neo4j.driver.TransactionConfig; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.jta.JtaTransactionManager; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionSynchronizationManager; -import org.springframework.transaction.support.TransactionTemplate; /** * @author Michael J. Simons @@ -54,218 +36,29 @@ import org.springframework.transaction.support.TransactionTemplate; @MockitoSettings(strictness = Strictness.LENIENT) class Neo4jTransactionUtilsTest { - private String databaseName = "aDatabase"; - - @Mock - private Driver driver; - @Mock - private Session session; - @Mock - private Transaction transaction; - @Mock - UserTransaction userTransaction; - - @BeforeEach - void setUp() { - - AtomicBoolean sessionIsOpen = new AtomicBoolean(true); - AtomicBoolean transactionIsOpen = new AtomicBoolean(true); - - when(driver.session(any(Consumer.class))).thenReturn(session); - - when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); - doAnswer(invocation -> { - sessionIsOpen.set(false); - return null; - }).when(session).close(); - when(session.isOpen()).thenAnswer(invocation -> sessionIsOpen.get()); - - doAnswer(invocation -> { - transactionIsOpen.set(false); - return null; - }).when(transaction).close(); - when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get()); - } - - @Test - void shouldWorkWithoutSynchronizations() { - Optional optionalTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); - - assertThat(optionalTransaction).isEmpty(); - - verifyZeroInteractions(driver, session, transaction); - } - @Nested - class BasedOnNeo4jTransactions { - @Test - void shouldOpenNewTransaction() { + class DatabaseNameComparision { - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver, databaseName); - TransactionTemplate txTemplate = new TransactionTemplate(txManager); + @ParameterizedTest + @CsvSource({ + ",", + "a,a" + }) + void nameComparisionShouldWorkForNamesTargetingTheSame(String name1, String name2) { - txTemplate.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { - - assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); - assertThat(transactionStatus.isNewTransaction()).isTrue(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); - - Optional optionalTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); - assertThat(optionalTransaction).isPresent(); - - transactionStatus.setRollbackOnly(); - } - }); - - verify(driver).session(any(Consumer.class)); - - verify(session).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - - verify(transaction, times(2)).isOpen(); - verify(transaction).failure(); - verify(transaction).close(); + assertThat(namesMapToTheSameDatabase(name1, name2)).isTrue(); } - @Test - void shouldParticipateInOngoingTransaction() { + @ParameterizedTest + @CsvSource({ + "a,", + ",b", + "a,b" + }) + void nameComparisionShouldWorkForNamesTargetingOther(String name1, String name2) { - Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver, databaseName); - TransactionTemplate txTemplate = new TransactionTemplate(txManager); - - txTemplate.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { - - Optional outerNativeTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); - assertThat(outerNativeTransaction).isPresent(); - assertThat(outerStatus.isNewTransaction()).isTrue(); - - txTemplate.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus innerStatus) { - - assertThat(innerStatus.isNewTransaction()).isFalse(); - - Optional innerNativeTransaction = Neo4jTransactionUtils - .retrieveTransaction(driver, databaseName); - assertThat(innerNativeTransaction).isPresent(); - } - }); - - outerStatus.setRollbackOnly(); - } - }); - - verify(driver).session(any(Consumer.class)); - - verify(session).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - - verify(transaction, times(2)).isOpen(); - verify(transaction).failure(); - verify(transaction).close(); + assertThat(namesMapToTheSameDatabase(name1, name2)).isFalse(); } } - @Nested - class BasedOnJtaTransactions { - - @Test - void shouldParticipateInOngoingTransactionWithCommit() throws Exception { - - when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, - Status.STATUS_ACTIVE); - - JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); - TransactionTemplate txTemplate = new TransactionTemplate(txManager); - - txTemplate.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { - - assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); - assertThat(transactionStatus.isNewTransaction()).isTrue(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - - Optional nativeTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); - - assertThat(nativeTransaction).isPresent(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); - } - }); - - verify(userTransaction).begin(); - - verify(driver).session(any(Consumer.class)); - - verify(session, times(2)).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - - verify(transaction, times(3)).isOpen(); - verify(transaction).success(); - verify(transaction).close(); - } - - @Test - void shouldParticipateInOngoingTransactionWithRollback() throws Exception { - - when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, - Status.STATUS_ACTIVE); - - JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); - TransactionTemplate txTemplate = new TransactionTemplate(txManager); - - txTemplate.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { - - assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); - assertThat(transactionStatus.isNewTransaction()).isTrue(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - - Optional nativeTransaction = Neo4jTransactionUtils.retrieveTransaction(driver, databaseName); - - assertThat(nativeTransaction).isPresent(); - assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); - - transactionStatus.setRollbackOnly(); - } - }); - - verify(userTransaction).begin(); - verify(userTransaction).rollback(); - - verify(driver).session(any(Consumer.class)); - - verify(session, times(2)).isOpen(); - verify(session).beginTransaction(any(TransactionConfig.class)); - verify(session).close(); - - verify(transaction, times(3)).isOpen(); - verify(transaction).failure(); - verify(transaction).close(); - } - } - - @AfterEach - void verifyTransactionSynchronizationManagerState() { - - assertThat(TransactionSynchronizationManager.getResourceMap().isEmpty()).isTrue(); - assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); - assertThat(TransactionSynchronizationManager.getCurrentTransactionName()).isNull(); - assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); - assertThat(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel()).isNull(); - assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); - } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java new file mode 100644 index 000000000..c8dc98688 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.springframework.data.neo4j.core.transaction; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager.*; + +import io.r2dbc.h2.H2ConnectionConfiguration; +import io.r2dbc.h2.H2ConnectionFactory; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.function.Consumer; + +import javax.transaction.UserTransaction; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.neo4j.driver.Driver; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxTransaction; +import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.reactive.TransactionalOperator; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ReactiveNeo4jTransactionManagerTest { + + private String databaseName = "aDatabase"; + + @Mock + private Driver driver; + + @Mock + private RxSession session; + @Mock + private RxTransaction transaction; + @Mock + UserTransaction userTransaction; + + @BeforeEach + void setUp() { + + when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(Mono.just(transaction)); + when(transaction.rollback()).thenReturn(Mono.empty()); + when(transaction.commit()).thenReturn(Mono.empty()); + when(session.close()).thenReturn(Mono.empty()); + } + + @Test + void shouldWorkWithoutSynchronizations() { + + Mono transactionMono = retrieveReactiveTransaction(driver, databaseName); + + StepVerifier.create(transactionMono) + .verifyComplete(); + } + + @Nested + class BasedOnNeo4jTransactions { + @Test + void shouldUseTxFromNeo4jTxManager() { + + ReactiveNeo4jTransactionManager txManager = new ReactiveNeo4jTransactionManager(driver, databaseName); + TransactionalOperator transactionalOperator = TransactionalOperator.create(txManager); + + transactionalOperator + .execute(transactionStatus -> TransactionSynchronizationManager + .currentTransaction().doOnNext(tsm -> { + assertThat(tsm.hasResource(driver)).isTrue(); + transactionStatus.setRollbackOnly(); + }).then(retrieveReactiveTransaction(driver, databaseName)) + ) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + verify(driver).rxSession(any(Consumer.class)); + + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + verify(transaction).rollback(); + verify(transaction, never()).commit(); + } + + @Test + void shouldParticipateInOngoingTransaction() { + + ReactiveNeo4jTransactionManager txManager = new ReactiveNeo4jTransactionManager(driver, databaseName); + TransactionalOperator transactionalOperator = TransactionalOperator.create(txManager); + + transactionalOperator + .execute(outerStatus -> { + assertThat(outerStatus.isNewTransaction()).isTrue(); + outerStatus.setRollbackOnly(); + return transactionalOperator.execute(innerStatus -> { + assertThat(innerStatus.isNewTransaction()).isFalse(); + return retrieveReactiveTransaction(driver, databaseName); + }).then(retrieveReactiveTransaction(driver, databaseName)); + }) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + verify(driver).rxSession(any(Consumer.class)); + + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + verify(transaction).rollback(); + verify(transaction, never()).commit(); + } + } + + @Nested + class BasedOnOtherTransactions { + + @Test + void shouldSynchronizeWithExternalWithCommit() { + + R2dbcTransactionManager t = new R2dbcTransactionManager(new H2ConnectionFactory( + H2ConnectionConfiguration.builder().inMemory("test").build())); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(t); + + transactionalOperator + .execute(transactionStatus -> TransactionSynchronizationManager + .currentTransaction().doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isFalse()) + .then(retrieveReactiveTransaction(driver, databaseName)) + .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.currentTransaction() + .doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isTrue())) + ) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + verify(driver).rxSession(any(Consumer.class)); + + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + verify(transaction).commit(); + verify(transaction, never()).rollback(); + } + + @Test + void shouldSynchronizeWithExternalWithRollback() { + + R2dbcTransactionManager t = new R2dbcTransactionManager(new H2ConnectionFactory( + H2ConnectionConfiguration.builder().inMemory("test").build())); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(t); + + transactionalOperator + .execute(transactionStatus -> TransactionSynchronizationManager + .currentTransaction() + .doOnNext(tsm -> { + assertThat(tsm.hasResource(driver)).isFalse(); + transactionStatus.setRollbackOnly(); + }) + .then(retrieveReactiveTransaction(driver, databaseName)) + .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.currentTransaction() + .doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isTrue())) + ) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + verify(driver).rxSession(any(Consumer.class)); + + verify(session).beginTransaction(any(TransactionConfig.class)); + verify(session).close(); + verify(transaction).rollback(); + verify(transaction, never()).commit(); + } + } +} From fe2e90dc669cd77da59888006db7623a68b1ff15 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 4 Jun 2019 13:18:15 +0200 Subject: [PATCH 107/342] Move classes to new neo4j package and change Maven coordinates. According to the new maven artifact coordinates the classes should live in a package that represents the right naming. --- README.adoc | 6 +-- etc/adr/adr-001.adoc | 2 +- etc/jqassistant/naming.adoc | 4 +- etc/jqassistant/structure.adoc | 6 +-- pom.xml | 2 +- spring-data-neo4j/pom.xml | 2 +- .../data}/config/AbstractNeo4jConfig.java | 4 +- .../config/AbstractReactiveNeo4jConfig.java | 4 +- .../config/Neo4jConfigurationSupport.java | 10 ++--- .../ReactiveNeo4jConfigurationSupport.java | 8 ++-- .../data}/core/DefaultNeo4jClient.java | 8 ++-- .../core/DefaultReactiveNeo4jClient.java | 12 +++--- ...elegatingMappingFunctionWithNullCheck.java | 2 +- .../data}/core/NamedParameters.java | 2 +- .../data}/core/Neo4jClient.java | 4 +- .../data}/core/PreparedQuery.java | 2 +- .../data}/core/ReactiveNeo4jClient.java | 10 ++--- .../core/SingleValueMappingFunction.java | 4 +- .../data}/core/cypher/Aliased.java | 2 +- .../data}/core/cypher/AliasedExpression.java | 6 +-- .../data}/core/cypher/Asterisk.java | 2 +- .../core/cypher/BooleanFunctionCondition.java | 4 +- .../data}/core/cypher/BooleanLiteral.java | 2 +- .../data}/core/cypher/Comparison.java | 6 +-- .../data}/core/cypher/CompoundCondition.java | 8 ++-- .../data}/core/cypher/Condition.java | 2 +- .../data}/core/cypher/Conditions.java | 2 +- .../data}/core/cypher/Create.java | 4 +- .../data}/core/cypher/Cypher.java | 14 +++---- .../core/cypher/DefaultStatementBuilder.java | 12 +++--- .../data}/core/cypher/Delete.java | 4 +- .../core/cypher/ExposesRelationships.java | 2 +- .../data}/core/cypher/Expression.java | 6 +-- .../data}/core/cypher/ExpressionList.java | 8 ++-- .../data}/core/cypher/Expressions.java | 2 +- .../data}/core/cypher/FunctionInvocation.java | 4 +- .../data}/core/cypher/Functions.java | 2 +- .../data}/core/cypher/Limit.java | 6 +-- .../data}/core/cypher/ListExpression.java | 4 +- .../data}/core/cypher/ListLiteral.java | 2 +- .../data}/core/cypher/Literal.java | 2 +- .../data}/core/cypher/MapEntry.java | 6 +-- .../data}/core/cypher/MapExpression.java | 4 +- .../data}/core/cypher/Match.java | 6 +-- .../data}/core/cypher/Merge.java | 4 +- .../data}/core/cypher/MultiPartElement.java | 6 +-- .../data}/core/cypher/MultiPartQuery.java | 6 +-- .../data}/core/cypher/Named.java | 2 +- .../data}/core/cypher/NestedExpression.java | 6 +-- .../data}/core/cypher/Node.java | 8 ++-- .../data}/core/cypher/NodeLabel.java | 4 +- .../data}/core/cypher/NodeLabels.java | 6 +-- .../data}/core/cypher/NotCondition.java | 4 +- .../data}/core/cypher/NullLiteral.java | 2 +- .../data}/core/cypher/NumberLiteral.java | 2 +- .../data}/core/cypher/Operation.java | 6 +-- .../data}/core/cypher/Operations.java | 2 +- .../data}/core/cypher/Operator.java | 4 +- .../data}/core/cypher/Order.java | 4 +- .../data}/core/cypher/Parameter.java | 2 +- .../data}/core/cypher/Pattern.java | 4 +- .../data}/core/cypher/PatternElement.java | 4 +- .../data}/core/cypher/Properties.java | 6 +-- .../data}/core/cypher/Property.java | 4 +- .../data}/core/cypher/ReadingClause.java | 4 +- .../data}/core/cypher/Relationship.java | 4 +- .../data}/core/cypher/RelationshipChain.java | 6 +-- .../data}/core/cypher/RelationshipDetail.java | 8 ++-- .../data}/core/cypher/Remove.java | 4 +- .../data}/core/cypher/Return.java | 6 +-- .../data}/core/cypher/ReturnBody.java | 6 +-- .../data}/core/cypher/Set.java | 4 +- .../data}/core/cypher/SinglePartQuery.java | 8 ++-- .../data}/core/cypher/Skip.java | 6 +-- .../data}/core/cypher/SortItem.java | 8 ++-- .../data}/core/cypher/Statement.java | 4 +- .../data}/core/cypher/StatementBuilder.java | 4 +- .../data}/core/cypher/StringLiteral.java | 2 +- .../data}/core/cypher/SymbolicName.java | 2 +- .../data}/core/cypher/UnionPart.java | 8 ++-- .../data}/core/cypher/UnionQuery.java | 6 +-- .../data}/core/cypher/Unwind.java | 4 +- .../data}/core/cypher/UpdatingClause.java | 4 +- .../data}/core/cypher/Where.java | 6 +-- .../data}/core/cypher/With.java | 6 +-- .../data}/core/cypher/package-info.java | 2 +- .../core/cypher/renderer/CypherRenderer.java | 4 +- .../data}/core/cypher/renderer/Renderer.java | 4 +- .../cypher/renderer/RenderingVisitor.java | 10 ++--- .../core/cypher/renderer/package-info.java | 4 ++ .../cypher/support/ReflectiveVisitor.java | 2 +- .../core/cypher/support/TypedSubtree.java | 2 +- .../data}/core/cypher/support/Visitable.java | 2 +- .../data}/core/cypher/support/Visitor.java | 2 +- .../core/cypher/support/package-info.java | 4 ++ .../mapping/DefaultNeo4jBinderFunction.java | 4 +- .../mapping/DefaultNeo4jIsNewStrategy.java | 4 +- .../mapping/DefaultNeo4jMappingFunction.java | 6 +-- .../mapping/DefaultNeo4jPersistentEntity.java | 12 +++--- .../DefaultNeo4jPersistentProperty.java | 6 +-- .../DefaultRelationshipDescription.java | 4 +- .../core/mapping/Neo4jMappingContext.java | 12 +++--- .../core/mapping/Neo4jPersistentEntity.java | 4 +- .../core/mapping/Neo4jPersistentProperty.java | 4 +- .../data}/core/mapping/package-info.java | 2 +- .../core/schema/GraphPropertyDescription.java | 2 +- .../springframework/data}/core/schema/Id.java | 2 +- .../data}/core/schema/IdDescription.java | 2 +- .../data}/core/schema/IdGenerator.java | 2 +- .../data}/core/schema/Neo4jSimpleTypes.java | 2 +- .../data}/core/schema/Node.java | 2 +- .../data}/core/schema/NodeDescription.java | 2 +- .../data}/core/schema/Property.java | 2 +- .../data}/core/schema/Relationship.java | 2 +- .../core/schema/RelationshipDescription.java | 2 +- .../data}/core/schema/Schema.java | 2 +- .../core/schema/UnknownEntityException.java | 2 +- .../data}/core/schema/package-info.java | 2 +- .../Neo4jSessionSynchronization.java | 2 +- .../transaction/Neo4jTransactionHolder.java | 4 +- .../transaction/Neo4jTransactionManager.java | 6 +-- .../transaction/Neo4jTransactionUtils.java | 2 +- .../ReactiveNeo4jSessionSynchronization.java | 2 +- .../ReactiveNeo4jTransactionHolder.java | 4 +- .../ReactiveNeo4jTransactionManager.java | 4 +- .../data}/core/transaction/package-info.java | 2 +- .../data}/repository/Neo4jRepository.java | 2 +- .../data}/repository/NoResultException.java | 2 +- .../repository/ReactiveNeo4jRepository.java | 2 +- .../config/EnableNeo4jRepositories.java | 10 ++--- .../EnableReactiveNeo4jRepositories.java | 10 ++--- .../config/Neo4jRepositoriesRegistrar.java | 2 +- ...Neo4jRepositoryConfigurationExtension.java | 8 ++-- .../ReactiveNeo4jRepositoriesRegistrar.java | 2 +- ...Neo4jRepositoryConfigurationExtension.java | 8 ++-- .../data}/repository/config/package-info.java | 2 +- .../data}/repository/package-info.java | 2 +- .../repository/query/AbstractNeo4jQuery.java | 8 ++-- .../query/AbstractReactiveNeo4jQuery.java | 8 ++-- .../data}/repository/query/CountQuery.java | 2 +- .../repository/query/CypherAdapterUtils.java | 26 ++++++------- .../repository/query/CypherQueryCreator.java | 38 +++++++++---------- .../data}/repository/query/ExistsQuery.java | 2 +- .../repository/query/Neo4jQueryExecution.java | 8 ++-- .../query/Neo4jQueryLookupStrategy.java | 6 +-- .../repository/query/Neo4jQueryMethod.java | 2 +- .../repository/query/PartTreeNeo4jQuery.java | 10 ++--- .../data}/repository/query/Query.java | 2 +- .../ReactiveNeo4jQueryLookupStrategy.java | 6 +-- .../query/ReactivePartTreeNeo4jQuery.java | 10 ++--- .../query/ReactiveStringBasedNeo4jQuery.java | 8 ++-- .../query/StringBasedNeo4jQuery.java | 8 ++-- .../data}/repository/query/package-info.java | 2 +- .../DefaultNeo4jEntityInformation.java | 8 ++-- .../support/Neo4jEntityInformation.java | 8 ++-- .../support/Neo4jRepositoryFactory.java | 16 ++++---- .../support/Neo4jRepositoryFactoryBean.java | 6 +-- .../data}/repository/support/Predicate.java | 28 +++++++------- .../ReactiveNeo4jRepositoryFactory.java | 16 ++++---- .../ReactiveNeo4jRepositoryFactoryBean.java | 6 +-- .../support/SimpleNeo4jRepository.java | 26 ++++++------- .../support/SimpleQueryByExampleExecutor.java | 24 ++++++------ .../SimpleReactiveNeo4jRepository.java | 20 +++++----- .../SimpleReactiveQueryByExampleExecutor.java | 20 +++++----- .../core/cypher/renderer/package-info.java | 4 -- .../core/cypher/support/package-info.java | 4 -- ...atingMappingFunctionWithNullCheckTest.java | 2 +- .../data}/core/NamedParametersTest.java | 2 +- .../data}/core/Neo4jClientTest.java | 2 +- .../data}/core/ReactiveNeo4jClientTest.java | 12 +++--- .../core/SingleValueMappingFunctionTest.java | 2 +- .../data}/core/TransactionHandlingTest.java | 4 +- .../data}/core/cypher/ComparisonTest.java | 2 +- .../data}/core/cypher/CypherIT.java | 10 ++--- .../data}/core/cypher/CypherTest.java | 2 +- .../cypher/DefaultStatementBuilderTest.java | 2 +- .../data}/core/cypher/FunctionsTest.java | 4 +- .../data}/core/cypher/NodeTest.java | 6 +-- .../data}/core/cypher/PropertyTest.java | 2 +- .../data}/core/cypher/RelationshipTest.java | 2 +- .../data}/core/cypher/StringLiteralTest.java | 2 +- .../cypher/renderer/RenderingVisitorTest.java | 2 +- .../DefaultNeo4jIsNewStrategyTest.java | 8 ++-- .../core/mapping/Neo4jMappingContextTest.java | 16 ++++---- .../Neo4jTransactionManagerTest.java | 6 +-- .../Neo4jTransactionUtilsTest.java | 4 +- .../ReactiveNeo4jTransactionManagerTest.java | 4 +- .../imperative/PersonRepository.java | 14 +++---- .../integration/imperative/RepositoryIT.java | 20 +++++----- .../imperative/ThingRepository.java | 4 +- .../reactive/ReactivePersonRepository.java | 8 ++-- .../reactive/ReactiveRepositoryIT.java | 12 +++--- .../reactive/ReactiveThingRepository.java | 4 +- .../shared/PersonWithAllConstructor.java | 8 ++-- .../shared/PersonWithNoConstructor.java | 8 ++-- .../integration/shared/PersonWithWither.java | 6 +-- .../shared/ThingWithAssignedId.java | 8 ++-- .../config/EnableNeo4jRepositoriesTests.java | 2 +- .../repository/query/RepositoryQueryTest.java | 6 +-- .../data}/test/Neo4jExtension.java | 2 +- .../data}/integration/shared/KotlinPerson.kt | 6 +-- .../src/test/resources/logback-test.xml | 4 +- 202 files changed, 573 insertions(+), 573 deletions(-) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/config/AbstractNeo4jConfig.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/config/AbstractReactiveNeo4jConfig.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/config/Neo4jConfigurationSupport.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/config/ReactiveNeo4jConfigurationSupport.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/DefaultNeo4jClient.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/DefaultReactiveNeo4jClient.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/DelegatingMappingFunctionWithNullCheck.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/NamedParameters.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/Neo4jClient.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/PreparedQuery.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/ReactiveNeo4jClient.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/SingleValueMappingFunction.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Aliased.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/AliasedExpression.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Asterisk.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/BooleanFunctionCondition.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/BooleanLiteral.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Comparison.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/CompoundCondition.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Condition.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Conditions.java (99%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Create.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Cypher.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/DefaultStatementBuilder.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Delete.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/ExposesRelationships.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Expression.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/ExpressionList.java (84%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Expressions.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/FunctionInvocation.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Functions.java (99%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Limit.java (88%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/ListExpression.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/ListLiteral.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Literal.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/MapEntry.java (87%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/MapExpression.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Match.java (89%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Merge.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/MultiPartElement.java (89%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/MultiPartQuery.java (89%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Named.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/NestedExpression.java (86%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Node.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/NodeLabel.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/NodeLabels.java (87%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/NotCondition.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/NullLiteral.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/NumberLiteral.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Operation.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Operations.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Operator.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Order.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Parameter.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Pattern.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/PatternElement.java (88%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Properties.java (87%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Property.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/ReadingClause.java (86%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Relationship.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/RelationshipChain.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/RelationshipDetail.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Remove.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Return.java (88%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/ReturnBody.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Set.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/SinglePartQuery.java (88%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Skip.java (88%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/SortItem.java (89%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Statement.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/StatementBuilder.java (99%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/StringLiteral.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/SymbolicName.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/UnionPart.java (84%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/UnionQuery.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Unwind.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/UpdatingClause.java (86%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/Where.java (87%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/With.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/package-info.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/renderer/CypherRenderer.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/renderer/Renderer.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/renderer/RenderingVisitor.java (96%) create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/package-info.java rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/support/ReflectiveVisitor.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/support/TypedSubtree.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/support/Visitable.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/support/Visitor.java (94%) create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/package-info.java rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/DefaultNeo4jBinderFunction.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/DefaultNeo4jIsNewStrategy.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/DefaultNeo4jMappingFunction.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/DefaultNeo4jPersistentEntity.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/DefaultNeo4jPersistentProperty.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/DefaultRelationshipDescription.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/Neo4jMappingContext.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/Neo4jPersistentEntity.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/Neo4jPersistentProperty.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/package-info.java (81%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/GraphPropertyDescription.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/Id.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/IdDescription.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/IdGenerator.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/Neo4jSimpleTypes.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/Node.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/NodeDescription.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/Property.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/Relationship.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/RelationshipDescription.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/Schema.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/UnknownEntityException.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/schema/package-info.java (85%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/Neo4jSessionSynchronization.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/Neo4jTransactionHolder.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/Neo4jTransactionManager.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/Neo4jTransactionUtils.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/ReactiveNeo4jSessionSynchronization.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/ReactiveNeo4jTransactionHolder.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/ReactiveNeo4jTransactionManager.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/package-info.java (70%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/Neo4jRepository.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/NoResultException.java (96%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/ReactiveNeo4jRepository.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/config/EnableNeo4jRepositories.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/config/EnableReactiveNeo4jRepositories.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/config/Neo4jRepositoriesRegistrar.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/config/Neo4jRepositoryConfigurationExtension.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/config/ReactiveNeo4jRepositoriesRegistrar.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/config/package-info.java (69%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/package-info.java (70%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/AbstractNeo4jQuery.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/AbstractReactiveNeo4jQuery.java (94%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/CountQuery.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/CypherAdapterUtils.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/CypherQueryCreator.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/ExistsQuery.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/Neo4jQueryExecution.java (89%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/Neo4jQueryLookupStrategy.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/Neo4jQueryMethod.java (98%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/PartTreeNeo4jQuery.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/Query.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/ReactiveNeo4jQueryLookupStrategy.java (93%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/ReactivePartTreeNeo4jQuery.java (95%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/ReactiveStringBasedNeo4jQuery.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/StringBasedNeo4jQuery.java (97%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/package-info.java (70%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/DefaultNeo4jEntityInformation.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/Neo4jEntityInformation.java (88%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/Neo4jRepositoryFactory.java (89%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/Neo4jRepositoryFactoryBean.java (92%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/Predicate.java (86%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/ReactiveNeo4jRepositoryFactory.java (89%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/ReactiveNeo4jRepositoryFactoryBean.java (91%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/SimpleNeo4jRepository.java (90%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/SimpleQueryByExampleExecutor.java (87%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/SimpleReactiveNeo4jRepository.java (89%) rename spring-data-neo4j/src/main/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/support/SimpleReactiveQueryByExampleExecutor.java (86%) delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/package-info.java delete mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/package-info.java rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/DelegatingMappingFunctionWithNullCheckTest.java (97%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/NamedParametersTest.java (98%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/Neo4jClientTest.java (99%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/ReactiveNeo4jClientTest.java (97%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/SingleValueMappingFunctionTest.java (98%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/TransactionHandlingTest.java (98%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/ComparisonTest.java (96%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/CypherIT.java (99%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/CypherTest.java (97%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/DefaultStatementBuilderTest.java (97%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/FunctionsTest.java (96%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/NodeTest.java (90%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/PropertyTest.java (96%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/RelationshipTest.java (95%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/StringLiteralTest.java (96%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/cypher/renderer/RenderingVisitorTest.java (95%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/DefaultNeo4jIsNewStrategyTest.java (96%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/mapping/Neo4jMappingContextTest.java (91%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/Neo4jTransactionManagerTest.java (98%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/Neo4jTransactionUtilsTest.java (93%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/core/transaction/ReactiveNeo4jTransactionManagerTest.java (98%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/imperative/PersonRepository.java (94%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/imperative/RepositoryIT.java (98%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/imperative/ThingRepository.java (88%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/reactive/ReactivePersonRepository.java (86%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/reactive/ReactiveRepositoryIT.java (97%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/reactive/ReactiveThingRepository.java (89%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/shared/PersonWithAllConstructor.java (86%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/shared/PersonWithNoConstructor.java (79%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/shared/PersonWithWither.java (87%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/shared/ThingWithAssignedId.java (82%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/config/EnableNeo4jRepositoriesTests.java (97%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/repository/query/RepositoryQueryTest.java (98%) rename spring-data-neo4j/src/test/java/org/{springframework/data/neo4j => neo4j/springframework/data}/test/Neo4jExtension.java (98%) rename spring-data-neo4j/src/test/kotlin/org/{springframework/data/neo4j => neo4j/springframework/data}/integration/shared/KotlinPerson.kt (82%) diff --git a/README.adoc b/README.adoc index 50e64fd18..2946cb271 100644 --- a/README.adoc +++ b/README.adoc @@ -42,8 +42,8 @@ That way we we can avoid a compile time dependency to Spring Data and have an in Spring Data JDBC doesn't restrict the supported or scanned classes from Spring Data sides. Our schema should also support non-annotated classes and be smart about naming things, but we will require at least the `@Node` or `@Relationship` annotation to the outside world. -The schema will life independent from Spring classes in `org.springframework.data.neo4j.core.schema`. -Each property of a class that is not identified as a simple type by `org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes` will be considered describing a relationship and thus required to be part of the schema as well. +The schema will life independent from Spring classes in `org.neo4j.springframework.data.core.schema`. +Each property of a class that is not identified as a simple type by `org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes` will be considered describing a relationship and thus required to be part of the schema as well. ==== Context @@ -173,7 +173,7 @@ It is only meant to be a basic for discussions. ---- @startuml note "Implementation of Spring Data Commons SPI" as SDC_note -package "org.springframework.data.neo4j" { +package "org.neo4j.springframework.data" { package "core" { interface Neo4jClient interface ReactiveNeo4jClient diff --git a/etc/adr/adr-001.adoc b/etc/adr/adr-001.adoc index 83c2b3614..e7ee17cc1 100644 --- a/etc/adr/adr-001.adoc +++ b/etc/adr/adr-001.adoc @@ -19,7 +19,7 @@ The strategy will either be `internal`, `assigned` or `generated`. The internal strategy will be the default. -To configure the Id strategy, a meta-annotated `@Id` annotation will be provided through `org.springframework.data.neo4j.core.schema.Id` +To configure the Id strategy, a meta-annotated `@Id` annotation will be provided through `org.neo4j.springframework.data.core.schema.Id` === Consequences diff --git a/etc/jqassistant/naming.adoc b/etc/jqassistant/naming.adoc index b8b760c84..debb0f602 100644 --- a/etc/jqassistant/naming.adoc +++ b/etc/jqassistant/naming.adoc @@ -5,12 +5,12 @@ The following naming conventions are used throughout the project: [[naming:TypeNameMustBeginWithGroupId]] [source,cypher,role=constraint] -.All Java types must be located in packages that start with `org.springframework.data.neo4j`. +.All Java types must be located in packages that start with `org.neo4j.springframework.data`. ---- MATCH (project:Maven:Project)-[:CREATES]->(:Artifact)-[:CONTAINS]->(type:Type) WHERE - NOT type.fqn starts with 'org.springframework.data.neo4j' + NOT type.fqn starts with 'org.neo4j.springframework.data' RETURN project as Project, collect(type) as TypeWithWrongName ---- diff --git a/etc/jqassistant/structure.adoc b/etc/jqassistant/structure.adoc index 495e34abc..43e778592 100644 --- a/etc/jqassistant/structure.adoc +++ b/etc/jqassistant/structure.adoc @@ -1,16 +1,16 @@ [[structure:Default]] [role=group,includesConstraints="structure:mapping"] -Most of the time, the package structure under `org.springframework.data.neo4j` should reflect the main building parts. +Most of the time, the package structure under `org.neo4j.springframework.data` should reflect the main building parts. [[structure:mapping]] [source,cypher,role=constraint,requiresConcepts="dependency:Package"] .The mapping package must not depend on any other SDN-RX packages than `schema` ---- MATCH (a:Main:Artifact) -MATCH (a) -[:CONTAINS]-> (s:Package) WHERE s.fqn = 'org.springframework.data.neo4j.core.schema' +MATCH (a) -[:CONTAINS]-> (s:Package) WHERE s.fqn = 'org.neo4j.springframework.data.core.schema' MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a) -WHERE p1.fqn = 'org.springframework.data.neo4j.core.mapping' +WHERE p1.fqn = 'org.neo4j.springframework.data.core.mapping' AND NOT (p2 = s OR (p1) -[:CONTAINS]-> (p2)) return p1,p2 ---- diff --git a/pom.xml b/pom.xml index 6703afbed..8c840027d 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ 2.2.0.BUILD-SNAPSHOT - org.springframework.data + org.neo4j.springframework.data spring-data-neo4j-rx-parent 1.0.0.BUILD-SNAPSHOT pom diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 31eae3069..41f14fc45 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -22,7 +22,7 @@ spring-data-neo4j-rx-parent - org.springframework.data + org.neo4j.springframework.data 1.0.0.BUILD-SNAPSHOT diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractNeo4jConfig.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractNeo4jConfig.java index b9640bf56..e02e8438d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractNeo4jConfig.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractNeo4jConfig.java @@ -16,13 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.config; +package org.neo4j.springframework.data.config; import org.apiguardian.api.API; import org.neo4j.driver.Driver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.core.Neo4jClient; +import org.neo4j.springframework.data.core.Neo4jClient; /** * Base class for imperative SDN-RX configuration using JavaConfig. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractReactiveNeo4jConfig.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractReactiveNeo4jConfig.java index 754f385d6..89e01a25d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/AbstractReactiveNeo4jConfig.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractReactiveNeo4jConfig.java @@ -16,13 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.config; +package org.neo4j.springframework.data.config; import org.apiguardian.api.API; import org.neo4j.driver.Driver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; /** * Base class for reactive SDN-RX configuration using JavaConfig. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java index 15c497d2c..5919baed9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfigurationSupport.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.config; +package org.neo4j.springframework.data.config; import java.util.Collection; import java.util.Collections; @@ -29,9 +29,9 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.type.filter.AnnotationTypeFilter; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -56,7 +56,7 @@ public abstract class Neo4jConfigurationSupport { public abstract Driver driver(); /** - * Creates a {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} equipped with entity classes + * Creates a {@link org.neo4j.springframework.data.core.mapping.Neo4jMappingContext} equipped with entity classes * scanned from the mapping base package. * * @return diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jConfigurationSupport.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java index 578c7373a..e3601b567 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/ReactiveNeo4jConfigurationSupport.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.config; +package org.neo4j.springframework.data.config; import java.util.Collection; import java.util.Collections; @@ -29,9 +29,9 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.type.filter.AnnotationTypeFilter; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.util.ClassUtils; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java similarity index 98% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java index 2f95c8524..02c0ea2fb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import static java.util.stream.Collectors.*; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager.*; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; +import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager.*; +import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -49,7 +49,7 @@ import org.neo4j.driver.exceptions.NoSuchRecordException; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.neo4j.repository.NoResultException; +import org.neo4j.springframework.data.repository.NoResultException; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java index 857914440..3b3e8e9ed 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; -import static org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager.*; +import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; +import static org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -43,9 +43,9 @@ import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; import org.reactivestreams.Publisher; import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; -import org.springframework.data.neo4j.core.Neo4jClient.OngoingBindSpec; -import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; +import org.neo4j.springframework.data.core.Neo4jClient.MappingSpec; +import org.neo4j.springframework.data.core.Neo4jClient.OngoingBindSpec; +import org.neo4j.springframework.data.core.Neo4jClient.RecordFetchSpec; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java index a7e2569f6..ccb429b2e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheck.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import java.util.function.BiFunction; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NamedParameters.java similarity index 98% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NamedParameters.java index b045a3fcb..e2ed9cd77 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NamedParameters.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NamedParameters.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import java.util.Collections; import java.util.HashMap; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java similarity index 98% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java index 3534e9195..46c655e74 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import java.util.Collection; import java.util.List; @@ -49,7 +49,7 @@ import org.springframework.lang.Nullable; @API(status = API.Status.STABLE, since = "1.0") public interface Neo4jClient { - Logger cypherLog = LoggerFactory.getLogger("org.springframework.data.neo4j.cypher"); + Logger cypherLog = LoggerFactory.getLogger("org.neo4j.springframework.data.cypher"); static Neo4jClient create(Driver driver) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java similarity index 98% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java index ec0c388f4..283256382 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import java.util.Collections; import java.util.HashMap; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java index 15fedc8f3..db95bfe94 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,9 +32,9 @@ import org.neo4j.driver.summary.ResultSummary; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.neo4j.core.Neo4jClient.BindSpec; -import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec; -import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; +import org.neo4j.springframework.data.core.Neo4jClient.BindSpec; +import org.neo4j.springframework.data.core.Neo4jClient.MappingSpec; +import org.neo4j.springframework.data.core.Neo4jClient.RecordFetchSpec; /** * Reactive Neo4j client. The main difference to the {@link Neo4jClient imperative Neo4j client} is the fact that all @@ -47,7 +47,7 @@ import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec; @API(status = API.Status.STABLE, since = "1.0") public interface ReactiveNeo4jClient { - Logger cypherLog = LoggerFactory.getLogger("org.springframework.data.neo4j.cypher"); + Logger cypherLog = LoggerFactory.getLogger("org.neo4j.springframework.data.cypher"); static ReactiveNeo4jClient create(Driver driver) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java index a4ce19154..82a2c5baa 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import lombok.RequiredArgsConstructor; @@ -25,7 +25,7 @@ import java.util.function.BiFunction; import org.neo4j.driver.Record; import org.neo4j.driver.Value; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes; +import org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes; /** * Used to automatically map single valued records to a sensible Java type based on {@link Value#asObject()}. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Aliased.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Aliased.java index 6020b563c..d587bd057 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Aliased.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Aliased.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/AliasedExpression.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/AliasedExpression.java index cdce556d1..2b2c15234 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/AliasedExpression.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/AliasedExpression.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.Expressions.*; +import static org.neo4j.springframework.data.core.cypher.Expressions.*; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Asterisk.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Asterisk.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Asterisk.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Asterisk.java index f0b3f08c6..8f7051bd4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Asterisk.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Asterisk.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanFunctionCondition.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/BooleanFunctionCondition.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanFunctionCondition.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/BooleanFunctionCondition.java index 2001f3804..b93c47050 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanFunctionCondition.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/BooleanFunctionCondition.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * This wraps a function into a condition so that it can be used in a where clause. The function is supposed to return a diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanLiteral.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/BooleanLiteral.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanLiteral.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/BooleanLiteral.java index 1e0469550..321e79535 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/BooleanLiteral.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/BooleanLiteral.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Comparison.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Comparison.java index b80b6e07a..6a2956ae0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Comparison.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Comparison.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.Expressions.*; +import static org.neo4j.springframework.data.core.cypher.Expressions.*; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/CompoundCondition.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/CompoundCondition.java index 19c9f38b4..966f26a75 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/CompoundCondition.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/CompoundCondition.java @@ -16,17 +16,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.Operator.*; +import static org.neo4j.springframework.data.core.cypher.Operator.*; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Condition.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Condition.java index 034de8fb5..62715dd3e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Condition.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Condition.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Conditions.java similarity index 99% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Conditions.java index 02bc45aba..896bb5fc7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Conditions.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Conditions.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Create.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Create.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Create.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Create.java index ba2640ff0..aca578818 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Create.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Create.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See Create. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Cypher.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Cypher.java index ff0a065d0..21f63c676 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Cypher.java @@ -16,18 +16,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.ArrayList; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.ExposesSet; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndWith; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingUnwind; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingUpdate; +import org.neo4j.springframework.data.core.cypher.Statement.SingleQuery; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.ExposesSet; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingAndWith; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingUnwind; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingUpdate; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java similarity index 98% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java index 0fa170dd8..a92e34dfd 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.DefaultStatementBuilder.UpdateType.*; +import static org.neo4j.springframework.data.core.cypher.DefaultStatementBuilder.UpdateType.*; import java.util.ArrayList; import java.util.Arrays; @@ -26,10 +26,10 @@ import java.util.EnumSet; import java.util.List; import java.util.Optional; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReading; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingWithWhere; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReading; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingWithWhere; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; +import org.neo4j.springframework.data.core.cypher.support.Visitable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Delete.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Delete.java index 0f25e353b..e2c8113df 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Delete.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Delete.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See Delete. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExposesRelationships.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExposesRelationships.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExposesRelationships.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExposesRelationships.java index 86ae63b6d..8e683ea6f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExposesRelationships.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExposesRelationships.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Expression.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Expression.java index c9b1efb26..56fa486d1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expression.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Expression.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitable; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExpressionList.java similarity index 84% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExpressionList.java index 04c78961e..cab151675 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ExpressionList.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExpressionList.java @@ -16,14 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.Expressions.*; +import static org.neo4j.springframework.data.core.cypher.Expressions.*; import java.util.List; -import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; +import org.neo4j.springframework.data.core.cypher.support.Visitable; /** * Represents a list of expressions. When visited, the expressions are treated as named expression if they have declared diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Expressions.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Expressions.java index 7de07547b..69a13134a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Expressions.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Expressions.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.Arrays; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/FunctionInvocation.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/FunctionInvocation.java index f738908d4..726ee6485 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/FunctionInvocation.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/FunctionInvocation.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Functions.java similarity index 99% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Functions.java index 4044e89b7..1e2681456 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Functions.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Functions.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Limit.java similarity index 88% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Limit.java index 8fed0dc97..4f9f32ff9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Limit.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListExpression.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ListExpression.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListExpression.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ListExpression.java index 43ae8fc55..6adfde189 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListExpression.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ListExpression.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * Represents a list expression in contrast to an {@link ExpressionList} which is a list of expressions. The list expression diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ListLiteral.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ListLiteral.java index b9ec5c984..d11a33dd0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ListLiteral.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ListLiteral.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static java.util.stream.Collectors.*; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Literal.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Literal.java index cb17367aa..dafb8c833 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Literal.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Literal.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; import org.springframework.lang.Nullable; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapEntry.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapEntry.java similarity index 87% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapEntry.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapEntry.java index aa662acd1..8491447e6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapEntry.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapEntry.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * Helper class, only for internal use. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java index a256de7b2..19a7085de 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MapExpression.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.ArrayList; import java.util.HashSet; @@ -24,7 +24,7 @@ import java.util.List; import java.util.Set; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; +import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Match.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Match.java index 572f44597..1a134d2b0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Match.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Match.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Merge.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Merge.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Merge.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Merge.java index f4c0d0bec..3e33eb02b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Merge.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Merge.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See Create. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartElement.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MultiPartElement.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartElement.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MultiPartElement.java index ea954612d..76188ed81 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartElement.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MultiPartElement.java @@ -16,14 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MultiPartQuery.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MultiPartQuery.java index ecd266084..018f024b4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/MultiPartQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MultiPartQuery.java @@ -16,14 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.ArrayList; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.Statement.SingleQuery; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See MultiPartQuery. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Named.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Named.java index 7e97ede50..cd35c9e0c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Named.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Named.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.Optional; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NestedExpression.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NestedExpression.java similarity index 86% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NestedExpression.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NestedExpression.java index 73a6c2d61..8767b7608 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NestedExpression.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NestedExpression.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.Expressions.*; +import static org.neo4j.springframework.data.core.cypher.Expressions.*; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java index 55b5bb3f2..b5d863444 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Node.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static java.util.stream.Collectors.*; @@ -28,9 +28,9 @@ import java.util.List; import java.util.Optional; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.Relationship.Direction; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.Relationship.Direction; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabel.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabel.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabel.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabel.java index 6055cfecc..b5fe90c4c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabel.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabel.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import lombok.ToString; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitable; /** * Expression for a single Node label. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabels.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabels.java similarity index 87% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabels.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabels.java index 58cc57709..e8db4e9e9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NodeLabels.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabels.java @@ -16,13 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * Makes a list of {@link NodeLabel node labels} visitable. Only used during setting and removal of labels on nodes. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NotCondition.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NotCondition.java index de429eeee..2d855bbad 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NotCondition.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NotCondition.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * A negated version of the condition passed during construnction of this condition. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NullLiteral.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NullLiteral.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NullLiteral.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NullLiteral.java index 49d91ad2a..0abe82cf2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NullLiteral.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NullLiteral.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NumberLiteral.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NumberLiteral.java index 82d848446..ae03e2379 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/NumberLiteral.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NumberLiteral.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operation.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operation.java index 491b3d3da..1a3399e12 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operation.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operation.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static java.util.stream.Collectors.*; @@ -25,8 +25,8 @@ import java.util.EnumSet; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operations.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operations.java index 8a8283c3e..92e88d26e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operations.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operations.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operator.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operator.java index 79dd7e2f9..9d68f6c51 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Operator.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operator.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitable; /** * An operator. See Operators. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Order.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Order.java index fdd85cf66..41aa61a41 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Order.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Order.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; +import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; /** * Represents the list of sort items that make up the order of records in a result set. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Parameter.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Parameter.java index d7b97ce7b..034b0fb6f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Parameter.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Parameter.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java index 2d47ddd60..c6c77891d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Pattern.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; +import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; /** * A pattern is something that can be matched. It consists of one or more pattern elements. Those can be nodes or chains diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/PatternElement.java similarity index 88% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/PatternElement.java index ae1bb0fc1..f1c2749a6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/PatternElement.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/PatternElement.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitable; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Properties.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Properties.java similarity index 87% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Properties.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Properties.java index d6ce3e020..19c131e3f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Properties.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Properties.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * Represents the properties of a {@link Node node} or a {@link Relationship relationship}. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Property.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Property.java index 1727c5e76..cf02767bc 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Property.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ReadingClause.java similarity index 86% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ReadingClause.java index b6a8cef73..5df75ae5f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReadingClause.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ReadingClause.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitable; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java index 4b935423c..ef75a6131 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Relationship.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java @@ -16,13 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.Arrays; import java.util.Optional; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipChain.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/RelationshipChain.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipChain.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/RelationshipChain.java index c1bd645d6..3052ae93a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipChain.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/RelationshipChain.java @@ -16,14 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.support.Visitable.*; +import static org.neo4j.springframework.data.core.cypher.support.Visitable.*; import java.util.LinkedList; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/RelationshipDetail.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/RelationshipDetail.java index c6c806f4e..47f37b030 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/RelationshipDetail.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/RelationshipDetail.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.ArrayList; import java.util.Arrays; @@ -25,9 +25,9 @@ import java.util.List; import java.util.Optional; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.Relationship.Direction; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.Relationship.Direction; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Remove.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Remove.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Remove.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Remove.java index 7c022e8d9..62573a2b3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Remove.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Remove.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See Remove. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Return.java similarity index 88% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Return.java index 5ff082331..140a7f8e8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Return.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Return.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See Return. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ReturnBody.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ReturnBody.java index ab436502e..8faae703b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/ReturnBody.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ReturnBody.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Set.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Set.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Set.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Set.java index 9ea43d204..3119a6d4e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Set.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Set.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See Set. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SinglePartQuery.java similarity index 88% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SinglePartQuery.java index c7412f1dc..de4198621 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SinglePartQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SinglePartQuery.java @@ -16,15 +16,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import java.util.ArrayList; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.Statement.SingleQuery; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Skip.java similarity index 88% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Skip.java index 0f71fd6d3..ff7601223 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Skip.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SortItem.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SortItem.java index f47b4f78d..b9722e9e7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SortItem.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SortItem.java @@ -16,15 +16,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.Expressions.*; +import static org.neo4j.springframework.data.core.cypher.Expressions.*; import java.util.Optional; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java index e09ee53a2..2d9b77981 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Statement.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitable; /** * Shall be the common interfaces for queries that we support. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java similarity index 99% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java index 6fbb32cc6..06eae145d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import static org.springframework.data.neo4j.core.cypher.Expressions.*; +import static org.neo4j.springframework.data.core.cypher.Expressions.*; import org.apiguardian.api.API; import org.springframework.lang.Nullable; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StringLiteral.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StringLiteral.java index 6929b73fd..5775d1e81 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StringLiteral.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StringLiteral.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static java.util.regex.Pattern.*; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java index ffe393fff..69466d6f1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/SymbolicName.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import lombok.ToString; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionPart.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UnionPart.java similarity index 84% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionPart.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UnionPart.java index 91564d340..e69034502 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionPart.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UnionPart.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.Statement.SingleQuery; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.Statement.SingleQuery; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * Represents a part of an union. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UnionQuery.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UnionQuery.java index abea662a8..c47530d6c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UnionQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UnionQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static java.util.stream.Collectors.*; @@ -24,8 +24,8 @@ import java.util.ArrayList; import java.util.List; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.Statement.RegularQuery; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.Statement.RegularQuery; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.util.Assert; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Unwind.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Unwind.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Unwind.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Unwind.java index c2a131378..e596c5598 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Unwind.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Unwind.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * See Unwind. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UpdatingClause.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UpdatingClause.java similarity index 86% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UpdatingClause.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UpdatingClause.java index f72fe8ea7..6c4e73247 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/UpdatingClause.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/UpdatingClause.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitable; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Where.java similarity index 87% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Where.java index cb8f13579..85099a434 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Where.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Where.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * Roughly corresponding to Where. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/With.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/With.java index c575464a6..517b5e67a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/With.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/With.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; import org.springframework.lang.Nullable; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/package-info.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/package-info.java index f3b665534..abba5c1bb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/package-info.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/package-info.java @@ -12,6 +12,6 @@ * @author Michael J. Simons */ @NonNullApi -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/CypherRenderer.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/CypherRenderer.java index 0d069ccf5..2979d0f80 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/CypherRenderer.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/CypherRenderer.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.renderer; +package org.neo4j.springframework.data.core.cypher.renderer; import java.util.LinkedHashMap; import java.util.Map; @@ -25,7 +25,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.springframework.data.neo4j.core.cypher.Statement; +import org.neo4j.springframework.data.core.cypher.Statement; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/Renderer.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/Renderer.java index 7543aa998..73cb6af1e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/Renderer.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/Renderer.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.renderer; +package org.neo4j.springframework.data.core.cypher.renderer; -import org.springframework.data.neo4j.core.cypher.Statement; +import org.neo4j.springframework.data.core.cypher.Statement; /** * Instances of this class are supposed to be threadsafe. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitor.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitor.java index d8c05671d..ff63278d8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.renderer; +package org.neo4j.springframework.data.core.cypher.renderer; import static java.util.stream.Collectors.*; @@ -26,10 +26,10 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.data.neo4j.core.cypher.*; -import org.springframework.data.neo4j.core.cypher.support.ReflectiveVisitor; -import org.springframework.data.neo4j.core.cypher.support.TypedSubtree; -import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.*; +import org.neo4j.springframework.data.core.cypher.support.ReflectiveVisitor; +import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; +import org.neo4j.springframework.data.core.cypher.support.Visitable; import org.springframework.lang.Nullable; /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/package-info.java new file mode 100644 index 000000000..112ea0d51 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/package-info.java @@ -0,0 +1,4 @@ +/** + * A renderer for {@link org.neo4j.springframework.data.core.cypher.Statement statements} in the simplest form possible. + */ +package org.neo4j.springframework.data.core.cypher.renderer; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java similarity index 98% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java index 593da6856..0137e80a6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/ReflectiveVisitor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.support; +package org.neo4j.springframework.data.core.cypher.support; import lombok.EqualsAndHashCode; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/TypedSubtree.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/TypedSubtree.java index eb43976dc..92ec9d234 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/TypedSubtree.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/TypedSubtree.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.support; +package org.neo4j.springframework.data.core.cypher.support; import java.util.ArrayList; import java.util.Arrays; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitable.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitable.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitable.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitable.java index 3a6f1f53f..cee50b70d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitable.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitable.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.support; +package org.neo4j.springframework.data.core.cypher.support; import org.springframework.lang.Nullable; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitor.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitor.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitor.java index 85e269354..b647400db 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/Visitor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.support; +package org.neo4j.springframework.data.core.cypher.support; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/package-info.java new file mode 100644 index 000000000..cd60dc367 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/package-info.java @@ -0,0 +1,4 @@ +/** + * Support for describing an abstract syntax tree. + */ +package org.neo4j.springframework.data.core.cypher.support; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jBinderFunction.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jBinderFunction.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java index 399e6d373..b62d85d2a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jBinderFunction.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.neo4j.springframework.data.core.mapping; -import static org.springframework.data.neo4j.core.schema.NodeDescription.*; +import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java index 5ac9fd556..ed07ecd3d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategy.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.neo4j.springframework.data.core.mapping; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -24,7 +24,7 @@ import lombok.extern.slf4j.Slf4j; import java.util.function.Function; -import org.springframework.data.neo4j.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Id; import org.springframework.data.support.IsNewStrategy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java similarity index 96% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java index 75deaef12..260546414 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.neo4j.springframework.data.core.mapping; import static java.util.stream.Collectors.*; @@ -38,11 +38,11 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.model.ParameterValueProvider; -import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.neo4j.springframework.data.core.schema.NodeDescription; /** * The central logic of mapping Neo4j's {@link org.neo4j.driver.Record records} to entities based on the Spring - * implementation of the {@link org.springframework.data.neo4j.core.schema.Schema}, + * implementation of the {@link org.neo4j.springframework.data.core.schema.Schema}, * represented by the {@link Neo4jMappingContext}. * * @author Gerrit Meier diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java index b43baffdb..26337f9b6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.neo4j.springframework.data.core.mapping; import java.util.ArrayList; import java.util.Collection; @@ -26,11 +26,11 @@ import java.util.Optional; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mapping.model.BasicPersistentEntity; -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.IdDescription; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Property; +import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.IdDescription; +import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.core.schema.Property; import org.springframework.data.support.IsNewStrategy; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java index 3f2f0a58f..bb88227e8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.neo4j.springframework.data.core.mapping; import java.util.Optional; @@ -81,8 +81,8 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class; /** - * Configures the name of the {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected. + * Configures the name of the {@link org.neo4j.springframework.data.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected. */ String neo4jMappingContextRef() default DEFAULT_MAPPING_CONTEXT_BEAN_NAME; /** - * Configures the name of the {@link org.springframework.data.neo4j.core.Neo4jClient} bean to be used with the repositories detected. + * Configures the name of the {@link org.neo4j.springframework.data.core.Neo4jClient} bean to be used with the repositories detected. */ String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_NAME; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java index a51c85fd5..3a0269e1a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableReactiveNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.config; +package org.neo4j.springframework.data.repository.config; -import static org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension.*; +import static org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension.*; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -31,7 +31,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.data.neo4j.repository.support.ReactiveNeo4jRepositoryFactoryBean; +import org.neo4j.springframework.data.repository.support.ReactiveNeo4jRepositoryFactoryBean; /** * Annotation to activate reactive Neo4j repositories. If no base package is configured through either {@link #value()}, @@ -76,12 +76,12 @@ public @interface EnableReactiveNeo4jRepositories { Class repositoryFactoryBeanClass() default ReactiveNeo4jRepositoryFactoryBean.class; /** - * Configures the name of the {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected. + * Configures the name of the {@link org.neo4j.springframework.data.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected. */ String neo4jMappingContextRef() default DEFAULT_MAPPING_CONTEXT_BEAN_NAME; /** - * Configures the name of the {@link org.springframework.data.neo4j.core.Neo4jClient} bean to be used with the repositories detected. + * Configures the name of the {@link org.neo4j.springframework.data.core.Neo4jClient} bean to be used with the repositories detected. */ String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_NAME; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoriesRegistrar.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoriesRegistrar.java index 533ba4d8c..bbb90de83 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoriesRegistrar.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoriesRegistrar.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.config; +package org.neo4j.springframework.data.repository.config; import java.lang.annotation.Annotation; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java index bcd9bc16c..16ecd9a27 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.config; +package org.neo4j.springframework.data.repository.config; import java.lang.annotation.Annotation; import java.util.Collection; @@ -24,9 +24,9 @@ import java.util.Collections; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; +import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.repository.Neo4jRepository; +import org.neo4j.springframework.data.repository.support.Neo4jRepositoryFactoryBean; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoriesRegistrar.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoriesRegistrar.java index 04a2cfb0f..d7783c1b0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoriesRegistrar.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoriesRegistrar.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.config; +package org.neo4j.springframework.data.repository.config; import java.lang.annotation.Annotation; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java index bfbb942f8..d13f40371 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.config; +package org.neo4j.springframework.data.repository.config; import java.lang.annotation.Annotation; import java.util.Collection; @@ -24,9 +24,9 @@ import java.util.Collections; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; +import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.repository.Neo4jRepository; +import org.neo4j.springframework.data.repository.support.Neo4jRepositoryFactoryBean; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/package-info.java similarity index 69% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/package-info.java index 0cd9cd83a..de61b211e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/package-info.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/package-info.java @@ -2,6 +2,6 @@ * Configuration infrastructure for Neo4j specific repositories. */ @NonNullApi -package org.springframework.data.neo4j.repository.config; +package org.neo4j.springframework.data.repository.config; import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/package-info.java similarity index 70% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/package-info.java index d3fc29f62..2b184ba74 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/package-info.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/package-info.java @@ -2,6 +2,6 @@ * This package contains the repository infrastructure. */ @NonNullApi -package org.springframework.data.neo4j.repository; +package org.neo4j.springframework.data.repository; import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java index 8455771a2..f6dfb9331 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import java.util.HashMap; import java.util.Map; @@ -25,9 +25,9 @@ import org.springframework.data.domain.Range; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractReactiveNeo4jQuery.java similarity index 94% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractReactiveNeo4jQuery.java index fb7310977..ead581d1d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractReactiveNeo4jQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import java.util.HashMap; import java.util.Map; @@ -25,9 +25,9 @@ import org.springframework.data.domain.Range; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CountQuery.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CountQuery.java index aa9f204c1..2c3d63b28 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CountQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CountQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java index 1b3e2adde..2cbf80151 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.core.schema.NodeDescription.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import java.util.ArrayList; import java.util.List; @@ -30,18 +30,18 @@ import org.apiguardian.api.API; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.MappingException; -import org.springframework.data.neo4j.core.cypher.*; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableStatement; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndWith; -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.IdDescription; -import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.Schema; -import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; +import org.neo4j.springframework.data.core.cypher.*; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.BuildableStatement; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingAndWith; +import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.IdDescription; +import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.neo4j.springframework.data.core.schema.Schema; +import org.neo4j.springframework.data.repository.support.Neo4jEntityInformation; /** - * A set of shared utils to adapt {@code org.springframework.data.neo4j.core.cypher} functionality with Spring Data + * A set of shared utils to adapt {@code org.neo4j.springframework.data.core.cypher} functionality with Spring Data * and vice versa. * * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java index 0eb30770e..f8154cf8d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java @@ -16,14 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import static lombok.AccessLevel.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.core.cypher.Functions.*; -import static org.springframework.data.neo4j.core.schema.NodeDescription.*; -import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; -import static org.springframework.data.neo4j.repository.query.PartTreeNeo4jQuery.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.core.cypher.Functions.*; +import static org.neo4j.springframework.data.core.schema.NodeDescription.*; +import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; +import static org.neo4j.springframework.data.repository.query.PartTreeNeo4jQuery.*; import lombok.RequiredArgsConstructor; import lombok.ToString; @@ -42,19 +42,19 @@ import org.springframework.data.domain.Sort; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.neo4j.core.cypher.Condition; -import org.springframework.data.neo4j.core.cypher.Conditions; -import org.springframework.data.neo4j.core.cypher.Cypher; -import org.springframework.data.neo4j.core.cypher.Expression; -import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.SortItem; -import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.renderer.Renderer; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; -import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameter; -import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.neo4j.springframework.data.core.cypher.Condition; +import org.neo4j.springframework.data.core.cypher.Conditions; +import org.neo4j.springframework.data.core.cypher.Cypher; +import org.neo4j.springframework.data.core.cypher.Expression; +import org.neo4j.springframework.data.core.cypher.Functions; +import org.neo4j.springframework.data.core.cypher.SortItem; +import org.neo4j.springframework.data.core.cypher.Statement; +import org.neo4j.springframework.data.core.cypher.renderer.Renderer; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty; +import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.neo4j.springframework.data.repository.query.Neo4jQueryMethod.Neo4jParameter; +import org.neo4j.springframework.data.repository.query.Neo4jQueryMethod.Neo4jParameters; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.parser.AbstractQueryCreator; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ExistsQuery.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ExistsQuery.java index 58c5e2daf..61e888be4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ExistsQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ExistsQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryExecution.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryExecution.java index ffce33218..fd360ce7a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryExecution.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryExecution.java @@ -16,13 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import lombok.RequiredArgsConstructor; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.PreparedQuery; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.PreparedQuery; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; /** * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryLookupStrategy.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryLookupStrategy.java index f3300d364..293f4fe5b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryLookupStrategy.java @@ -16,15 +16,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import lombok.RequiredArgsConstructor; import java.lang.reflect.Method; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryMethod.java similarity index 98% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryMethod.java index d333dff47..187f12ccd 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryMethod.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryMethod.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import static java.lang.String.*; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java index c49016940..6cf50354e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import static java.util.stream.Collectors.*; @@ -35,10 +35,10 @@ import java.util.Map; import java.util.Set; import org.neo4j.driver.types.Point; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Query.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Query.java index 6036a3e6d..c1b775789 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/Query.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Query.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveNeo4jQueryLookupStrategy.java similarity index 93% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveNeo4jQueryLookupStrategy.java index 3e2cd8e89..090e32934 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveNeo4jQueryLookupStrategy.java @@ -16,15 +16,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import lombok.RequiredArgsConstructor; import java.lang.reflect.Method; import org.apiguardian.api.API; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java similarity index 95% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java index e61c164c1..afc53077b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import static java.util.stream.Collectors.*; @@ -35,10 +35,10 @@ import java.util.Map; import java.util.Set; import org.neo4j.driver.types.Point; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java index 9bc8a50a8..48c760d13 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -16,16 +16,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import java.util.HashMap; import java.util.Map; import java.util.Optional; import org.springframework.data.mapping.MappingException; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java similarity index 97% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java index 7f011f1c7..bc3fb1006 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java @@ -16,16 +16,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import java.util.HashMap; import java.util.Map; import java.util.Optional; import org.springframework.data.mapping.MappingException; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.PreparedQuery; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/package-info.java similarity index 70% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/package-info.java index c70426963..dc80f4330 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/package-info.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/package-info.java @@ -2,6 +2,6 @@ * A set of annotations for providing custom queries to repositories. */ @NonNullApi -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/DefaultNeo4jEntityInformation.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/DefaultNeo4jEntityInformation.java index ed7734146..1dd360434 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/DefaultNeo4jEntityInformation.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/DefaultNeo4jEntityInformation.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import java.util.Map; import java.util.function.BiFunction; @@ -24,9 +24,9 @@ import java.util.function.Function; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.cypher.Expression; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import org.springframework.data.neo4j.repository.query.CypherAdapterUtils; +import org.neo4j.springframework.data.core.cypher.Expression; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; +import org.neo4j.springframework.data.repository.query.CypherAdapterUtils; import org.springframework.data.repository.core.support.PersistentEntityInformation; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java similarity index 88% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java index 5774af456..3b1ff541e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEntityInformation.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import java.util.Map; import java.util.function.BiFunction; @@ -24,8 +24,8 @@ import java.util.function.Function; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.cypher.Expression; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.neo4j.springframework.data.core.cypher.Expression; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.core.EntityInformation; /** @@ -54,7 +54,7 @@ public interface Neo4jEntityInformation extends EntityInformation /** * @return A mapping function that creates instances of {@code T} for the given record. - * @see org.springframework.data.neo4j.core.schema.Schema#getMappingFunctionFor(Class) + * @see org.neo4j.springframework.data.core.schema.Schema#getMappingFunctionFor(Class) */ BiFunction getMappingFunction(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java index 107488c79..a5421d713 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; -import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; +import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import java.util.Map; import java.util.Optional; @@ -27,12 +27,12 @@ import java.util.function.Function; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.query.CypherAdapterUtils; -import org.springframework.data.neo4j.repository.query.Neo4jQueryLookupStrategy; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; +import org.neo4j.springframework.data.repository.Neo4jRepository; +import org.neo4j.springframework.data.repository.query.CypherAdapterUtils; +import org.neo4j.springframework.data.repository.query.Neo4jQueryLookupStrategy; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java similarity index 92% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java index 8b853649e..fddb201c8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import java.io.Serializable; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java similarity index 86% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java index 84464570a..d9b0d60fe 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/Predicate.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import static lombok.AccessLevel.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.core.schema.NodeDescription.*; -import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.core.schema.NodeDescription.*; +import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,16 +35,16 @@ import java.util.function.BiFunction; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; -import org.springframework.data.neo4j.core.cypher.Condition; -import org.springframework.data.neo4j.core.cypher.Conditions; -import org.springframework.data.neo4j.core.cypher.Cypher; -import org.springframework.data.neo4j.core.cypher.Expression; -import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.StatementBuilder; -import org.springframework.data.neo4j.core.cypher.SymbolicName; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; -import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.neo4j.springframework.data.core.cypher.Condition; +import org.neo4j.springframework.data.core.cypher.Conditions; +import org.neo4j.springframework.data.core.cypher.Cypher; +import org.neo4j.springframework.data.core.cypher.Expression; +import org.neo4j.springframework.data.core.cypher.Functions; +import org.neo4j.springframework.data.core.cypher.StatementBuilder; +import org.neo4j.springframework.data.core.cypher.SymbolicName; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; +import org.neo4j.springframework.data.core.schema.NodeDescription; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java index 5fa1734ae..1ce112bf4 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -16,20 +16,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import java.util.Optional; import java.util.function.BiFunction; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; -import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; -import org.springframework.data.neo4j.repository.query.CypherAdapterUtils; -import org.springframework.data.neo4j.repository.query.CypherAdapterUtils.SchemaBasedStatementBuilder; -import org.springframework.data.neo4j.repository.query.ReactiveNeo4jQueryLookupStrategy; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; +import org.neo4j.springframework.data.repository.ReactiveNeo4jRepository; +import org.neo4j.springframework.data.repository.query.CypherAdapterUtils; +import org.neo4j.springframework.data.repository.query.CypherAdapterUtils.SchemaBasedStatementBuilder; +import org.neo4j.springframework.data.repository.query.ReactiveNeo4jQueryLookupStrategy; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java similarity index 91% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index 7c50fce3f..923bce7e3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import java.io.Serializable; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java similarity index 90% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java index cb4160681..c24a13b83 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import static java.util.Collections.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import lombok.extern.slf4j.Slf4j; @@ -37,16 +37,16 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.Neo4jClient.ExecutableQuery; -import org.springframework.data.neo4j.core.PreparedQuery; -import org.springframework.data.neo4j.core.cypher.Condition; -import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.StatementBuilder; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingReadingAndReturn; -import org.springframework.data.neo4j.core.cypher.renderer.Renderer; -import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.Neo4jClient.ExecutableQuery; +import org.neo4j.springframework.data.core.PreparedQuery; +import org.neo4j.springframework.data.core.cypher.Condition; +import org.neo4j.springframework.data.core.cypher.Functions; +import org.neo4j.springframework.data.core.cypher.Statement; +import org.neo4j.springframework.data.core.cypher.StatementBuilder; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingAndReturn; +import org.neo4j.springframework.data.core.cypher.renderer.Renderer; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java similarity index 87% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java index 15143b848..8f3a22dfe 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import static lombok.AccessLevel.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,15 +37,15 @@ import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.Neo4jClient.ExecutableQuery; -import org.springframework.data.neo4j.core.PreparedQuery; -import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.StatementBuilder; -import org.springframework.data.neo4j.core.cypher.StatementBuilder.BuildableStatement; -import org.springframework.data.neo4j.core.cypher.renderer.Renderer; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.Neo4jClient.ExecutableQuery; +import org.neo4j.springframework.data.core.PreparedQuery; +import org.neo4j.springframework.data.core.cypher.Functions; +import org.neo4j.springframework.data.core.cypher.Statement; +import org.neo4j.springframework.data.core.cypher.StatementBuilder; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.BuildableStatement; +import org.neo4j.springframework.data.core.cypher.renderer.Renderer; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.repository.support.PageableExecutionUtils; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java similarity index 89% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java index 81f96a38c..72fe0550b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import static java.util.Collections.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; @@ -32,13 +32,13 @@ import java.util.Optional; import org.reactivestreams.Publisher; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.core.PreparedQuery; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient.ExecutableQuery; -import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.renderer.Renderer; -import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.neo4j.springframework.data.core.PreparedQuery; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient.ExecutableQuery; +import org.neo4j.springframework.data.core.cypher.Functions; +import org.neo4j.springframework.data.core.cypher.Statement; +import org.neo4j.springframework.data.core.cypher.renderer.Renderer; +import org.neo4j.springframework.data.core.schema.NodeDescription; import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java similarity index 86% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java index 384d9fb5e..1aa164ac5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleReactiveQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.support; +package org.neo4j.springframework.data.repository.support; import static lombok.AccessLevel.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.repository.query.CypherAdapterUtils.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -34,13 +34,13 @@ import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.domain.Example; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.core.PreparedQuery; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient; -import org.springframework.data.neo4j.core.ReactiveNeo4jClient.ExecutableQuery; -import org.springframework.data.neo4j.core.cypher.Functions; -import org.springframework.data.neo4j.core.cypher.Statement; -import org.springframework.data.neo4j.core.cypher.renderer.Renderer; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.PreparedQuery; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient.ExecutableQuery; +import org.neo4j.springframework.data.core.cypher.Functions; +import org.neo4j.springframework.data.core.cypher.Statement; +import org.neo4j.springframework.data.core.cypher.renderer.Renderer; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/package-info.java deleted file mode 100644 index 796c5a552..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/renderer/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * A renderer for {@link org.springframework.data.neo4j.core.cypher.Statement statements} in the simplest form possible. - */ -package org.springframework.data.neo4j.core.cypher.renderer; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/package-info.java deleted file mode 100644 index 8647fa100..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/support/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Support for describing an abstract syntax tree. - */ -package org.springframework.data.neo4j.core.cypher.support; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheckTest.java similarity index 97% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheckTest.java index bf9c30b26..808e78208 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/DelegatingMappingFunctionWithNullCheckTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheckTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/NamedParametersTest.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/NamedParametersTest.java index a716849e8..a52260b3f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/NamedParametersTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/NamedParametersTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java similarity index 99% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java index 9f480de43..9c88e464f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java similarity index 97% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java index a633c6a0d..1f81c773f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/ReactiveNeo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -52,11 +52,11 @@ import org.neo4j.driver.reactive.RxStatementResult; import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.Neo4jClientTest.Bike; -import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwner; -import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwnerBinder; -import org.springframework.data.neo4j.core.Neo4jClientTest.BikeOwnerReader; -import org.springframework.data.neo4j.core.Neo4jClientTest.MapAssertionMatcher; +import org.neo4j.springframework.data.core.Neo4jClientTest.Bike; +import org.neo4j.springframework.data.core.Neo4jClientTest.BikeOwner; +import org.neo4j.springframework.data.core.Neo4jClientTest.BikeOwnerBinder; +import org.neo4j.springframework.data.core.Neo4jClientTest.BikeOwnerReader; +import org.neo4j.springframework.data.core.Neo4jClientTest.MapAssertionMatcher; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/SingleValueMappingFunctionTest.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/SingleValueMappingFunctionTest.java index 406965c4a..2c890e589 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/SingleValueMappingFunctionTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/SingleValueMappingFunctionTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java index e841ba737..b61ed6d6c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/TransactionHandlingTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core; +package org.neo4j.springframework.data.core; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -45,7 +45,7 @@ import org.neo4j.driver.internal.SessionParameters; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager; import org.springframework.transaction.support.TransactionTemplate; /** diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/ComparisonTest.java similarity index 96% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/ComparisonTest.java index 9b581c7ae..c85b31608 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/ComparisonTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/ComparisonTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java similarity index 99% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java index 567d3ac6c..2ac81f5da 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java @@ -16,16 +16,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.neo4j.core.cypher.Conditions.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; -import static org.springframework.data.neo4j.core.cypher.Functions.*; +import static org.neo4j.springframework.data.core.cypher.Conditions.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.core.cypher.Functions.*; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.data.neo4j.core.cypher.renderer.Renderer; +import org.neo4j.springframework.data.core.cypher.renderer.Renderer; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherTest.java similarity index 97% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherTest.java index d08196331..b6618aa68 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilderTest.java similarity index 97% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilderTest.java index 7aa2dc793..9585924c6 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilderTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilderTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/FunctionsTest.java similarity index 96% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/FunctionsTest.java index eba3a948c..d1c226709 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/FunctionsTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/FunctionsTest.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.data.neo4j.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.core.cypher.Cypher.*; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/NodeTest.java similarity index 90% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/NodeTest.java index 44fbdf424..07ad5024e 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/NodeTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/NodeTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; @@ -24,8 +24,8 @@ import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; -import org.springframework.data.neo4j.core.cypher.support.Visitable; -import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.neo4j.springframework.data.core.cypher.support.Visitable; +import org.neo4j.springframework.data.core.cypher.support.Visitor; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/PropertyTest.java similarity index 96% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/PropertyTest.java index 386e02749..840a29a32 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/PropertyTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/PropertyTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/RelationshipTest.java similarity index 95% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/RelationshipTest.java index f42a273ea..0eee69fbe 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/RelationshipTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/RelationshipTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/StringLiteralTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/StringLiteralTest.java similarity index 96% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/StringLiteralTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/StringLiteralTest.java index 7960e232e..dcceb0eb1 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/StringLiteralTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/StringLiteralTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher; +package org.neo4j.springframework.data.core.cypher; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitorTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitorTest.java similarity index 95% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitorTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitorTest.java index 449e320bc..3e5b1db0e 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/renderer/RenderingVisitorTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitorTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.cypher.renderer; +package org.neo4j.springframework.data.core.cypher.renderer; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategyTest.java similarity index 96% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategyTest.java index f96dddc91..f65952b14 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jIsNewStrategyTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategyTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.neo4j.springframework.data.core.mapping; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -28,8 +28,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.IdDescription; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.IdDescription; import org.springframework.data.support.IsNewStrategy; /** @@ -116,7 +116,7 @@ class DefaultNeo4jIsNewStrategyTest { assertThatIllegalArgumentException().isThrownBy(() -> DefaultNeo4jIsNewStrategy.basedOn(entityMetaData)) .withMessage( - "Cannot use org.springframework.data.neo4j.core.mapping.DefaultNeo4jIsNewStrategy with externally generated, primitive ids."); + "Cannot use org.neo4j.springframework.data.core.mapping.DefaultNeo4jIsNewStrategy with externally generated, primitive ids."); } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java similarity index 91% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java index 0425fe721..7f98ed605 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.neo4j.springframework.data.core.mapping; import static org.assertj.core.api.Assertions.*; @@ -29,13 +29,13 @@ import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.data.mapping.Association; -import org.springframework.data.neo4j.core.schema.GraphPropertyDescription; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.NodeDescription; -import org.springframework.data.neo4j.core.schema.Property; -import org.springframework.data.neo4j.core.schema.Relationship; -import org.springframework.data.neo4j.core.schema.RelationshipDescription; +import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.neo4j.springframework.data.core.schema.Property; +import org.neo4j.springframework.data.core.schema.Relationship; +import org.neo4j.springframework.data.core.schema.RelationshipDescription; /** * @author Michael J. Simons diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java index 5673ee669..929efab97 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.transaction; +package org.neo4j.springframework.data.core.transaction; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager.*; +import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager.*; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; @@ -42,7 +42,7 @@ import org.neo4j.driver.StatementResult; import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.neo4j.core.Neo4jClient; +import org.neo4j.springframework.data.core.Neo4jClient; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.transaction.support.DefaultTransactionDefinition; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtilsTest.java similarity index 93% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtilsTest.java index 508a4156b..b05140513 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionUtilsTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtilsTest.java @@ -16,10 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.transaction; +package org.neo4j.springframework.data.core.transaction; import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils.*; +import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java index c8dc98688..93a9976d0 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/ReactiveNeo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.transaction; +package org.neo4j.springframework.data.core.transaction; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import static org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager.*; +import static org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager.*; import io.r2dbc.h2.H2ConnectionConfiguration; import io.r2dbc.h2.H2ConnectionFactory; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/PersonRepository.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/PersonRepository.java similarity index 94% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/PersonRepository.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/PersonRepository.java index 18d44c835..0acfc40da 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/PersonRepository.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.imperative; +package org.neo4j.springframework.data.integration.imperative; import java.time.LocalDate; import java.util.List; @@ -26,12 +26,12 @@ import org.neo4j.driver.types.Point; import org.springframework.data.domain.Range; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; -import org.springframework.data.neo4j.integration.shared.KotlinPerson; -import org.springframework.data.neo4j.integration.shared.PersonWithAllConstructor; -import org.springframework.data.neo4j.integration.shared.PersonWithNoConstructor; -import org.springframework.data.neo4j.integration.shared.PersonWithWither; -import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.query.Query; +import org.neo4j.springframework.data.integration.shared.KotlinPerson; +import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.integration.shared.PersonWithNoConstructor; +import org.neo4j.springframework.data.integration.shared.PersonWithWither; +import org.neo4j.springframework.data.repository.Neo4jRepository; +import org.neo4j.springframework.data.repository.query.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java index b16740b7b..19c1034bb 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.imperative; +package org.neo4j.springframework.data.integration.imperative; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Range.Bound.*; @@ -54,15 +54,15 @@ import org.springframework.data.domain.Sort; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; -import org.springframework.data.neo4j.config.AbstractNeo4jConfig; -import org.springframework.data.neo4j.integration.shared.KotlinPerson; -import org.springframework.data.neo4j.integration.shared.PersonWithAllConstructor; -import org.springframework.data.neo4j.integration.shared.PersonWithNoConstructor; -import org.springframework.data.neo4j.integration.shared.PersonWithWither; -import org.springframework.data.neo4j.integration.shared.ThingWithAssignedId; -import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; -import org.springframework.data.neo4j.test.Neo4jExtension; -import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.KotlinPerson; +import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.integration.shared.PersonWithNoConstructor; +import org.neo4j.springframework.data.integration.shared.PersonWithWither; +import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/ThingRepository.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/ThingRepository.java similarity index 88% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/ThingRepository.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/ThingRepository.java index 95b1d23c2..17dc5fb54 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/imperative/ThingRepository.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/ThingRepository.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.imperative; +package org.neo4j.springframework.data.integration.imperative; import java.util.List; -import org.springframework.data.neo4j.integration.shared.ThingWithAssignedId; +import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; import org.springframework.data.repository.CrudRepository; /** diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactivePersonRepository.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactivePersonRepository.java similarity index 86% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactivePersonRepository.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactivePersonRepository.java index 2ca02c034..4bbf97d6d 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactivePersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactivePersonRepository.java @@ -16,14 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.reactive; +package org.neo4j.springframework.data.integration.reactive; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.data.neo4j.integration.shared.PersonWithAllConstructor; -import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; -import org.springframework.data.neo4j.repository.query.Query; +import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.repository.ReactiveNeo4jRepository; +import org.neo4j.springframework.data.repository.query.Query; import org.springframework.transaction.annotation.Transactional; /** diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java similarity index 97% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index 4269409c8..5055d1a49 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.reactive; +package org.neo4j.springframework.data.integration.reactive; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -41,11 +41,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; -import org.springframework.data.neo4j.integration.shared.PersonWithAllConstructor; -import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; -import org.springframework.data.neo4j.test.Neo4jExtension; -import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport; +import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveThingRepository.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveThingRepository.java similarity index 89% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveThingRepository.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveThingRepository.java index 015c26117..7a5bc446c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveThingRepository.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveThingRepository.java @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.reactive; +package org.neo4j.springframework.data.integration.reactive; import java.util.List; -import org.springframework.data.neo4j.integration.shared.ThingWithAssignedId; +import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; import org.springframework.data.repository.CrudRepository; /** diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithAllConstructor.java similarity index 86% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithAllConstructor.java index 16faf095e..ec632baf3 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithAllConstructor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.shared; +package org.neo4j.springframework.data.integration.shared; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -29,9 +29,9 @@ import java.time.LocalDate; import java.util.List; import org.neo4j.driver.types.Point; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Property; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.core.schema.Property; /** * @author Gerrit Meier diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithNoConstructor.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java similarity index 79% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithNoConstructor.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java index 6fde45e6c..e3bc86ee5 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithNoConstructor.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java @@ -16,15 +16,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.shared; +package org.neo4j.springframework.data.integration.shared; import lombok.Getter; import lombok.Setter; import lombok.ToString; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Property; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.core.schema.Property; @Getter @Setter diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithWither.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java similarity index 87% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithWither.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java index 4df50ad22..38d279fa4 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/PersonWithWither.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java @@ -16,14 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.shared; +package org.neo4j.springframework.data.integration.shared; import lombok.Getter; import lombok.Setter; import lombok.ToString; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; @Getter @Setter diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/ThingWithAssignedId.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithAssignedId.java similarity index 82% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/ThingWithAssignedId.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithAssignedId.java index 0201bee97..4d3453de5 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/shared/ThingWithAssignedId.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithAssignedId.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.integration.shared; +package org.neo4j.springframework.data.integration.shared; -import static org.springframework.data.neo4j.core.schema.Id.Strategy.*; +import static org.neo4j.springframework.data.core.schema.Id.Strategy.*; -import org.springframework.data.neo4j.core.schema.Id; -import org.springframework.data.neo4j.core.schema.Node; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; /** * Has an assigned id and a changeable name. diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositoriesTests.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositoriesTests.java similarity index 97% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositoriesTests.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositoriesTests.java index a7e7e14a6..8b224d471 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositoriesTests.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositoriesTests.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.config; +package org.neo4j.springframework.data.repository.config; import static org.assertj.core.api.Assertions.*; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/query/RepositoryQueryTest.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/query/RepositoryQueryTest.java index 40b7b681c..e2b965d5a 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/query/RepositoryQueryTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.repository.query; +package org.neo4j.springframework.data.repository.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -35,8 +35,8 @@ import org.neo4j.driver.Values; import org.neo4j.driver.types.Point; import org.springframework.data.annotation.Id; import org.springframework.data.mapping.MappingException; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.CrudRepository; diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java similarity index 98% rename from spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java index 0164068e2..99b680da4 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.test; +package org.neo4j.springframework.data.test; import lombok.extern.slf4j.Slf4j; diff --git a/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/shared/KotlinPerson.kt b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/shared/KotlinPerson.kt similarity index 82% rename from spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/shared/KotlinPerson.kt rename to spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/shared/KotlinPerson.kt index 5638f695d..f325ab0b0 100644 --- a/spring-data-neo4j/src/test/kotlin/org/springframework/data/neo4j/integration/shared/KotlinPerson.kt +++ b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/shared/KotlinPerson.kt @@ -17,10 +17,10 @@ * limitations under the License. */ -package org.springframework.data.neo4j.integration.shared +package org.neo4j.springframework.data.integration.shared -import org.springframework.data.neo4j.core.schema.Id -import org.springframework.data.neo4j.core.schema.Node +import org.neo4j.springframework.data.core.schema.Id +import org.neo4j.springframework.data.core.schema.Node /** * @author Gerrit Meier diff --git a/spring-data-neo4j/src/test/resources/logback-test.xml b/spring-data-neo4j/src/test/resources/logback-test.xml index 3f187a068..78b32841b 100644 --- a/spring-data-neo4j/src/test/resources/logback-test.xml +++ b/spring-data-neo4j/src/test/resources/logback-test.xml @@ -30,8 +30,8 @@ - - + + From 739095fd1bc9236bf01d4bae7c43dd250ee8928d Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 4 Jun 2019 16:58:24 +0200 Subject: [PATCH 108/342] Enable disabled reactive client test. --- .../data/core/TransactionHandlingTest.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java index b61ed6d6c..cab5c5ec4 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java @@ -91,7 +91,7 @@ class TransactionHandlingTest { class AutoCloseableStatementRunnerHandlerTest { @Test - public void shouldCallCloseOnSession() { + void shouldCallCloseOnSession() { ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); @@ -118,7 +118,7 @@ class TransactionHandlingTest { } @Test - public void shouldNotInvokeCloseOnTransaction() { + void shouldNotInvokeCloseOnTransaction() { AtomicBoolean transactionIsOpen = new AtomicBoolean(true); @@ -165,15 +165,16 @@ class TransactionHandlingTest { private RxTransaction transaction; @Test - @Disabled - public void shouldNotOpenTransactionsWithoutSubscription() { + void shouldNotOpenTransactionsWithoutSubscription() { + DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); + neo4jClient.query("RETURN 1").in("aDatabase").fetch().one(); verify(driver, never()).rxSession(any(Consumer.class)); verifyZeroInteractions(driver, session); } @Test - public void shouldCloseUnmanagedSessionOnComplete() { + void shouldCloseUnmanagedSessionOnComplete() { when(driver.rxSession(any(Consumer.class))).thenReturn(session); when(session.beginTransaction()).thenReturn(Mono.just(transaction)); @@ -197,7 +198,7 @@ class TransactionHandlingTest { } @Test - public void shouldCloseUnmanagedSessionOnError() { + void shouldCloseUnmanagedSessionOnError() { when(driver.rxSession(any(Consumer.class))).thenReturn(session); @@ -223,6 +224,6 @@ class TransactionHandlingTest { } } - static class SomeException extends RuntimeException { + private static class SomeException extends RuntimeException { } } From 863a86c95dcdc57e1fe2a962f95d1507445be6e4 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 4 Jun 2019 18:20:19 +0200 Subject: [PATCH 109/342] Remove unneeded import from test. --- .../neo4j/springframework/data/core/TransactionHandlingTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java index cab5c5ec4..a4775f3ac 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java @@ -29,7 +29,6 @@ import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; From ae5be3e8ddbc235977af21371ef05ba4c0c1eda2 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 5 Jun 2019 21:30:44 +0200 Subject: [PATCH 110/342] Set fix milestone version for Spring Data parent. --- pom.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8c840027d..b6dd07508 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ spring-data-parent org.springframework.data.build - 2.2.0.BUILD-SNAPSHOT + 2.2.0.M4 org.neo4j.springframework.data @@ -390,5 +390,9 @@ spring-libs-snapshot https://repo.spring.io/libs-snapshot + + spring-libs-milestone + https://repo.spring.io/milestone + From 6edb306a656c9b97b736d401cb5562364e093325 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 4 Jun 2019 16:44:50 +0200 Subject: [PATCH 111/342] Extract driver starter from SDN-RX. --- pom.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b6dd07508..7f2894918 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,6 @@ 5.4.0 3.0.0 3.0.0-M1 - 3.0.0-M1 3.0.0-M2 3.0.0-M3 3.0.0-M1 @@ -108,6 +107,7 @@ 2.2.5 1.7.26 2.2.0.M4 + 1.0.0.BUILD-SNAPSHOT 1.10.7 ${skipTests} @@ -183,6 +183,11 @@ neo4j-java-driver ${neo4j-java-driver.version} + + org.neo4j.driver + neo4j-java-driver-spring-boot-starter + ${neo4j-java-driver-spring-boot-starter.version} + org.neo4j.test neo4j-harness From 70b75995c45cbc435798bc6555f9d8f264d73c05 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 12 Jun 2019 10:13:51 +0200 Subject: [PATCH 112/342] Separate Spring Boot Starter and example projects from SDN-RX. This creates independent projects for the Spring Boot Starter and the examples. The Spring Boot Starter project inherits now from Spring Boot. The required Java version for the examples has been bumped to 12 (latest version as of writing). --- pom.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7f2894918..1a47939fa 100644 --- a/pom.xml +++ b/pom.xml @@ -73,8 +73,6 @@ spring-data-neo4j-rx - spring-data-neo4j-rx-spring-boot-starter-parent - spring-data-neo4j-rx-examples-parent From 223a150e156a485cdc06ae3f2501c999362d947e Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 12 Jun 2019 10:42:19 +0200 Subject: [PATCH 113/342] Add Starter and example projects to parent pom. --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index 1a47939fa..7f2894918 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,8 @@ spring-data-neo4j-rx + spring-data-neo4j-rx-spring-boot-starter-parent + spring-data-neo4j-rx-examples-parent From a151166c31d83c9abae5c49b4d5b49cafeadb125 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 12 Jun 2019 10:54:27 +0200 Subject: [PATCH 114/342] Add missing package-info files. --- .../neo4j/springframework/data/config/package-info.java | 8 ++++++++ .../neo4j/springframework/data/core/package-info.java | 8 ++++++++ .../data/repository/support/package-info.java | 9 +++++++++ 3 files changed, 25 insertions(+) create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/package-info.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/package-info.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/package-info.java diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/package-info.java new file mode 100644 index 000000000..10837e9ec --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/package-info.java @@ -0,0 +1,8 @@ +/** + * This package contains configuration related support classes that can be used for the application specific + * {@link org.springframework.context.annotation.Configuration}. + */ +@NonNullApi +package org.neo4j.springframework.data.config; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/package-info.java new file mode 100644 index 000000000..c0b515b1f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/package-info.java @@ -0,0 +1,8 @@ +/** + * This package contains the core infrastructure for creating a imperative or reactive client that can execute + * queries. + */ +@NonNullApi +package org.neo4j.springframework.data.core; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/package-info.java new file mode 100644 index 000000000..75e1cc621 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/package-info.java @@ -0,0 +1,9 @@ +/** + * This package contains infrastructure code that set up the needed Spring beans to register the repository + * capabilities. Additional it holds the implementations of the {@link org.springframework.data.repository.PagingAndSortingRepository} + * and {@link org.springframework.data.repository.reactive.ReactiveSortingRepository}. + */ +@NonNullApi +package org.neo4j.springframework.data.repository.support; + +import org.springframework.lang.NonNullApi; From 7019d679f61a24deaf748b0050384fcd53d6f50d Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 12 Jun 2019 11:10:46 +0200 Subject: [PATCH 115/342] Define which public classes are internal or external (stable) API. * Add missing API annotation. * Add since information in API annotation. * Make newly annotated classes final to fit in with existing jQAssistant rule. --- .../data/core/mapping/Neo4jMappingContext.java | 4 +++- .../data/core/transaction/Neo4jTransactionManager.java | 2 +- .../core/transaction/ReactiveNeo4jTransactionManager.java | 2 ++ .../config/Neo4jRepositoryConfigurationExtension.java | 4 +++- .../config/ReactiveNeo4jRepositoryConfigurationExtension.java | 4 +++- .../data/repository/support/Neo4jRepositoryFactoryBean.java | 4 +++- .../support/ReactiveNeo4jRepositoryFactoryBean.java | 4 +++- .../mockito-extensions/org.mockito.plugins.MockMaker | 1 + 8 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 spring-data-neo4j/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java index cbac384f7..42d268774 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java @@ -33,6 +33,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import org.apiguardian.api.API; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.convert.EntityInstantiators; @@ -55,7 +56,8 @@ import org.springframework.data.util.TypeInformation; * * @author Michael J. Simons */ -public class Neo4jMappingContext +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Neo4jMappingContext extends AbstractMappingContext, Neo4jPersistentProperty> implements Schema { /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java index fc70b213e..edb3db49e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java @@ -49,8 +49,8 @@ import org.springframework.util.Assert; * * @author Michael J. Simons */ -@API(status = API.Status.STABLE, since = "1.0") @Slf4j +@API(status = API.Status.STABLE, since = "1.0") public class Neo4jTransactionManager extends AbstractPlatformTransactionManager { /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 08565a05f..bae09ccf1 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -25,6 +25,7 @@ import reactor.core.publisher.Mono; import java.util.Collections; import java.util.List; +import org.apiguardian.api.API; import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.TransactionConfig; @@ -44,6 +45,7 @@ import org.springframework.util.Assert; /** * @author Gerrit Meier */ +@API(status = API.Status.STABLE, since = "1.0") public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransactionManager { /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java index 16ecd9a27..f57ac7b86 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -22,6 +22,7 @@ import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Collections; +import org.apiguardian.api.API; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.neo4j.springframework.data.core.schema.Node; @@ -39,7 +40,8 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; * @author Michael J. Simons * @author Gerrit Meier */ -public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { private static final String MODULE_PREFIX = "neo4j"; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java index d13f40371..a3ec28c02 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java @@ -22,6 +22,7 @@ import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Collections; +import org.apiguardian.api.API; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.neo4j.springframework.data.core.schema.Node; @@ -40,7 +41,8 @@ import org.springframework.data.repository.core.RepositoryMetadata; * @author Michael J. Simons * @author Gerrit Meier */ -public class ReactiveNeo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { +@API(status = API.Status.INTERNAL, since = "1.0") +public final class ReactiveNeo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { private static final String MODULE_PREFIX = "neo4j"; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java index fddb201c8..41d21f61e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java @@ -20,6 +20,7 @@ package org.neo4j.springframework.data.repository.support; import java.io.Serializable; +import org.apiguardian.api.API; import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.Repository; @@ -33,7 +34,8 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF * @author Michael J. Simons * @author Gerrit Meier */ -public class Neo4jRepositoryFactoryBean, S, ID extends Serializable> +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Neo4jRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport { private Neo4jClient neo4jClient; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index 923bce7e3..4c596c263 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -20,6 +20,7 @@ package org.neo4j.springframework.data.repository.support; import java.io.Serializable; +import org.apiguardian.api.API; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.springframework.data.repository.Repository; @@ -32,7 +33,8 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF * * @author Gerrit Meier */ -public class ReactiveNeo4jRepositoryFactoryBean, S, ID extends Serializable> +@API(status = API.Status.INTERNAL, since = "1.0") +public final class ReactiveNeo4jRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport { private ReactiveNeo4jClient neo4jClient; diff --git a/spring-data-neo4j/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/spring-data-neo4j/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..1f0955d45 --- /dev/null +++ b/spring-data-neo4j/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline From f59b70904ad695c5b7d0f23ce11ca3c2dbd00c9a Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 12 Jun 2019 15:26:14 +0200 Subject: [PATCH 116/342] Extract shared conversion support from imperative and reactive queries. --- .../repository/query/AbstractNeo4jQuery.java | 59 +------------ .../query/AbstractReactiveNeo4jQuery.java | 61 +------------ .../repository/query/Neo4jQuerySupport.java | 88 +++++++++++++++++++ 3 files changed, 91 insertions(+), 117 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQuerySupport.java diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java index f6dfb9331..42c7e549c 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java @@ -18,13 +18,6 @@ */ package org.neo4j.springframework.data.repository.query; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.data.domain.Range; -import org.springframework.data.geo.Circle; -import org.springframework.data.geo.Distance; -import org.springframework.data.geo.Metrics; import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.core.PreparedQuery; @@ -39,7 +32,7 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -abstract class AbstractNeo4jQuery implements RepositoryQuery { +abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements RepositoryQuery { protected final Neo4jClient neo4jClient; protected final Neo4jMappingContext mappingContext; @@ -93,54 +86,4 @@ abstract class AbstractNeo4jQuery implements RepositoryQuery { * @return True if the query has an explicit limit set. */ protected abstract boolean isLimiting(); - - /** - * Converts parameter as needed by the query generated, which is not covered by standard conversion services. - * - * @param parameter The parameter to fit into the generated query. - * @return A parameter that fits the place holders of a generated query - */ - final Object convertParameter(Object parameter) { - if (parameter instanceof Range) { - return convertRange((Range) parameter); - } else if (parameter instanceof Distance) { - return calculateDistanceInMeter((Distance) parameter); - } else if (parameter instanceof Circle) { - return convertCircle((Circle) parameter); - } - - // Good hook to check the NodeManager whether the thing is an entity and we replace the value with a known id. - - return parameter; - } - - private Map convertRange(Range range) { - Map map = new HashMap<>(); - range.getLowerBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("lb", v)); - range.getUpperBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("ub", v)); - return map; - } - - private Map convertCircle(Circle circle) { - Map map = new HashMap<>(); - map.put("x", convertParameter(circle.getCenter().getX())); - map.put("y", convertParameter(circle.getCenter().getY())); - map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); - return map; - } - - private static double calculateDistanceInMeter(Distance distance) { - - if (distance.getMetric() == Metrics.KILOMETERS) { - double kilometersDivisor = 0.001d; - return distance.getValue() / kilometersDivisor; - - } else if (distance.getMetric() == Metrics.MILES) { - double milesDivisor = 0.00062137d; - return distance.getValue() / milesDivisor; - - } else { - return distance.getValue(); - } - } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractReactiveNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractReactiveNeo4jQuery.java index ead581d1d..1060bb77a 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractReactiveNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractReactiveNeo4jQuery.java @@ -18,16 +18,9 @@ */ package org.neo4j.springframework.data.repository.query; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.data.domain.Range; -import org.springframework.data.geo.Circle; -import org.springframework.data.geo.Distance; -import org.springframework.data.geo.Metrics; +import org.neo4j.springframework.data.core.PreparedQuery; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; -import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; @@ -39,7 +32,7 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -abstract class AbstractReactiveNeo4jQuery implements RepositoryQuery { +abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements RepositoryQuery { final ReactiveNeo4jClient neo4jClient; final Neo4jMappingContext mappingContext; @@ -92,54 +85,4 @@ abstract class AbstractReactiveNeo4jQuery implements RepositoryQuery { * @return True if the query has an explicit limit set. */ protected abstract boolean isLimiting(); - - /** - * Converts parameter as needed by the query generated, which is not covered by standard conversion services. - * - * @param parameter The parameter to fit into the generated query. - * @return A parameter that fits the place holders of a generated query - */ - final Object convertParameter(Object parameter) { - if (parameter instanceof Range) { - return convertRange((Range) parameter); - } else if (parameter instanceof Distance) { - return calculateDistanceInMeter((Distance) parameter); - } else if (parameter instanceof Circle) { - return convertCircle((Circle) parameter); - } - - // Good hook to check the NodeManager whether the thing is an entity and we replace the value with a known id. - - return parameter; - } - - private Map convertRange(Range range) { - Map map = new HashMap<>(); - range.getLowerBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("lb", v)); - range.getUpperBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("ub", v)); - return map; - } - - private Map convertCircle(Circle circle) { - Map map = new HashMap<>(); - map.put("x", convertParameter(circle.getCenter().getX())); - map.put("y", convertParameter(circle.getCenter().getY())); - map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); - return map; - } - - private static double calculateDistanceInMeter(Distance distance) { - - if (distance.getMetric() == Metrics.KILOMETERS) { - double kilometersDivisor = 0.001d; - return distance.getValue() / kilometersDivisor; - - } else if (distance.getMetric() == Metrics.MILES) { - double milesDivisor = 0.00062137d; - return distance.getValue() / milesDivisor; - - } else { - return distance.getValue(); - } - } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQuerySupport.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQuerySupport.java new file mode 100644 index 000000000..ede18047b --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQuerySupport.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.query; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.domain.Range; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; + +/** + * Some conversions used by both reactive and imperative Neo4j queries. While we try to separate reactive and imperative + * flows, it is cumbersome to repeate those conversions all over the place. + * + * @author Gerrit Meier + * @author Michael J. Simons + * @since 1.0 + */ +abstract class Neo4jQuerySupport { + + /** + * Converts parameter as needed by the query generated, which is not covered by standard conversion services. + * + * @param parameter The parameter to fit into the generated query. + * @return A parameter that fits the place holders of a generated query + */ + final Object convertParameter(Object parameter) { + if (parameter instanceof Range) { + return convertRange((Range) parameter); + } else if (parameter instanceof Distance) { + return calculateDistanceInMeter((Distance) parameter); + } else if (parameter instanceof Circle) { + return convertCircle((Circle) parameter); + } + + // Good hook to check the NodeManager whether the thing is an entity and we replace the value with a known id. + + return parameter; + } + + private Map convertRange(Range range) { + Map map = new HashMap<>(); + range.getLowerBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("lb", v)); + range.getUpperBound().getValue().map(this::convertParameter).ifPresent(v -> map.put("ub", v)); + return map; + } + + private Map convertCircle(Circle circle) { + Map map = new HashMap<>(); + map.put("x", convertParameter(circle.getCenter().getX())); + map.put("y", convertParameter(circle.getCenter().getY())); + map.put("radius", convertParameter(calculateDistanceInMeter(circle.getRadius()))); + return map; + } + + private static double calculateDistanceInMeter(Distance distance) { + + if (distance.getMetric() == Metrics.KILOMETERS) { + double kilometersDivisor = 0.001d; + return distance.getValue() / kilometersDivisor; + + } else if (distance.getMetric() == Metrics.MILES) { + double milesDivisor = 0.00062137d; + return distance.getValue() / milesDivisor; + + } else { + return distance.getValue(); + } + } +} From f002e84ebfff010f5a5f226c676865ab033ec0fb Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 12 Jun 2019 15:27:38 +0200 Subject: [PATCH 117/342] Implement saveAll() in SimpleNeo4jRepository. --- .../data/core/schema/NodeDescription.java | 1 + .../repository/query/CypherAdapterUtils.java | 27 ++++++++- .../support/SimpleNeo4jRepository.java | 48 +++++++++++---- .../integration/imperative/RepositoryIT.java | 59 +++++++++++++++++++ 4 files changed, 122 insertions(+), 13 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java index a5914a1dc..70a6309f8 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java @@ -38,6 +38,7 @@ public interface NodeDescription { String NAME_OF_INTERNAL_ID = "__internalNeo4jId__"; String NAME_OF_ID_PARAM = "__id__"; String NAME_OF_PROPERTIES_PARAM = "__properties__"; + String NAME_OF_ENTITY_LIST_PARAM = "__entities__"; /** * @return The primary label of this entity inside Neo4j. diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java index 2cbf80151..3a181e51e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java @@ -27,9 +27,6 @@ import java.util.Optional; import java.util.function.Function; import org.apiguardian.api.API; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.mapping.MappingException; import org.neo4j.springframework.data.core.cypher.*; import org.neo4j.springframework.data.core.cypher.StatementBuilder.BuildableStatement; import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingAndWith; @@ -39,6 +36,10 @@ import org.neo4j.springframework.data.core.schema.IdDescription; import org.neo4j.springframework.data.core.schema.NodeDescription; import org.neo4j.springframework.data.core.schema.Schema; import org.neo4j.springframework.data.repository.support.Neo4jEntityInformation; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.MappingException; +import org.springframework.util.Assert; /** * A set of shared utils to adapt {@code org.neo4j.springframework.data.core.cypher} functionality with Spring Data @@ -186,6 +187,26 @@ public final class CypherAdapterUtils { return Cypher.union(createIfNew, updateIfExists); } } + + public Statement prepareSaveOfMultipleInstancesOf(NodeDescription nodeDescription) { + + Assert.isTrue(!nodeDescription.isUsingInternalIds(), + "Only entities that use external IDs can be saved in a batch."); + + Node rootNode = node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); + IdDescription idDescription = nodeDescription.getIdDescription(); + + String nameOfIdProperty = idDescription.getOptionalGraphPropertyName() + .orElseThrow(() -> new MappingException("External id does not correspond to a graph property!")); + + String row = "entity"; + return Cypher + .unwind(parameter(NAME_OF_ENTITY_LIST_PARAM)).as(row) + .merge(rootNode.properties(nameOfIdProperty, property(row, NAME_OF_ID_PARAM))) + .set(rootNode, property(row, NAME_OF_PROPERTIES_PARAM)) + .returning(rootNode.internalId()) + .build(); + } } public static BuildableStatement addPagingParameter( diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java index c24a13b83..6a3adaad0 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java @@ -19,7 +19,9 @@ package org.neo4j.springframework.data.repository.support; import static java.util.Collections.*; +import static java.util.stream.Collectors.*; import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import lombok.extern.slf4j.Slf4j; @@ -28,15 +30,12 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.function.LongSupplier; -import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.neo4j.driver.summary.ResultSummary; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.neo4j.driver.summary.SummaryCounters; import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.Neo4jClient.ExecutableQuery; import org.neo4j.springframework.data.core.PreparedQuery; @@ -47,6 +46,10 @@ import org.neo4j.springframework.data.core.cypher.StatementBuilder; import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingAndReturn; import org.neo4j.springframework.data.core.cypher.renderer.Renderer; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; @@ -131,7 +134,29 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Override @Transactional public Iterable saveAll(Iterable entities) { - throw new UnsupportedOperationException("Not there yet."); + + if (entityMetaData.isUsingInternalIds()) { + log.debug("Saving entities using single statements."); + return StreamSupport.stream(entities.spliterator(), false) + .map(this::save) + .collect(toList()); + } + + final Function> binderFunction = entityInformation.getBinderFunction(); + List> entityList = StreamSupport.stream(entities.spliterator(), false) + .map(binderFunction).collect(toList()); + + ResultSummary resultSummary = neo4jClient + .query(() -> renderer.render(statementBuilder.prepareSaveOfMultipleInstancesOf(entityMetaData))) + .bind(entityList).to(NAME_OF_ENTITY_LIST_PARAM) + .run(); + + SummaryCounters counters = resultSummary.counters(); + log.debug("Created {} and deleted {} nodes, created {} and deleted {} relationships and set {} properties.", + counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(), + counters.relationshipsDeleted(), counters.propertiesSet()); + + return entities; } @Override @@ -197,7 +222,8 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository .bind(id).to(nameOfParameter) .run(); - log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); + log.debug("Deleted {} nodes and {} relationships.", summary.counters().nodesDeleted(), + summary.counters().relationshipsDeleted()); } @Override @@ -216,7 +242,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository Condition condition = entityInformation.getIdExpression().in(parameter(nameOfParameter)); List ids = StreamSupport.stream(entities.spliterator(), false) - .map(this.entityInformation::getId).collect(Collectors.toList()); + .map(this.entityInformation::getId).collect(toList()); log.debug("Deleting all entities with the following ids: {} ", ids); @@ -225,7 +251,8 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository .bind(ids).to(nameOfParameter) .run(); - log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); + log.debug("Deleted {} nodes and {} relationships.", summary.counters().nodesDeleted(), + summary.counters().relationshipsDeleted()); } @Override @@ -237,7 +264,8 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.empty()); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).run(); - log.debug("Deleted {} entities.", summary.counters().nodesDeleted()); + log.debug("Deleted {} nodes and {} relationships.", summary.counters().nodesDeleted(), + summary.counters().relationshipsDeleted()); } private ExecutableQuery createExecutableQuery(Statement statement) { diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java index 19c1034bb..51d7fe1f2 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java @@ -18,6 +18,7 @@ */ package org.neo4j.springframework.data.integration.imperative; +import static java.util.stream.Collectors.*; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Range.Bound.*; @@ -28,6 +29,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.IntStream; +import java.util.stream.StreamSupport; import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.BeforeEach; @@ -37,6 +39,7 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Point; @@ -182,6 +185,36 @@ class RepositoryIT { } } + @Test + void saveAll() { + + PersonWithAllConstructor newPerson = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, + LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null); + + PersonWithAllConstructor existingPerson = repository.findById(id1).get(); + existingPerson.setFirstName("Updated first name"); + existingPerson.setNullable("Updated nullable field"); + + assertThat(repository.count()).isEqualTo(2); + + List ids = StreamSupport.stream(repository.saveAll(Arrays.asList(existingPerson, newPerson)).spliterator(), false) + .map(PersonWithAllConstructor::getId) + .collect(toList()); + + assertThat(repository.count()).isEqualTo(3); + + try (Session session = driver.session()) { + + Record record = session + .run("MATCH (n:PersonWithAllConstructor) WHERE id(n) IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names", Values.parameters("ids", ids)) + .single(); + + assertThat(record.containsKey("names")).isTrue(); + List names = record.get("names").asList(Value::asString); + assertThat(names).contains("Mercury", TEST_PERSON1_NAME); + } + } + @Test void updateSingleEntity() { @@ -277,6 +310,32 @@ class RepositoryIT { } } + @Test + void saveAllWithAssignedId() { + + assertThat(thingRepository.count()).isEqualTo(21); + + ThingWithAssignedId newThing = new ThingWithAssignedId("aaBB"); + newThing.setName("That's the thing."); + + ThingWithAssignedId existingThing = thingRepository.findById("anId").get(); + existingThing.setName("Updated name."); + + thingRepository.saveAll(Arrays.asList(newThing, existingThing)); + + try (Session session = driver.session()) { + Record record = session + .run("MATCH (n:Thing) WHERE n.theId IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names", Values.parameters("ids", Arrays.asList(newThing.getTheId(), existingThing.getTheId()))) + .single(); + + assertThat(record.containsKey("names")).isTrue(); + List names = record.get("names").asList(Value::asString); + assertThat(names).contains(newThing.getName(), existingThing.getName()); + + assertThat(thingRepository.count()).isEqualTo(22); + } + } + @Test void updateWithAssignedId() { From e4014f343bbd2801338c5efc7a79169a98731bbe Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 11 Jun 2019 15:12:01 +0200 Subject: [PATCH 118/342] Introduce strict JavaDoc checks. * check for empty at-clauses. * add more return statements. * add missing since JavaDoc annotation. --- etc/checkstyle/config.xml | 10 +++ pom.xml | 4 +- .../config/Neo4jConfigurationSupport.java | 12 +-- .../ReactiveNeo4jConfigurationSupport.java | 4 +- .../data/core/DefaultNeo4jClient.java | 1 + .../data/core/DefaultReactiveNeo4jClient.java | 2 +- ...elegatingMappingFunctionWithNullCheck.java | 2 +- .../data/core/Neo4jClient.java | 58 +++++++++----- .../data/core/PreparedQuery.java | 2 + .../data/core/ReactiveNeo4jClient.java | 36 +++++---- .../data/core/SingleValueMappingFunction.java | 3 +- .../data/core/cypher/Cypher.java | 8 +- .../core/cypher/DefaultStatementBuilder.java | 2 +- .../core/cypher/ExposesRelationships.java | 1 + .../data/core/cypher/Literal.java | 1 + .../data/core/cypher/MapExpression.java | 1 + .../data/core/cypher/Operator.java | 4 + .../data/core/cypher/Order.java | 1 + .../data/core/cypher/Pattern.java | 1 + .../data/core/cypher/Property.java | 7 +- .../data/core/cypher/Relationship.java | 1 + .../data/core/cypher/SortItem.java | 4 + .../data/core/cypher/Statement.java | 8 ++ .../data/core/cypher/StatementBuilder.java | 75 +++++++++++++++++-- .../data/core/cypher/renderer/Renderer.java | 6 +- .../cypher/support/ReflectiveVisitor.java | 2 +- .../core/cypher/support/TypedSubtree.java | 2 +- .../data/core/cypher/support/Visitable.java | 3 +- .../mapping/DefaultNeo4jBinderFunction.java | 2 +- .../mapping/DefaultNeo4jPersistentEntity.java | 1 + .../DefaultNeo4jPersistentProperty.java | 3 +- .../DefaultRelationshipDescription.java | 1 + .../core/mapping/Neo4jMappingContext.java | 1 + .../core/mapping/Neo4jPersistentEntity.java | 2 + .../core/mapping/Neo4jPersistentProperty.java | 1 + .../springframework/data/core/schema/Id.java | 5 ++ .../data/core/schema/Relationship.java | 1 + .../data/core/schema/Schema.java | 2 +- .../Neo4jSessionSynchronization.java | 2 + .../transaction/Neo4jTransactionHolder.java | 13 ++-- .../transaction/Neo4jTransactionManager.java | 5 +- .../transaction/Neo4jTransactionUtils.java | 2 + .../ReactiveNeo4jSessionSynchronization.java | 1 + .../ReactiveNeo4jTransactionHolder.java | 7 +- .../ReactiveNeo4jTransactionManager.java | 7 +- .../data/repository/Neo4jRepository.java | 3 + .../repository/ReactiveNeo4jRepository.java | 3 + .../config/EnableNeo4jRepositories.java | 1 + .../EnableReactiveNeo4jRepositories.java | 1 + .../config/Neo4jRepositoriesRegistrar.java | 1 + ...Neo4jRepositoryConfigurationExtension.java | 1 + .../ReactiveNeo4jRepositoriesRegistrar.java | 1 + ...Neo4jRepositoryConfigurationExtension.java | 1 + .../repository/query/AbstractNeo4jQuery.java | 4 +- .../repository/query/CypherAdapterUtils.java | 14 ++-- .../repository/query/CypherQueryCreator.java | 18 ++--- .../repository/query/PartTreeNeo4jQuery.java | 4 +- .../data/repository/query/Query.java | 4 +- .../query/ReactivePartTreeNeo4jQuery.java | 4 +- .../query/ReactiveStringBasedNeo4jQuery.java | 24 +++--- .../query/StringBasedNeo4jQuery.java | 24 +++--- .../support/Neo4jEntityInformation.java | 2 +- .../support/Neo4jRepositoryFactoryBean.java | 4 + .../ReactiveNeo4jRepositoryFactoryBean.java | 4 + .../support/SimpleQueryByExampleExecutor.java | 2 +- .../SimpleReactiveQueryByExampleExecutor.java | 2 +- .../shared/PersonWithNoConstructor.java | 3 + .../integration/shared/PersonWithWither.java | 4 + .../data/test/Neo4jExtension.java | 5 ++ 69 files changed, 317 insertions(+), 134 deletions(-) diff --git a/etc/checkstyle/config.xml b/etc/checkstyle/config.xml index 81afd6835..ea3eefad8 100644 --- a/etc/checkstyle/config.xml +++ b/etc/checkstyle/config.xml @@ -103,5 +103,15 @@ + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 7f2894918..059312b5f 100644 --- a/pom.xml +++ b/pom.xml @@ -80,13 +80,13 @@ 1.0.0 3.11.1 - 8.16 + 8.21 0.8.2 1.8 1.6.0 1.6.0 5.4.0 - 3.0.0 + 3.1.0 3.0.0-M1 3.0.0-M2 3.0.0-M3 diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java index 5919baed9..634321f67 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java @@ -51,7 +51,7 @@ public abstract class Neo4jConfigurationSupport { /** * The driver to be used for interacting with Neo4j. * - * @return + * @return the Neo4j Java driver instance to work with. */ public abstract Driver driver(); @@ -59,7 +59,7 @@ public abstract class Neo4jConfigurationSupport { * Creates a {@link org.neo4j.springframework.data.core.mapping.Neo4jMappingContext} equipped with entity classes * scanned from the mapping base package. * - * @return + * @return A new {@link Neo4jMappingContext} with initial classes to scan for entities set. * @see #getMappingBasePackages() */ @Bean @@ -102,8 +102,8 @@ public abstract class Neo4jConfigurationSupport { * Scans the mapping base package for classes annotated with {@link Node}. * By default, it scans for entities in all packages returned by {@link #getMappingBasePackages()}. * - * @return - * @throws ClassNotFoundException + * @return initial set of domain classes + * @throws ClassNotFoundException if the given class cannot be found in the class path. * @see #getMappingBasePackages() */ protected final Set> getInitialEntitySet() throws ClassNotFoundException { @@ -121,8 +121,8 @@ public abstract class Neo4jConfigurationSupport { * Scans the given base package for entities, i.e. Neo4j specific types annotated with {@link Node}. * * @param basePackage must not be {@literal null}. - * @return - * @throws ClassNotFoundException + * @return found entities in the package to scan. + * @throws ClassNotFoundException if the given class cannot be loaded by the class loader. */ protected final Set> scanForEntities(String basePackage) throws ClassNotFoundException { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java index e3601b567..5597af684 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java @@ -52,7 +52,7 @@ public abstract class ReactiveNeo4jConfigurationSupport { /** * The driver to be used for interacting with Neo4j. * - * @return + * @return the Neo4j Java driver instance to work with. */ public abstract Driver driver(); @@ -60,7 +60,7 @@ public abstract class ReactiveNeo4jConfigurationSupport { * Creates a {@link Neo4jMappingContext} equipped with entity classes * scanned from the mapping base package. * - * @return + * @return A new {@link Neo4jMappingContext} with initial classes to scan for entities set. * @see #getMappingBasePackages() */ @Bean diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java index 02c0ea2fb..1b62b6619 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java @@ -59,6 +59,7 @@ import org.springframework.util.Assert; * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ class DefaultNeo4jClient implements Neo4jClient { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java index 3b3e8e9ed..1e30d9dfb 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java @@ -310,7 +310,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { /** * @return A single result - * @throws IncorrectResultSizeDataAccessException + * @throws IncorrectResultSizeDataAccessException if there is no or more than one result */ public Mono getSingleResult() { try { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java index ccb429b2e..25eab7a4b 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java @@ -30,8 +30,8 @@ import org.neo4j.driver.types.TypeSystem; * This class has been introduced instead of {@code Function#andThen} notion to be able throw a decent exception * containing some information about the delegate used and which record was problematic. * - * @param The expected type of this function * @author Michael J. Simons + * @param The expected type of this function * @soundtrack Manowar - Fighting The World * @since 1.0 */ diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java index 46c655e74..fe1bd26f8 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java @@ -32,8 +32,10 @@ import org.neo4j.driver.Record; import org.neo4j.driver.StatementRunner; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; +import org.neo4j.springframework.data.repository.NoResultException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.lang.Nullable; /** @@ -61,7 +63,7 @@ public interface Neo4jClient { * removal of things. * * @param cypher The cypher code that shall be executed - * @return A new CypherSpec + * @return A runnable query specification. */ RunnableSpec query(String cypher); @@ -71,7 +73,7 @@ public interface Neo4jClient { * the Cypher statement. * * @param cypherSupplier A supplier of arbitrary Cypher code - * @return + * @return A runnable query specification. */ RunnableSpec query(Supplier cypherSupplier); @@ -88,28 +90,30 @@ public interface Neo4jClient { * Takes a prepared query, containing all the information about the cypher template to be used, needed parameters and * an optional mapping function, and turns it into an executable query. * - * @param preparedQuery + * @param preparedQuery prepared query that should get converted to an executable query * @param The type of the objects returned by this query. - * @return + * @return An executable query */ ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery); /** * Contract for a runnable query that can be either run returning it's result, run without results or be parameterized. + * @since 1.0 */ interface RunnableSpec extends RunnableSpecTightToDatabase { /** * Pins the previously defined query to a specific database. * - * @param targetDatabase - * @return + * @param targetDatabase selected database to use + * @return A runnable query specification that is now tight to a given database. */ RunnableSpecTightToDatabase in(String targetDatabase); } /** * Contract for a runnable query inside a dedicated database. + * @since 1.0 */ interface RunnableSpecTightToDatabase extends BindSpec { @@ -118,21 +122,22 @@ public interface Neo4jClient { * * @param targetClass The class each record should be mapped to * @param The type of the class - * @return A mapping spec that allows specifying a mapping function + * @return A mapping spec that allows specifying a mapping function. */ MappingSpec, Collection, T> fetchAs(Class targetClass); /** * Fetch all records mapped into generic maps * - * @return A fetch specification that maps into generic maps + * @return A fetch specification that maps into generic maps. */ RecordFetchSpec>, Collection>, Map> fetch(); /** - * Execute the query and discard the results + * Execute the query and discard the results. It returns the drivers result summary, including various counters + * and other statistics. * - * @return + * @return The native summary of the query. */ ResultSummary run(); } @@ -141,6 +146,7 @@ public interface Neo4jClient { * Contract for binding parameters to a query. * * @param This {@link BindSpec specs} own type + * @since 1.0 */ interface BindSpec> { @@ -157,6 +163,8 @@ public interface Neo4jClient { * Ongoing bind specification. * * @param This {@link OngoingBindSpec specs} own type + * @param Binding value type + * @since 1.0 */ interface OngoingBindSpec> { @@ -164,7 +172,7 @@ public interface Neo4jClient { * Bind one convertible object to the given name. * * @param name The named parameter to bind the value to - * @return + * @return The bind specification itself for binding more values or execution. */ S to(String name); @@ -172,7 +180,7 @@ public interface Neo4jClient { * Use a binder function for the previously defined value. * * @param binder The binder function to create a map of parameters from the given value - * @return + * @return The bind specification itself for binding more values or execution. */ S with(Function> binder); } @@ -181,6 +189,7 @@ public interface Neo4jClient { * @param The type of the class holding zero or one result element * @param The type of the class holding zero or more result elements * @param The resulting type of this mapping + * @since 1.0 */ interface MappingSpec extends RecordFetchSpec { @@ -189,7 +198,7 @@ public interface Neo4jClient { * itself and in addition, the type system that the Neo4j Java-Driver used while executing the query. * * @param mappingFunction The mapping function used to create new domain objects - * @return A specification how to fetch one or more records + * @return A specification how to fetch one or more records. */ RecordFetchSpec mappedBy(BiFunction mappingFunction); } @@ -198,6 +207,7 @@ public interface Neo4jClient { * @param The type of the class holding zero or one result element * @param The type of the class holding zero or more result elements * @param The type to which the fetched records are eventually mapped + * @since 1.0 */ interface RecordFetchSpec { @@ -227,13 +237,14 @@ public interface Neo4jClient { * A contract for an ongoing delegation in the selected database. * * @param The type of the returned value. + * @since 1.0 */ interface OngoingDelegation extends RunnableDelegation { /** * Runs the delegation in the given target database. * - * @param targetDatabase + * @param targetDatabase selected database to use * @return An ongoing delegation */ RunnableDelegation in(String targetDatabase); @@ -242,14 +253,15 @@ public interface Neo4jClient { /** * A runnable delegation. * - * @param + * @param the type that gets returned + * @since 1.0 */ interface RunnableDelegation { /** * Runs the stored callback. * - * @return + * @return The optional result of the callback that has been executed with the given database. */ Optional run(); } @@ -257,14 +269,26 @@ public interface Neo4jClient { /** * An interface for controlling query execution. * - * @param + * @param the type that gets returned by the query + * @since 1.0 */ interface ExecutableQuery { + /** + * @return The list of all results. That can be an empty list but is never null. + */ List getResults(); + /** + * @return An optional, single result. + * @throws IncorrectResultSizeDataAccessException when there is more than one result + */ Optional getSingleResult(); + /** + * @return A required, single result. + * @throws NoResultException when there is no result + */ T getRequiredSingleResult(); } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java index 283256382..e849c3cc6 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java @@ -80,6 +80,7 @@ public final class PreparedQuery { /** * @param The concrete type of this build step. + * @since 1.0 */ public static class RequiredBuildStep { private final Class resultType; @@ -95,6 +96,7 @@ public final class PreparedQuery { /** * @param The concrete type of this build step. + * @since 1.0 */ public static class OptionalBuildSteps { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java index db95bfe94..3d1c26b88 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java @@ -29,12 +29,12 @@ import org.apiguardian.api.API; import org.neo4j.driver.Driver; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.summary.ResultSummary; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.neo4j.springframework.data.core.Neo4jClient.BindSpec; import org.neo4j.springframework.data.core.Neo4jClient.MappingSpec; import org.neo4j.springframework.data.core.Neo4jClient.RecordFetchSpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.IncorrectResultSizeDataAccessException; /** * Reactive Neo4j client. The main difference to the {@link Neo4jClient imperative Neo4j client} is the fact that all @@ -69,7 +69,7 @@ public interface ReactiveNeo4jClient { * the Cypher statement. * * @param cypherSupplier A supplier of arbitrary Cypher code - * @return + * @return A runnable query specification. */ ReactiveRunnableSpec query(Supplier cypherSupplier); @@ -86,28 +86,30 @@ public interface ReactiveNeo4jClient { * Takes a prepared query, containing all the information about the cypher template to be used, needed parameters and * an optional mapping function, and turns it into an executable query. * - * @param preparedQuery + * @param preparedQuery prepared query that should get converted to an executable query * @param The type of the objects returned by this query. - * @return + * @return An executable query */ ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery); /** * Contract for a runnable query that can be either run returning it's result, run without results or be parameterized. + * @since 1.0 */ interface ReactiveRunnableSpec extends ReactiveRunnableSpecTightToDatabase { /** * Pins the previously defined query to a specific database. * - * @param targetDatabase - * @return + * @param targetDatabase selected database to use + * @return A runnable query specification that is now tight to a given database. */ ReactiveRunnableSpecTightToDatabase in(String targetDatabase); } /** * Contract for a runnable query inside a dedicated database. + * @since 1.0 */ interface ReactiveRunnableSpecTightToDatabase extends BindSpec { @@ -128,9 +130,10 @@ public interface ReactiveNeo4jClient { RecordFetchSpec>, Flux>, Map> fetch(); /** - * Execute the query and discard the results + * Execute the query and discard the results. It returns the drivers result summary, including various counters + * and other statistics. * - * @return + * @return A mono containing the native summary of the query. */ Mono run(); } @@ -139,13 +142,14 @@ public interface ReactiveNeo4jClient { * A contract for an ongoing delegation in the selected database. * * @param The type of the returned value. + * @since 1.0 */ interface OngoingReactiveDelegation extends ReactiveRunnableDelegation { /** * Runs the delegation in the given target database. * - * @param targetDatabase + * @param targetDatabase selected database to use * @return An ongoing delegation */ ReactiveRunnableDelegation in(String targetDatabase); @@ -154,14 +158,15 @@ public interface ReactiveNeo4jClient { /** * A runnable delegation. * - * @param + * @param the type that gets returned by the query + * @since 1.0 */ interface ReactiveRunnableDelegation { /** * Runs the stored callback. * - * @return + * @return The optional result of the callback that has been executed with the given database. */ Mono run(); } @@ -169,7 +174,8 @@ public interface ReactiveNeo4jClient { /** * An interface for controlling query execution in a reactive fashion. * - * @param + * @param the type that gets returned by the query + * @since 1.0 */ interface ExecutableQuery { @@ -180,7 +186,7 @@ public interface ReactiveNeo4jClient { /** * @return A single result - * @throws IncorrectResultSizeDataAccessException + * @throws IncorrectResultSizeDataAccessException if there are more than one result */ Mono getSingleResult(); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java index 82a2c5baa..c61f4ae7b 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java @@ -30,8 +30,9 @@ import org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes; /** * Used to automatically map single valued records to a sensible Java type based on {@link Value#asObject()}. * - * @param * @author Michael J. Simons + * @param type of the domain class to map + * @since 1.0 */ @RequiredArgsConstructor final class SingleValueMappingFunction implements BiFunction { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Cypher.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Cypher.java index 21f63c676..6d732963d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Cypher.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Cypher.java @@ -197,8 +197,8 @@ public final class Cypher { /** * Starts building a statement starting with an {@code UNWIND} clause. The expressions passed will be turned into a * list expression - * @param expressions - * @return + * @param expressions expressions to unwind + * @return a new instance of {@link OngoingUnwind} */ public static OngoingUnwind unwind(Expression... expressions) { @@ -230,8 +230,8 @@ public final class Cypher { /** * Creates a {@link ListExpression list-expression} from several expressions. * - * @param expressions - * @return + * @param expressions expressions to get combined into a list + * @return a new instance of {@link ListExpression} */ public static ListExpression listOf(Expression... expressions) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java index a92e34dfd..a8cd438ea 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java @@ -400,7 +400,7 @@ class DefaultStatementBuilder } /** - * Ongoing with extends from {@link WithBuilderSupport} and therefore from {@Defaultd} + * Ongoing with extends from {@link WithBuilderSupport} and therefore from {@link DefaultStatementWithReturnBuilder}. */ protected final class DefaultStatementWithWithBuilder extends WithBuilderSupport implements OngoingReadingAndWithWithoutWhere, OngoingReadingAndWithWithWhere { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExposesRelationships.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExposesRelationships.java index 8e683ea6f..ef5bcdbd2 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExposesRelationships.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/ExposesRelationships.java @@ -24,6 +24,7 @@ import org.apiguardian.api.API; * A marker interface for things that expose methods to create new relationships to other elements. * * @author Michael J. Simons + * @param Typically a {@link Relationship} or {@link RelationshipChain} * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Literal.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Literal.java index dafb8c833..783cf8b69 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Literal.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Literal.java @@ -25,6 +25,7 @@ import org.springframework.lang.Nullable; * Represents a literal with an optional content. * * @author Michael J. Simons + * @param type of content * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java index 19a7085de..06e99892d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java @@ -34,6 +34,7 @@ import org.springframework.util.Assert; * or {@link Relationship relationships}. * * @author Michael J. Simons + * @param type of properties or parameter * @soundtrack Rammstein - RAMMSTEIN * @since 1.0 */ diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operator.java index 9d68f6c51..1f71ee128 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operator.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Operator.java @@ -94,6 +94,10 @@ public enum Operator implements Visitable { return type; } + /** + * {@link Operator} type. + * @since 1.0 + */ public enum Type { BINARY, PREFIX, diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Order.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Order.java index 41aa61a41..bfcf70917 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Order.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Order.java @@ -28,6 +28,7 @@ import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; * * @author Gerrit Meier * @author Michael J. Simons + * @param The order's entry type * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java index c6c77891d..2c4e6e5b6 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java @@ -30,6 +30,7 @@ import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; * See Pattern. * * @author Michael J. Simons + * @param The pattern's entry type * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Property.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Property.java index cf02767bc..4d8994d2c 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Property.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Property.java @@ -70,10 +70,11 @@ public final class Property implements Expression { } /** - * Creates a {@link Operation} setting this property to a new value. + * Creates an {@link Operation} setting this property to a new value. The property does not track the operations + * created with this method. * - * @param expression - * @return + * @param expression expression describing the new value + * @return A new operation. */ public Operation to(Expression expression) { return Operations.set(this, expression); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java index ef75a6131..09d1e8669 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java @@ -39,6 +39,7 @@ public final class Relationship implements /** * While the direction in the schema package is centered around the node, the direction here is the direction between two nodes. + * @since 1.0 */ public enum Direction { /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SortItem.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SortItem.java index b9722e9e7..80a78565d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SortItem.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SortItem.java @@ -68,6 +68,10 @@ public final class SortItem implements Visitable { visitor.leave(this); } + /** + * Sort direction. + * @since 1.0 + */ public enum Direction implements Visitable { UNDEFINED(""), ASC("ASC"), DESC("DESC"); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java index 2d9b77981..f8a9a7a6a 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java @@ -39,9 +39,17 @@ public interface Statement extends Visitable { return new DefaultStatementBuilder(); } + /** + * Represents {@code RegularQuery}. + * @since 1.0 + */ interface RegularQuery extends Statement { } + /** + * Represents a {@code StandaloneCall}. + * @since 1.0 + */ interface SingleQuery extends RegularQuery { } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java index 06eae145d..441ab3137 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java @@ -32,29 +32,30 @@ import org.springframework.lang.Nullable; public interface StatementBuilder { /** + * @param pattern patterns to match + * @return An ongoing match * @see Cypher#optionalMatch(PatternElement...) - * @return */ OngoingReadingWithoutWhere optionalMatch(PatternElement... pattern); /** + * @param pattern patterns to match + * @return An ongoing match * @see Cypher#match(PatternElement...) - * @param pattern - * @return */ OngoingReadingWithoutWhere match(PatternElement... pattern); /** + * @param pattern patterns to create + * @return An ongoing merge * @see Cypher#create(PatternElement...) - * @param pattern - * @return */ T create(PatternElement... pattern); /** + * @param pattern patterns to merge + * @return An ongoing merge * @see Cypher#merge(PatternElement...) - * @param pattern - * @return */ T merge(PatternElement... pattern); @@ -64,6 +65,7 @@ public interface StatementBuilder { /** * An ongoing update statement that can be used to chain more update statements or add a with or return clause. + * @since 1.0 */ interface OngoingUpdate extends BuildableStatement, ExposesCreate, ExposesMerge, ExposesReturning, ExposesWith { } @@ -72,6 +74,7 @@ public interface StatementBuilder { * A match that exposes {@code returning} and {@code where} methods to add required information. * While the where clause is optional, an returning clause needs to be specified before the * statement can be build. + * @since 1.0 */ interface OngoingReadingWithoutWhere extends OngoingReading, ExposesMatch, ExposesCreate, ExposesMerge { @@ -86,10 +89,15 @@ public interface StatementBuilder { /** * A match that has a non-empty {@code where}-part. THe returning clause is still open. + * @since 1.0 */ interface OngoingReadingWithWhere extends OngoingReading, ExposesMatch, ExposesConditions { } + /** + * @param Type of ongoing query. + * @since 1.0 + */ interface ExposesConditions { /** * Adds an additional condition to the existing conditions, connected by an {@literal and}. @@ -115,10 +123,15 @@ public interface StatementBuilder { /** * A match that exposes {@code returning} and for which it is not decided whether the optional * where part has been used or note. + * @since 1.0 */ interface OngoingReading extends ExposesReturning, ExposesWith, ExposesUpdatingClause, ExposesUnwind { } + /** + * Builder part for unwinding. + * @since 1.0 + */ interface OngoingUnwind { OngoingReading as(String variable); @@ -126,12 +139,14 @@ public interface StatementBuilder { /** * A match that knows what to return and which is ready to be build. + * @since 1.0 */ interface OngoingReadingAndReturn extends ExposesOrderBy, ExposesSkip, ExposesLimit, BuildableStatement { } /** * A match that knows what to pipe to the next part of a multi part query. + * @since 1.0 */ interface OngoingReadingAndWithWithoutWhere extends OngoingReadingAndWith { @@ -144,15 +159,28 @@ public interface StatementBuilder { OngoingReadingAndWithWithWhere where(Condition condition); } + /** + * @see OngoingReadingAndWith + * @see ExposesConditions + * @since 1.0 + */ interface OngoingReadingAndWithWithWhere extends OngoingReadingAndWith, ExposesConditions { } + /** + * @see OngoingReading + * @since 1.0 + */ interface OngoingReadingAndWith extends OngoingReading, ExposesMatch, ExposesOrderBy, ExposesSkip, ExposesLimit, ExposesReturning, ExposesCreate, ExposesMerge { } + /** + * Combines the capabilities of skip, limit and adds additional expressions to the order-by items. + * @since 1.0 + */ interface OngoingMatchAndReturnWithOrder extends ExposesSkip, ExposesLimit, BuildableStatement { /** @@ -164,7 +192,8 @@ public interface StatementBuilder { } /** - * An intermediate step while defining the order of a resultset. + * An intermediate step while defining the order of a result set. + * @since 1.0 */ interface OngoingOrderDefinition extends ExposesSkip, ExposesLimit { @@ -185,6 +214,7 @@ public interface StatementBuilder { /** * A statement that has all information required to be build and exposes a build method. + * @since 1.0 */ interface BuildableStatement { @@ -194,6 +224,10 @@ public interface StatementBuilder { Statement build(); } + /** + * Return part of a statement. + * @since 1.0 + */ interface ExposesReturning { default OngoingReadingAndReturn returning(String... variables) { @@ -224,6 +258,7 @@ public interface StatementBuilder { /** * A step that exposes the {@code WITH} clause. + * @since 1.0 */ interface ExposesWith { @@ -254,6 +289,7 @@ public interface StatementBuilder { /** * A step that exposes several methods to specify ordering. + * @since 1.0 */ interface ExposesOrderBy { @@ -278,6 +314,7 @@ public interface StatementBuilder { /** * A step that exposes the {@link #skip(Number)} method. + * @since 1.0 */ interface ExposesSkip { @@ -293,6 +330,7 @@ public interface StatementBuilder { /** * A step that exposes the {@link #limit(Number)} method. + * @since 1.0 */ interface ExposesLimit { @@ -306,12 +344,14 @@ public interface StatementBuilder { /** * A step providing all the supported updating clauses (DELETE, SET) + * @since 1.0 */ interface ExposesUpdatingClause extends ExposesDelete, ExposesMerge, ExposesSetAndRemove { } /** * A step that exposes only the delete clause. + * @since 1.0 */ interface ExposesDelete { @@ -340,6 +380,10 @@ public interface StatementBuilder { T detachDelete(Expression... expressions); } + /** + * Set part of a statement. + * @since 1.0 + */ interface ExposesSet { /** @@ -354,6 +398,7 @@ public interface StatementBuilder { /** * A step that exposes the set clause. + * @since 1.0 */ interface ExposesSetAndRemove extends ExposesSet { @@ -366,6 +411,7 @@ public interface StatementBuilder { /** * A step exposing a {@link #match(PatternElement...)} method. + * @since 1.0 */ interface ExposesMatch { @@ -386,16 +432,28 @@ public interface StatementBuilder { OngoingReadingWithoutWhere optionalMatch(PatternElement... pattern); } + /** + * A step exposing a {@link #create(PatternElement...)} method. + * @since 1.0 + */ interface ExposesCreate { T create(PatternElement... pattern); } + /** + * A step exposing a {@link #merge(PatternElement...)} method. + * @since 1.0 + */ interface ExposesMerge { T merge(PatternElement... pattern); } + /** + * A step exposing a {@link #unwind(Expression...)},{@link #unwind(Expression)}, {@link #unwind(String)} and method. + * @since 1.0 + */ interface ExposesUnwind { default OngoingUnwind unwind(Expression... expressions) { @@ -411,6 +469,7 @@ public interface StatementBuilder { /** * A buildable step that will create a MATCH ... DELETE statement. + * @since 1.0 */ interface OngoingMatchAndUpdate extends ExposesReturning, ExposesWith, ExposesUpdatingClause { } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/Renderer.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/Renderer.java index 73cb6af1e..9a2e8589d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/Renderer.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/Renderer.java @@ -29,15 +29,15 @@ public interface Renderer { /** * Renders a statement. - * @param statement - * @return + * @param statement the statement to render + * @return The rendered Cypher statement. */ String render(Statement statement); /** * Provides the default renderer. This method may or may not provide shared instances of the renderer. * - * @return The default renderer + * @return The default renderer. */ static Renderer getDefaultRenderer() { return CypherRenderer.INSTANCE; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java index 0137e80a6..c3869f148 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java @@ -85,7 +85,7 @@ public abstract class ReflectiveVisitor implements Visitor { *

* The hook is called regardless wither a matching {@code leave} is found or not. * - * @param visitable + * @param visitable The visitable that is passed on to a matching leave after this call. */ protected abstract void postLeave(Visitable visitable); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/TypedSubtree.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/TypedSubtree.java index 92ec9d234..409397d28 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/TypedSubtree.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/TypedSubtree.java @@ -26,9 +26,9 @@ import java.util.List; * This class helps to group items of the same type on the same level of the tree into a list structure that can be * recognized by visitors. * + * @author Michael J. Simons * @param The children's type * @param The concrete type of the implementing class. - * @author Michael J. Simons * @since 1.0 */ public abstract class TypedSubtree> implements Visitable { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitable.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitable.java index cee50b70d..f034f58cb 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitable.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/Visitable.java @@ -23,9 +23,8 @@ import org.springframework.lang.Nullable; /** * Interface for implementations that accepts {@link Visitor visitors}. * - * @see Visitor - * * @author Michael Simons + * @see Visitor * @since 1.0 */ public interface Visitable { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java index b62d85d2a..61eb8573e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java @@ -30,7 +30,7 @@ import java.util.function.Function; import org.springframework.data.mapping.PersistentPropertyAccessor; /** - * @param + * @param type that should get mapped by the binder function * @author Michael J. Simons * @since 1.0 */ diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java index 26337f9b6..2d1497904 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java @@ -38,6 +38,7 @@ import org.springframework.util.Assert; /** * @author Michael J. Simons + * @since 1.0 */ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity implements Neo4jPersistentEntity { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java index bb88227e8..1a1d02576 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java @@ -30,6 +30,7 @@ import org.springframework.data.util.Lazy; /** * @author Michael J. Simons + * @since 1.0 */ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty implements Neo4jPersistentProperty { @@ -42,7 +43,7 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty owner, diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java index 08c7720bd..c84964d40 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java @@ -27,6 +27,7 @@ import org.neo4j.springframework.data.core.schema.RelationshipDescription; /** * @author Michael J. Simons + * @since 1.0 */ @RequiredArgsConstructor @Getter diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java index 42d268774..18bcbe450 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java @@ -55,6 +55,7 @@ import org.springframework.data.util.TypeInformation; * the initial set of classes through {@link #setInitialEntitySet(Set)}. * * @author Michael J. Simons + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public final class Neo4jMappingContext diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentEntity.java index 015ad2b77..1a72826cc 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentEntity.java @@ -32,6 +32,8 @@ import org.neo4j.springframework.data.core.schema.NodeDescription; * (in Spring Data terms: if it is an association). * * @author Michael J. Simons + * @param type of the underlying class + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public interface Neo4jPersistentEntity diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentProperty.java index 564eefbe2..bc115c843 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentProperty.java @@ -26,6 +26,7 @@ import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; * A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related to Neo4j. * * @author Michael J. Simons + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public interface Neo4jPersistentProperty diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Id.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Id.java index affe732b7..db9121c96 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Id.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Id.java @@ -44,6 +44,7 @@ public @interface Id { /** * Enumerating available strategies for providing ids for new entities. + * @since 1.0 */ enum Strategy { @@ -90,6 +91,10 @@ public @interface Id { */ Class generator() default NoopIdGenerator.class; + /** + * This {@link IdGenerator} does nothing. It is used for relying on the internal, database-side created id. + * @since 1.0 + */ // Needed to make the generator attribute defaultable (null is not a constant) class NoopIdGenerator implements IdGenerator { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Relationship.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Relationship.java index daa486837..286804ed6 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Relationship.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Relationship.java @@ -43,6 +43,7 @@ public @interface Relationship { /** * Enumeration of the direction a relationship can take. + * @since 1.0 */ enum Direction { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java index 4b50162b5..f656509dd 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java @@ -67,7 +67,7 @@ public interface Schema { Optional> getNodeDescription(Class underlyingClass); /** - * @param underlyingClass + * @param underlyingClass the type to find in the schema registry * @return The node description for the given. class * @throws UnknownEntityException When {@code targetClass} is not a known entity class. */ diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jSessionSynchronization.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jSessionSynchronization.java index 31b5b7c15..83650d100 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jSessionSynchronization.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jSessionSynchronization.java @@ -25,6 +25,8 @@ import org.springframework.transaction.support.TransactionSynchronization; /** * Neo4j specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when * participating in a non-native Neo4j transaction, such as a Jta transaction. + * + * @since 1.0 */ class Neo4jSessionSynchronization extends ResourceHolderSynchronization { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java index 2f728b647..c8976af1d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java @@ -20,6 +20,8 @@ package org.neo4j.springframework.data.core.transaction; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; +import java.util.Optional; + import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.springframework.transaction.support.ResourceHolderSupport; @@ -32,6 +34,7 @@ import org.springframework.util.Assert; * Note: Intended for internal usage only. * * @author Michael J. Simons + * @since 1.0 */ class Neo4jTransactionHolder extends ResourceHolderSupport { @@ -47,14 +50,14 @@ class Neo4jTransactionHolder extends ResourceHolderSupport { } /** - * Returns the transaction if it has been opened in a session for the requested database. + * Returns the transaction if it has been opened in a session for the requested database or an empty optional. * - * @param inDatabase - * @return + * @param inDatabase selected database to use + * @return An optional, ongoing transaction. */ - Transaction getTransaction(String inDatabase) { + Optional getTransaction(String inDatabase) { - return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? transaction : null; + return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? Optional.of(transaction) : Optional.empty(); } void commit() { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java index edb3db49e..ae3ac10d1 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java @@ -48,6 +48,7 @@ import org.springframework.util.Assert; * transaction manager will synchronize a pair of a native Neo4j session/transaction with the transaction. * * @author Michael J. Simons + * @since 1.0 */ @Slf4j @API(status = API.Status.STABLE, since = "1.0") @@ -94,7 +95,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager if (connectionHolder != null) { - return Optional.of(connectionHolder.getTransaction(targetDatabase)); + return connectionHolder.getTransaction(targetDatabase); } // Otherwise we open a session and synchronize it. @@ -108,7 +109,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager new Neo4jSessionSynchronization(connectionHolder, driver)); TransactionSynchronizationManager.bindResource(driver, connectionHolder); - return Optional.of(connectionHolder.getTransaction(targetDatabase)); + return connectionHolder.getTransaction(targetDatabase); } private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java index 26496b432..c99ecc7a4 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java @@ -32,6 +32,8 @@ import org.springframework.transaction.TransactionDefinition; /** * Internal use only. + * + * @since 1.0 */ public final class Neo4jTransactionUtils { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jSessionSynchronization.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jSessionSynchronization.java index 239a8db57..0a81929a7 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jSessionSynchronization.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jSessionSynchronization.java @@ -28,6 +28,7 @@ import org.springframework.transaction.reactive.TransactionSynchronizationManage /** * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ class ReactiveNeo4jSessionSynchronization extends ReactiveResourceSynchronization { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java index dbca74d6c..ade47bbb7 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java @@ -22,6 +22,8 @@ import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUt import reactor.core.publisher.Mono; +import java.util.Optional; + import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxTransaction; import org.springframework.transaction.support.ResourceHolderSupport; @@ -29,6 +31,7 @@ import org.springframework.transaction.support.ResourceHolderSupport; /** * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport { @@ -47,9 +50,9 @@ class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport { return session; } - RxTransaction getTransaction(String inDatabase) { + Optional getTransaction(String inDatabase) { - return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? transaction : null; + return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? Optional.of(transaction) : Optional.empty(); } Mono commit() { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index bae09ccf1..6e93e4340 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -44,6 +44,7 @@ import org.springframework.util.Assert; /** * @author Gerrit Meier + * @since 1.0 */ @API(status = API.Status.STABLE, since = "1.0") public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransactionManager { @@ -98,7 +99,7 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction }); }); }) - .map(connectionHolder -> connectionHolder.getTransaction(targetDatabase)) + .map(connectionHolder -> connectionHolder.getTransaction(targetDatabase).get()) // If not, than just don't open a transaction .onErrorResume(NoTransactionException.class, nte -> Mono.empty()); } @@ -181,7 +182,7 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager .getResource(driver); - return Mono.from(holder.getTransaction(databaseName).commit()).then(); + return Mono.from(holder.getTransaction(databaseName).get().commit()).then(); } @Override @@ -190,7 +191,7 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager .getResource(driver); - return Mono.from(holder.getTransaction(databaseName).rollback()).then(); + return Mono.from(holder.getTransaction(databaseName).get().rollback()).then(); } /* * (non-Javadoc) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/Neo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/Neo4jRepository.java index 65e277ce2..35a2b0956 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/Neo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/Neo4jRepository.java @@ -26,6 +26,9 @@ import org.springframework.data.repository.query.QueryByExampleExecutor; * Neo4j specific {@link org.springframework.data.repository.Repository} interface. * * @author Michael J. Simons + * @param type of the domain class to map + * @param identifier type in the domain class + * @since 1.0 */ @NoRepositoryBean public interface Neo4jRepository extends PagingAndSortingRepository, QueryByExampleExecutor { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/ReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/ReactiveNeo4jRepository.java index 2ff126360..501aaf721 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/ReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/ReactiveNeo4jRepository.java @@ -26,6 +26,9 @@ import org.springframework.data.repository.reactive.ReactiveSortingRepository; * Neo4j specific {@link org.springframework.data.repository.Repository} interface with reactive support. * * @author Michael J. Simons + * @param type of the domain class to map + * @param identifier type in the domain class + * @since 1.0 */ @NoRepositoryBean public interface ReactiveNeo4jRepository diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java index 5704d1f35..827a7f269 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java @@ -39,6 +39,7 @@ import org.neo4j.springframework.data.repository.support.Neo4jRepositoryFactoryB * configuration class. * * @author Gerrit Meier + * @since 1.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java index 3a0269e1a..9d131c006 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java @@ -39,6 +39,7 @@ import org.neo4j.springframework.data.repository.support.ReactiveNeo4jRepository * configuration class. * * @author Gerrit Meier + * @since 1.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoriesRegistrar.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoriesRegistrar.java index bbb90de83..397ca13ff 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoriesRegistrar.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoriesRegistrar.java @@ -31,6 +31,7 @@ import org.springframework.data.repository.config.RepositoryConfigurationExtensi * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ class Neo4jRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java index f57ac7b86..c457e3b0e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -39,6 +39,7 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; * * @author Michael J. Simons * @author Gerrit Meier + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoriesRegistrar.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoriesRegistrar.java index d7783c1b0..e08bfc761 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoriesRegistrar.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoriesRegistrar.java @@ -31,6 +31,7 @@ import org.springframework.data.repository.config.RepositoryConfigurationExtensi * * @author Gerrit Meier * @author Michael J. Simons + * @since 1.0 */ class ReactiveNeo4jRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java index a3ec28c02..7d1f01cf0 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java @@ -40,6 +40,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; * * @author Michael J. Simons * @author Gerrit Meier + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public final class ReactiveNeo4jRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java index 42c7e549c..ab4ebb5aa 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/AbstractNeo4jQuery.java @@ -66,9 +66,7 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor protected abstract PreparedQuery prepareQuery(Object[] parameters); /** - * Returns whether the query should get a count projection applied. - * - * @return + * @return True if the query should get a count projection applied. */ protected abstract boolean isCountQuery(); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java index 3a181e51e..7e6edeede 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java @@ -56,7 +56,7 @@ public final class CypherAdapterUtils { * Maps Spring Datas {@link Sort.Order} to a {@link SortItem}. * See {@link #toSortItems(NodeDescription, Sort)}. * - * @param nodeDescription + * @param nodeDescription {@link NodeDescription} to get properties for sorting from. * @return A stream if sort items. Will be empty when sort is unsorted. */ public static Function sortAdapterFor(NodeDescription nodeDescription) { @@ -93,7 +93,7 @@ public final class CypherAdapterUtils { * Provides a builder for statements based on the schema of entity classes. * * @param schema The schema used for creating queries. - * @return + * @return new {@link SchemaBasedStatementBuilder} instance based on the given {@link Schema} */ public static SchemaBasedStatementBuilder createSchemaBasedStatementBuilder(Schema schema) { return new SchemaBasedStatementBuilder(schema); @@ -101,6 +101,8 @@ public final class CypherAdapterUtils { /** * This is a adapter between the Schema and the Cypher module. + * + * @since 1.0 */ public static class SchemaBasedStatementBuilder { @@ -111,8 +113,8 @@ public final class CypherAdapterUtils { } /** - * @param nodeDescription - * @return + * @param nodeDescription The node description for which a match clause should be generated + * @return An ongoing match * @see #prepareMatchOf(NodeDescription, Optional) */ public OngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription) { @@ -224,8 +226,8 @@ public final class CypherAdapterUtils { } /** - * @param nodeDescription - * @return + * @param nodeDescription The node description for which a id expression gets generated + * @return An expression that represents the right identifier type. * @see Neo4jEntityInformation#getIdExpression() */ public static Expression createIdExpression(final NodeDescription nodeDescription) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java index f8154cf8d..cbc7d3e6f 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java @@ -224,22 +224,22 @@ final class CypherQueryCreator extends AbstractQueryCreator { /** * Checks whether or not to ignore the case for some operations. {@link PartTreeNeo4jQuery} will already have validated - * which properties can be made case insenstive given a certain keyword. + * which properties can be made case insensitive given a certain keyword. * - * @param p - * @return + * @param part query part to get checked if case should get ignored + * @return should the case get ignored */ - boolean ignoreCase(Part p) { + boolean ignoreCase(Part part) { - switch (p.shouldIgnoreCase()) { + switch (part.shouldIgnoreCase()) { case ALWAYS: return true; case WHEN_POSSIBLE: - return canIgnoreCase(p); + return canIgnoreCase(part); case NEVER: return false; default: - throw new IllegalArgumentException("Unsupported option for ignoring case: " + p.shouldIgnoreCase()); + throw new IllegalArgumentException("Unsupported option for ignoring case: " + part.shouldIgnoreCase()); } } @@ -322,8 +322,8 @@ final class CypherQueryCreator extends AbstractQueryCreator { } /** - * @param property - * @param rangeParameter + * @param property property for which the range should get checked + * @param rangeParameter parameter that expresses the range * @return The equivalent of a A BETWEEN B AND C expression for a given range. */ private static Condition createRangeConditionForProperty(Expression property, Parameter rangeParameter) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java index 6cf50354e..b67ed514e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java @@ -61,7 +61,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { /** * A set of the temporal types that are directly passable to the driver and support a meaningful comparision in a * temporal sense (after, before). - * See */ private static final Set> COMPARABLE_TEMPORAL_TYPES = Collections .unmodifiableSet(new HashSet<>(Arrays.asList(LocalDate.class, OffsetTime.class, ZonedDateTime.class, @@ -112,7 +112,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { /** * Checks whether the given part can be queried without case sensitivity. * - * @param part + * @param part query part to check if ignoring case sensitivity is possible * @return True when {@code part} can be queried case insensitive. */ static boolean canIgnoreCase(Part part) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Query.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Query.java index c1b775789..d44fc98d8 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Query.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Query.java @@ -29,8 +29,8 @@ import org.springframework.data.annotation.QueryAnnotation; /** * Annotation to provide Cypher statements that will be used for executing the method. The Cypher statement may contain named - * parameters as supported by the >Neo4j Java Driver>Neo4j Java Driver. + * Those parameters will get bound to the arguments of the annotated method. * * @author Michael J. Simons * @since 1.0 diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java index afc53077b..7d3b4a833 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java @@ -60,7 +60,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { /** * A set of the temporal types that are directly passable to the driver and support a meaningful comparision in a * temporal sense (after, before). - * See */ private static final Set> COMPARABLE_TEMPORAL_TYPES = Collections .unmodifiableSet(new HashSet<>(Arrays.asList(LocalDate.class, OffsetTime.class, ZonedDateTime.class, @@ -111,7 +111,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { /** * Checks whether the given part can be queried without case sensitivity. * - * @param part + * @param part query part to check if ignoring case sensitivity is possible * @return True when {@code part} can be queried case insensitive. */ static boolean canIgnoreCase(Part part) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java index 48c760d13..b1e1dc1cd 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -92,10 +92,10 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { * Create a {@link ReactiveStringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The annotation * is expected to have a value. * - * @param neo4jClient - * @param mappingContext - * @param evaluationContextProvider - * @param queryMethod + * @param neo4jClient a reactive Neo4jClient instance + * @param mappingContext a Neo4jMappingContext instance + * @param evaluationContextProvider a QueryMethodEvaluationContextProvider instance + * @param queryMethod the query method * @return A new instance of a String based Neo4j query. */ static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext, @@ -116,10 +116,10 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { /** * Create a {@link ReactiveStringBasedNeo4jQuery} based on an explicit Cypher template. * - * @param neo4jClient - * @param mappingContext - * @param evaluationContextProvider - * @param queryMethod + * @param neo4jClient a reactive Neo4jClient instance + * @param mappingContext a Neo4jMappingContext instance + * @param evaluationContextProvider a QueryMethodEvaluationContextProvider instance + * @param queryMethod the query method * @param cypherTemplate The template to use. * @return A new instance of a String based Neo4j query. */ @@ -209,9 +209,9 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { } /** - * @param index + * @param index position of this parameter placeholder * @param originalSpelExpression Not used for configuring parameter names atm. - * @return + * @return A new parameter name for the given index. */ private static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { return "__SpEL__" + index; @@ -220,8 +220,8 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { /** * @param originalPrefix The prefix passed to the replacement source is either ':' or '?', so that isn't usable for * Cypher templates and therefore ignored. - * @param parameterName - * @return + * @param parameterName name of the parameter + * @return The name of the parameter in its native Cypher form. */ private static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { return "$" + parameterName; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java index bc3fb1006..23e192718 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java @@ -92,10 +92,10 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { * Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The annotation * is expected to have a value. * - * @param neo4jClient - * @param mappingContext - * @param evaluationContextProvider - * @param queryMethod + * @param neo4jClient a reactive Neo4jClient instance + * @param mappingContext a Neo4jMappingContext instance + * @param evaluationContextProvider a QueryMethodEvaluationContextProvider instance + * @param queryMethod the query method * @return A new instance of a String based Neo4j query. */ static StringBasedNeo4jQuery create(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, @@ -116,10 +116,10 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { /** * Create a {@link StringBasedNeo4jQuery} based on an explicit Cypher template. * - * @param neo4jClient - * @param mappingContext - * @param evaluationContextProvider - * @param queryMethod + * @param neo4jClient a reactive Neo4jClient instance + * @param mappingContext a Neo4jMappingContext instance + * @param evaluationContextProvider a QueryMethodEvaluationContextProvider instance + * @param queryMethod the query method * @param cypherTemplate The template to use. * @return A new instance of a String based Neo4j query. */ @@ -209,9 +209,9 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { } /** - * @param index + * @param index position of this parameter placeholder * @param originalSpelExpression Not used for configuring parameter names atm. - * @return + * @return A new parameter name for the given index. */ private static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { return "__SpEL__" + index; @@ -220,8 +220,8 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { /** * @param originalPrefix The prefix passed to the replacement source is either ':' or '?', so that isn't usable for * Cypher templates and therefore ignored. - * @param parameterName - * @return + * @param parameterName name of the parameter + * @return The name of the parameter in its native Cypher form. */ private static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { return "$" + parameterName; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java index 3b1ff541e..205eabe4c 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java @@ -31,9 +31,9 @@ import org.springframework.data.repository.core.EntityInformation; /** * Neo4j specific contract for {@link EntityInformation entity informations}. * + * @author Michael J. Simons * @param The type of the entity * @param The type of the id - * @author Michael J. Simons * @soundtrack Bear McCreary - Battlestar Galactica Season 1 * @since 1.0 */ diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java index 41d21f61e..534563226 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java @@ -33,6 +33,10 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF * * @author Michael J. Simons * @author Gerrit Meier + * @param the type of the repository + * @param type of the domain class to map + * @param identifier type in the domain class + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public final class Neo4jRepositoryFactoryBean, S, ID extends Serializable> diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index 4c596c263..eeff67204 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -32,6 +32,10 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF * repository factories via Spring configuration. * * @author Gerrit Meier + * @param the type of the repository + * @param type of the domain class to map + * @param identifier type in the domain class + * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") public final class ReactiveNeo4jRepositoryFactoryBean, S, ID extends Serializable> diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java index 8f3a22dfe..36809527c 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java @@ -52,8 +52,8 @@ import org.springframework.data.repository.support.PageableExecutionUtils; /** * A fragment for repositories providing "Query by example" functionality. * - * @param * @author Michael J. Simons + * @param type of the domain class * @since 1.0 */ @Slf4j diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java index 1aa164ac5..775c3c98a 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java @@ -46,9 +46,9 @@ import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; /** * A fragment for repositories providing "Query by example" functionality in a reactive way. * - * @param * @author Gerrit Meier * @author Michael J. Simons + * @param type of the domain class * @since 1.0 */ @Slf4j diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java index e3bc86ee5..1c42b37ed 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java @@ -26,6 +26,9 @@ import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.core.schema.Property; +/** + * Example domain object with default constructor. + */ @Getter @Setter @Node diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java index 38d279fa4..2031be2a6 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java @@ -25,6 +25,10 @@ import lombok.ToString; import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.Node; +/** + * Example domain object that works with wither. + * Read more about this here. + */ @Getter @Setter @Node diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java index 99b680da4..feb83b636 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java @@ -42,6 +42,7 @@ import org.testcontainers.containers.Neo4jContainer; * otherwise it will start a test container and use that connection. * * @author Michael J. Simons + * @since 1.0 */ @Slf4j public class Neo4jExtension implements BeforeAllCallback { @@ -84,6 +85,10 @@ public class Neo4jExtension implements BeforeAllCallback { field.set(null, neo4jConnectionSupport); } + /** + * Support class that holds the connection information and opens a new connection on demand. + * @since 1.0 + */ public static class Neo4jConnectionSupport { public final String url; From 9abe11668922260f0a434b643ae4a36a52aba270 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 12 Jun 2019 16:49:36 +0200 Subject: [PATCH 119/342] Add test case for Kotlin based repository. --- .../data/integration/kotlin/KotlinIT.java | 94 +++++++++++++++++++ .../integration/kotlin/KotlinRepository.kt | 28 ++++++ 2 files changed, 122 insertions(+) create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java create mode 100644 spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/kotlin/KotlinRepository.kt diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java new file mode 100644 index 000000000..8e87609a1 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.kotlin; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collection; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.KotlinPerson; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Gerrit Meier + */ +@ExtendWith(SpringExtension.class) +@ExtendWith(Neo4jExtension.class) +@ContextConfiguration(classes = KotlinIT.Config.class) +class KotlinIT { + + private final static String PERSON_NAME = "test"; + + private static Neo4jConnectionSupport neo4jConnectionSupport; + + @Autowired + private KotlinRepository repository; + + @Autowired + private Driver driver; + + @BeforeEach + void setup() { + Session session = driver.session(); + Transaction transaction = session.beginTransaction(); + transaction.run("MATCH (n) detach delete n"); + transaction.run("CREATE (n:KotlinPerson) SET n.name = $personName", Values.parameters("personName", PERSON_NAME)); + transaction.success(); + transaction.close(); + session.close(); + } + + @Test + void findAllKotlinPersons() { + Iterable person = repository.findAll(); + assertThat(person.iterator().next().getName()).isEqualTo(PERSON_NAME); + } + + @Configuration + @EnableNeo4jRepositories + @EnableTransactionManagement + static class Config extends AbstractNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + public Collection getMappingBasePackages() { + return Collections.singletonList(KotlinPerson.class.getPackage().getName()); + } + } +} diff --git a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/kotlin/KotlinRepository.kt b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/kotlin/KotlinRepository.kt new file mode 100644 index 000000000..786a945b8 --- /dev/null +++ b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/kotlin/KotlinRepository.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.kotlin + +import org.neo4j.springframework.data.integration.shared.KotlinPerson +import org.neo4j.springframework.data.repository.Neo4jRepository + +/** + * @author Gerrit Meier + */ +interface KotlinRepository : Neo4jRepository From 73e368f3dcde691021c92e05335814f00ac9e6c9 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 12 Jun 2019 16:40:15 +0200 Subject: [PATCH 120/342] Add reactive deletes. --- .../support/SimpleNeo4jRepository.java | 5 +- .../SimpleReactiveNeo4jRepository.java | 107 +++++++++++++++--- .../reactive/ReactiveRepositoryIT.java | 75 +++++++++++- 3 files changed, 165 insertions(+), 22 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java index 6a3adaad0..f88518116 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java @@ -216,8 +216,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository log.debug("Deleting entity with id {} ", id); - Statement statement = statementBuilder - .prepareDeleteOf(entityMetaData, Optional.of(condition)); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) .bind(id).to(nameOfParameter) .run(); @@ -230,7 +229,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Transactional public void delete(T entity) { - ID id = (ID) this.entityInformation.getId(entity); + ID id = this.entityInformation.getId(entity); this.deleteById(id); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java index 72fe0550b..6d28dfeb5 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java @@ -29,19 +29,22 @@ import reactor.core.publisher.Mono; import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; -import org.reactivestreams.Publisher; -import org.springframework.data.domain.Sort; import org.neo4j.springframework.data.core.PreparedQuery; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; import org.neo4j.springframework.data.core.ReactiveNeo4jClient.ExecutableQuery; +import org.neo4j.springframework.data.core.cypher.Condition; import org.neo4j.springframework.data.core.cypher.Functions; import org.neo4j.springframework.data.core.cypher.Statement; import org.neo4j.springframework.data.core.cypher.renderer.Renderer; import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; /** * Repository base implementation for Neo4j. @@ -153,28 +156,104 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< return null; } - @Override public Mono deleteById(ID id) { - return null; + /* + * (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteById(java.lang.Object) + */ + @Override + @Transactional + public Mono deleteById(ID id) { + + Assert.notNull(id, "The given id must not be null!"); + + return Mono.just(id).as(this::deleteById); } - @Override public Mono deleteById(Publisher id) { - return null; + /* + * (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#delete(java.lang.Object) + */ + @Override + @Transactional + public Mono delete(T entity) { + + Assert.notNull(entity, "The given entity must not be null!"); + + return Mono.just(entity) + .map(this.entityInformation::getId) + .as(this::deleteById); } - @Override public Mono delete(T entity) { - return null; + /* + * (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteById(org.reactivestreams.Publisher) + */ + @Override + @Transactional + public Mono deleteById(Publisher idPublisher) { + + Assert.notNull(idPublisher, "The given Publisher of an id must not be null!"); + + return Mono.from(idPublisher) + .flatMap(id -> { + String nameOfParameter = "id"; + Condition condition = this.entityInformation.getIdExpression().isEqualTo(parameter(nameOfParameter)); + + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + return this.neo4jClient.query(() -> renderer.render(statement)) + .bind(id).to(nameOfParameter).run(); + }) + .then(); } - @Override public Mono deleteAll(Iterable entities) { - return null; + /* + * (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll(java.lang.Iterable) + */ + @Override + @Transactional + public Mono deleteAll(Iterable entities) { + + Assert.notNull(entities, "The given Iterable of entities must not be null!"); + + return Flux.fromIterable(entities) + .as(this::deleteAll); } - @Override public Mono deleteAll(Publisher entityStream) { - return null; + /* + * (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll(org.reactivestreams.Publisher) + */ + @Override + @Transactional + public Mono deleteAll(Publisher entitiesPublisher) { + + Assert.notNull(entitiesPublisher, "The given Publisher of entities must not be null!"); + + return Flux.from(entitiesPublisher) + .map(this.entityInformation::getId) + .collect(Collectors.toList()) + .flatMap(ids -> { + + String nameOfParameter = "ids"; + Condition condition = entityInformation.getIdExpression().in(parameter(nameOfParameter)); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + + return this.neo4jClient.query(() -> renderer.render(statement)).bind(ids).to(nameOfParameter).run(); + }) + .then(); } - @Override public Mono deleteAll() { - return null; + /* + * (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll() + */ + @Override + @Transactional + public Mono deleteAll() { + + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.empty()); + return this.neo4jClient.query(() -> renderer.render(statement)).run().then(); } private ExecutableQuery createExecutableQuery(Statement statement) { diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index 5055d1a49..7d8b67678 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -35,17 +35,17 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; import org.neo4j.driver.Values; import org.neo4j.driver.types.Point; +import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Sort; -import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; -import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; -import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; -import org.neo4j.springframework.data.test.Neo4jExtension; -import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -317,6 +317,71 @@ class ReactiveRepositoryIT { .expectNextMatches(personList::contains).verifyComplete(); } + @Test + void deleteAll() { + + repository.deleteAll() + .then(repository.count()) + .as(StepVerifier::create) + .expectNext(0L) + .verifyComplete(); + } + + @Test + void deleteById() { + + repository.deleteById(id1) + .then(repository.existsById(id1)) + .concatWith(repository.existsById(id2)) + .as(StepVerifier::create) + .expectNext(false, true) + .verifyComplete(); + } + + @Test + void deleteByIdPublisher() { + + repository.deleteById(Mono.just(id1)) + .then(repository.existsById(id1)) + .concatWith(repository.existsById(id2)) + .as(StepVerifier::create) + .expectNext(false, true) + .verifyComplete(); + } + + @Test + void delete() { + + repository.delete(person1) + .then(repository.existsById(id1)) + .concatWith(repository.existsById(id2)) + .as(StepVerifier::create) + .expectNext(false, true) + .verifyComplete(); + } + + @Test + void deleteAllEntities() { + + repository.deleteAll(Arrays.asList(person1, person2)) + .then(repository.existsById(id1)) + .concatWith(repository.existsById(id2)) + .as(StepVerifier::create) + .expectNext(false, false) + .verifyComplete(); + } + + @Test + void deleteAllEntitiesPublisher() { + + repository.deleteAll(Flux.just(person1, person2)) + .then(repository.existsById(id1)) + .concatWith(repository.existsById(id2)) + .as(StepVerifier::create) + .expectNext(false, false) + .verifyComplete(); + } + @Configuration @EnableReactiveNeo4jRepositories @EnableTransactionManagement From f96b28535a899f4afd9d6dee6f229827670a4315 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 14 Jun 2019 14:33:24 +0200 Subject: [PATCH 121/342] Rework Spring Boot autoconfiguration. This introduces separate names for the imperative and reactive versions of client and transaction manager and configures stuff accordingly. It also takes care that the repository configuration extensions answers useRepositoryConfiguration correct. --- .../data/config/AbstractNeo4jConfig.java | 5 +++-- .../config/AbstractReactiveNeo4jConfig.java | 3 ++- .../data/config/Neo4jConfigurationSupport.java | 3 ++- .../ReactiveNeo4jConfigurationSupport.java | 5 +++-- .../config/EnableNeo4jRepositories.java | 2 +- .../config/EnableReactiveNeo4jRepositories.java | 5 +++-- .../Neo4jRepositoryConfigurationExtension.java | 17 +++++++++++++---- ...veNeo4jRepositoryConfigurationExtension.java | 12 +++++++----- .../support/Neo4jRepositoryFactoryBean.java | 2 ++ .../ReactiveNeo4jRepositoryFactoryBean.java | 2 ++ 10 files changed, 38 insertions(+), 18 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractNeo4jConfig.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractNeo4jConfig.java index e02e8438d..535474bac 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractNeo4jConfig.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractNeo4jConfig.java @@ -20,9 +20,10 @@ package org.neo4j.springframework.data.config; import org.apiguardian.api.API; import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.neo4j.springframework.data.core.Neo4jClient; /** * Base class for imperative SDN-RX configuration using JavaConfig. @@ -42,7 +43,7 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport { * @param driver The driver to connect with. * @return A imperative Neo4j client. */ - @Bean + @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) public Neo4jClient neo4jClient(Driver driver) { return Neo4jClient.create(driver); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractReactiveNeo4jConfig.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractReactiveNeo4jConfig.java index 89e01a25d..f08d6763e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractReactiveNeo4jConfig.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/AbstractReactiveNeo4jConfig.java @@ -20,6 +20,7 @@ package org.neo4j.springframework.data.config; import org.apiguardian.api.API; import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; @@ -41,7 +42,7 @@ public abstract class AbstractReactiveNeo4jConfig extends ReactiveNeo4jConfigura * @param driver The driver to connect with. * @return A reactive Neo4j client. */ - @Bean + @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) public ReactiveNeo4jClient neo4jClient(Driver driver) { return ReactiveNeo4jClient.create(driver); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java index 634321f67..388307328 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jConfigurationSupport.java @@ -25,6 +25,7 @@ import java.util.Set; import org.apiguardian.api.API; import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -77,7 +78,7 @@ public abstract class Neo4jConfigurationSupport { * @param driver The driver to synchronize against * @return A platform transaction manager */ - @Bean + @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) public PlatformTransactionManager transactionManager(Driver driver) { return new Neo4jTransactionManager(driver); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java index 5597af684..f7579ba44 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/ReactiveNeo4jConfigurationSupport.java @@ -25,6 +25,7 @@ import java.util.Set; import org.apiguardian.api.API; import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -78,8 +79,8 @@ public abstract class ReactiveNeo4jConfigurationSupport { * @param driver The driver to synchronize against * @return A platform transaction manager */ - @Bean - public ReactiveTransactionManager transactionManager(Driver driver) { + @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) + public ReactiveTransactionManager reactiveTransactionManager(Driver driver) { return new ReactiveNeo4jTransactionManager(driver); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java index 827a7f269..303740a2a 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java @@ -84,7 +84,7 @@ public @interface EnableNeo4jRepositories { /** * Configures the name of the {@link org.neo4j.springframework.data.core.Neo4jClient} bean to be used with the repositories detected. */ - String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_NAME; + String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_BEAN_NAME; /** * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java index 9d131c006..587207ac9 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java @@ -18,7 +18,7 @@ */ package org.neo4j.springframework.data.repository.config; -import static org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension.*; +import static org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension.*; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -39,6 +39,7 @@ import org.neo4j.springframework.data.repository.support.ReactiveNeo4jRepository * configuration class. * * @author Gerrit Meier + * @author Michael J. Simons * @since 1.0 */ @Target(ElementType.TYPE) @@ -84,7 +85,7 @@ public @interface EnableReactiveNeo4jRepositories { /** * Configures the name of the {@link org.neo4j.springframework.data.core.Neo4jClient} bean to be used with the repositories detected. */ - String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_NAME; + String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_BEAN_NAME; /** * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java index c457e3b0e..f25b8e7e3 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -23,13 +23,14 @@ import java.util.Collection; import java.util.Collections; import org.apiguardian.api.API; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.repository.Neo4jRepository; import org.neo4j.springframework.data.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.data.repository.core.RepositoryMetadata; /** * This dedicated Neo4j repository extension will be registered via {@link Neo4jRepositoriesRegistrar} and then provide @@ -49,7 +50,9 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi /** * See {@link AbstractBeanDefinition#INFER_METHOD}. */ - static final String DEFAULT_NEO4J_CLIENT_NAME = "neo4jClient"; + public static final String DEFAULT_NEO4J_CLIENT_BEAN_NAME = "neo4jClient"; + + public static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; /** * See {@link AbstractBeanDefinition#INFER_METHOD}. @@ -84,6 +87,12 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi return Collections.singleton(Neo4jRepository.class); } + @Override + protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { + + return !metadata.isReactiveRepository(); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) @@ -92,7 +101,7 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { builder.addPropertyReference("neo4jClient", - source.getAttribute("neo4jClientRef").orElse(DEFAULT_NEO4J_CLIENT_NAME)); + source.getAttribute("neo4jClientRef").orElse(DEFAULT_NEO4J_CLIENT_BEAN_NAME)); builder.addPropertyReference("neo4jMappingContext", source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java index 7d1f01cf0..78ed57013 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java @@ -23,11 +23,11 @@ import java.util.Collection; import java.util.Collections; import org.apiguardian.api.API; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.repository.Neo4jRepository; import org.neo4j.springframework.data.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; @@ -50,7 +50,9 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit /** * See {@link AbstractBeanDefinition#INFER_METHOD}. */ - static final String DEFAULT_NEO4J_CLIENT_NAME = "neo4jClient"; + public static final String DEFAULT_NEO4J_CLIENT_BEAN_NAME = "reactiveNeo4jClient"; + + public static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "reactiveTransactionManager"; /** * See {@link AbstractBeanDefinition#INFER_METHOD}. @@ -87,7 +89,7 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit @Override protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { - return true; + return metadata.isReactiveRepository(); } /* @@ -98,7 +100,7 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { builder.addPropertyReference("neo4jClient", - source.getAttribute("neo4jClientRef").orElse(DEFAULT_NEO4J_CLIENT_NAME)); + source.getAttribute("neo4jClientRef").orElse(DEFAULT_NEO4J_CLIENT_BEAN_NAME)); builder.addPropertyReference("neo4jMappingContext", source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java index 534563226..c07309841 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java @@ -23,6 +23,7 @@ import java.io.Serializable; import org.apiguardian.api.API; import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; @@ -53,6 +54,7 @@ public final class Neo4jRepositoryFactoryBean, S, ID */ protected Neo4jRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); + super.setTransactionManager(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME); } public void setNeo4jClient(Neo4jClient neo4jClient) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index eeff67204..474a23cf4 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -23,6 +23,7 @@ import java.io.Serializable; import org.apiguardian.api.API; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; @@ -52,6 +53,7 @@ public final class ReactiveNeo4jRepositoryFactoryBean repositoryInterface) { super(repositoryInterface); + super.setTransactionManager(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME); } public void setNeo4jClient(ReactiveNeo4jClient neo4jClient) { From 8026998a8011e1acbf912c966f57283974630c30 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 14 Jun 2019 14:46:21 +0200 Subject: [PATCH 122/342] Commit with holder, analogue to imperative. --- .../core/transaction/ReactiveNeo4jTransactionManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 6e93e4340..2564cfae6 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -182,7 +182,7 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager .getResource(driver); - return Mono.from(holder.getTransaction(databaseName).get().commit()).then(); + return holder.commit().then(); } @Override @@ -191,7 +191,7 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager .getResource(driver); - return Mono.from(holder.getTransaction(databaseName).get().rollback()).then(); + return holder.rollback().then(); } /* * (non-Javadoc) From 25fc6ffcd7d7225843ce22793fb9fa4eb0becca3 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 14 Jun 2019 15:07:59 +0200 Subject: [PATCH 123/342] Add prelimary support for saving entities. Co-authored-by: Gerrit Meier --- .../support/Neo4jRepositoryFactory.java | 4 +- .../ReactiveNeo4jRepositoryFactory.java | 5 +- .../ReactiveNeo4jRepositoryFactoryBean.java | 1 + .../SimpleReactiveNeo4jRepository.java | 59 ++++++- .../integration/imperative/RepositoryIT.java | 42 +++-- .../reactive/ReactiveRepositoryIT.java | 162 +++++++++++++++++- .../reactive/ReactiveThingRepository.java | 9 +- 7 files changed, 242 insertions(+), 40 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java index a5421d713..6a1934b96 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java @@ -69,9 +69,9 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); BiFunction mappingFunction = mappingContext.getRequiredMappingFunctionFor(domainClass); - Function> binderFUnction = mappingContext.getRequiredBinderFunctionFor(domainClass); + Function> binderFunction = mappingContext.getRequiredBinderFunctionFor(domainClass); - return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction, binderFUnction); + return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction, binderFunction); } @Override diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java index 1ce112bf4..173d62131 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -18,8 +18,10 @@ */ package org.neo4j.springframework.data.repository.support; +import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; +import java.util.function.Function; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; @@ -66,8 +68,9 @@ final class ReactiveNeo4jRepositoryFactory extends RepositoryFactorySupport { Neo4jPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); BiFunction mappingFunction = mappingContext.getRequiredMappingFunctionFor(domainClass); + Function> binderFunction = mappingContext.getRequiredBinderFunctionFor(domainClass); - return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction, null); + return new DefaultNeo4jEntityInformation<>((Neo4jPersistentEntity) entity, mappingFunction, binderFunction); } @Override diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index 474a23cf4..889fc08b4 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -33,6 +33,7 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF * repository factories via Spring configuration. * * @author Gerrit Meier + * @author Michael J. Simons * @param the type of the repository * @param type of the domain class to map * @param identifier type in the domain class diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java index 6d28dfeb5..c1963194d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java @@ -20,6 +20,7 @@ package org.neo4j.springframework.data.repository.support; import static java.util.Collections.*; import static org.neo4j.springframework.data.core.cypher.Cypher.*; +import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import lombok.extern.slf4j.Slf4j; @@ -29,8 +30,10 @@ import reactor.core.publisher.Mono; import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; +import org.neo4j.driver.summary.SummaryCounters; import org.neo4j.springframework.data.core.PreparedQuery; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; import org.neo4j.springframework.data.core.ReactiveNeo4jClient.ExecutableQuery; @@ -38,9 +41,10 @@ import org.neo4j.springframework.data.core.cypher.Condition; import org.neo4j.springframework.data.core.cypher.Functions; import org.neo4j.springframework.data.core.cypher.Statement; import org.neo4j.springframework.data.core.cypher.renderer.Renderer; -import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; import org.reactivestreams.Publisher; import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -64,12 +68,12 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< private final Neo4jEntityInformation entityInformation; - private final NodeDescription entityMetaData; + private final Neo4jPersistentEntity entityMetaData; private final SchemaBasedStatementBuilder statementBuilder; SimpleReactiveNeo4jRepository(ReactiveNeo4jClient neo4jClient, Neo4jEntityInformation entityInformation, - SchemaBasedStatementBuilder statementBuilder) { + SchemaBasedStatementBuilder statementBuilder) { this.neo4jClient = neo4jClient; this.entityInformation = entityInformation; this.entityMetaData = this.entityInformation.getEntityMetaData(); @@ -144,16 +148,53 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< .getSingleResult(); } - @Override public Mono save(S entity) { - return null; + @Override + @Transactional + public Mono save(S entity) { + + Statement saveStatement = statementBuilder.prepareSaveOf(entityMetaData); + + Mono idMono = this.neo4jClient.query(() -> renderer.render(saveStatement)).bind((T) entity) + .with(entityInformation.getBinderFunction()).fetchAs(Long.class).one(); + + if (!entityMetaData.isUsingInternalIds()) { + return idMono.thenReturn(entity); + } else { + return idMono.map(internalId -> { + PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entity); + propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId); + return (S) propertyAccessor.getBean(); + }); + } } - @Override public Flux saveAll(Iterable entities) { - return null; + @Override + @Transactional + public Flux saveAll(Iterable entities) { + return Flux.fromIterable(entities).as(this::saveAll); } - @Override public Flux saveAll(Publisher entityStream) { - return null; + @Override + @Transactional + public Flux saveAll(Publisher entityStream) { + + if (entityMetaData.isUsingInternalIds()) { + log.debug("Saving entities using single statements."); + + return Flux.from(entityStream).flatMap(this::save); + } + + final Function> binderFunction = entityInformation.getBinderFunction(); + return Flux.from(entityStream).map(binderFunction) + .flatMap(entities -> neo4jClient + .query(() -> renderer.render(statementBuilder.prepareSaveOfMultipleInstancesOf(entityMetaData))) + .bind(entities).to(NAME_OF_ENTITY_LIST_PARAM).run()) + .doOnNext(resultSummary -> { + SummaryCounters counters = resultSummary.counters(); + log.debug("Created {} and deleted {} nodes, created {} and deleted {} relationships and set {} properties.", + counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(), + counters.relationshipsDeleted(), counters.propertiesSet()); + }).thenMany(entityStream); } /* diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java index 51d7fe1f2..65562fc24 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java @@ -43,6 +43,15 @@ import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Point; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.KotlinPerson; +import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.integration.shared.PersonWithNoConstructor; +import org.neo4j.springframework.data.integration.shared.PersonWithWither; +import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -57,15 +66,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; -import org.neo4j.springframework.data.config.AbstractNeo4jConfig; -import org.neo4j.springframework.data.integration.shared.KotlinPerson; -import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; -import org.neo4j.springframework.data.integration.shared.PersonWithNoConstructor; -import org.neo4j.springframework.data.integration.shared.PersonWithWither; -import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; -import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; -import org.neo4j.springframework.data.test.Neo4jExtension; -import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -226,14 +226,15 @@ class RepositoryIT { try (Session session = driver.session()) { session.readTransaction(tx -> { Record record = tx.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", - Values.parameters("id", originalPerson.getId())).single(); + Values.parameters("id", id1)).single(); + assertThat(record.containsKey("n")).isTrue(); Node node = record.get("n").asNode(); - assertThat(savedPerson.getId()).isEqualTo(node.id()); - assertThat(savedPerson.getId()).isEqualTo(originalPerson.getId()); - assertThat(savedPerson.getFirstName()).isEqualTo(originalPerson.getFirstName()); - assertThat(savedPerson.getNullable()).isEqualTo(originalPerson.getNullable()); + assertThat(node.id()).isEqualTo(savedPerson.getId()); + assertThat(node.get("first_name").asString()).isEqualTo(savedPerson.getFirstName()); + assertThat(node.get("nullable").asString()).isEqualTo(savedPerson.getNullable()); + return null; }); } @@ -330,7 +331,7 @@ class RepositoryIT { assertThat(record.containsKey("names")).isTrue(); List names = record.get("names").asList(Value::asString); - assertThat(names).contains(newThing.getName(), existingThing.getName()); + assertThat(names).containsExactly(newThing.getName(), existingThing.getName()); assertThat(thingRepository.count()).isEqualTo(22); } @@ -350,12 +351,15 @@ class RepositoryIT { thingRepository.save(thing); try (Session session = driver.session()) { - List records = session - .run("MATCH (n:Thing) WHERE n.theId in $ids RETURN n ORDER BY n.name ASC", + Record record = session + .run("MATCH (n:Thing) WHERE n.theId IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names", Values.parameters("ids", Arrays.asList("id07", "id15"))) - .list(r -> r.get("n").get("name").asString()); + .single(); + + assertThat(record.containsKey("names")).isTrue(); + List names = record.get("names").asList(Value::asString); + assertThat(names).containsExactly("An updated thing", "Another updated thing"); - assertThat(records).hasSize(2).containsExactly("An updated thing", "Another updated thing"); assertThat(thingRepository.count()).isEqualTo(21); } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index 7d8b67678..4ed3ce7f8 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -18,25 +18,33 @@ */ package org.neo4j.springframework.data.integration.reactive; +import static org.neo4j.driver.Values.*; + +import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; import org.neo4j.driver.Transaction; +import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.types.Point; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; import org.neo4j.springframework.data.test.Neo4jExtension; import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; @@ -52,10 +60,12 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author Gerrit Meier + * @author Michael J. Simons */ @ExtendWith(Neo4jExtension.class) @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = ReactiveRepositoryIT.Config.class) +@Slf4j class ReactiveRepositoryIT { private static final String TEST_PERSON1_NAME = "Test"; @@ -72,6 +82,7 @@ class ReactiveRepositoryIT { private static final long NOT_EXISTING_NODE_ID = 3123131231L; private static Neo4jConnectionSupport neo4jConnectionSupport; @Autowired private ReactivePersonRepository repository; + @Autowired private ReactiveThingRepository thingRepository; @Autowired private Driver driver; private long id1; private long id2; @@ -91,17 +102,22 @@ class ReactiveRepositoryIT { id1 = transaction.run("" + "CREATE (n:PersonWithAllConstructor) " + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + "RETURN id(n)", - Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) .next().get(0).asLong(); id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", - Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", + parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) .next().get(0).asLong(); + transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})"); + IntStream.rangeClosed(1, 20).forEach(i -> + transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", + parameters("i", String.format("%02d", i)))); + transaction.success(); transaction.close(); @@ -382,6 +398,148 @@ class ReactiveRepositoryIT { .verifyComplete(); } + @Test + void saveSingleEntity() { + + PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, + LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null); + + List ids = new ArrayList<>(); + repository + .save(person) + .map(PersonWithAllConstructor::getId) + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(1L) + .verifyComplete(); + + driverWorkaround(); + + Flux.usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n", parameters("ids", ids)) + .records(), + RxSession::close + ).map(r -> r.get("n").asNode().get("first_name").asString()) + .as(StepVerifier::create) + .expectNext("Freddie") + .verifyComplete(); + } + + @Test + void updateSingleEntity() { + + repository.findById(id1) + .map(originalPerson -> { + originalPerson.setFirstName("Updated first name"); + originalPerson.setNullable("Updated nullable field"); + return originalPerson; + }) + .flatMap(repository::save) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + driverWorkaround(); + + Flux.usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> { + Value parameters = parameters("id", id1); + return s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", parameters).records(); + }, + RxSession::close + ) + .map(r -> r.get("n").asNode()) + .as(StepVerifier::create) + .expectNextMatches(node -> node.get("first_name").asString().equals("Updated first name") && + node.get("nullable").asString().equals("Updated nullable field")) + .verifyComplete(); + } + + @Test + void saveWithAssignedId() { + + thingRepository.count().as(StepVerifier::create).expectNext(21L).verifyComplete(); + + Mono.fromSupplier(() -> { + ThingWithAssignedId thing = new ThingWithAssignedId("aaBB"); + thing.setName("That's the thing."); + return thing; + }) + .flatMap(thingRepository::save) + .as(StepVerifier::create) + .expectNextCount(1L) + .verifyComplete(); + + driverWorkaround(); + + Flux.usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> s.run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", parameters("id", "aaBB")).records(), + RxSession::close + ) + .map(r -> r.get("n").asNode().get("name").asString()) + .as(StepVerifier::create) + .expectNext("That's the thing.") + .verifyComplete(); + + thingRepository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); + } + + @Test + void updateWithAssignedId() { + thingRepository.count().as(StepVerifier::create).expectNext(21L).verifyComplete(); + + Flux.concat( + // Without prior selection + Mono.fromSupplier(() -> { + ThingWithAssignedId thing = new ThingWithAssignedId("id07"); + thing.setName("An updated thing"); + return thing; + }).flatMap(thingRepository::save), + + // With prior selection + thingRepository.findById("id15") + .flatMap(thing -> { + thing.setName("Another updated thing"); + return thingRepository.save(thing); + }) + ) + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); + + driverWorkaround(); + + Flux.usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> { + Value parameters = parameters("ids", Arrays.asList("id07", "id15")); + return s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", + parameters).records(); + }, + RxSession::close + ) + .map(r -> r.get("name").asString()) + .as(StepVerifier::create) + .expectNext("An updated thing", "Another updated thing") + .verifyComplete(); + + thingRepository.count().as(StepVerifier::create).expectNext(21L).verifyComplete(); + } + + @Deprecated + static void driverWorkaround() { + log.warn( + "Sleeping a bit to let the driver finish it's commit. This is a workaround and needs to be removed in the future!!"); + try { + Thread.sleep(1000L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + @Configuration @EnableReactiveNeo4jRepositories @EnableTransactionManagement diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveThingRepository.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveThingRepository.java index 7a5bc446c..e09983bcb 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveThingRepository.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveThingRepository.java @@ -18,16 +18,11 @@ */ package org.neo4j.springframework.data.integration.reactive; -import java.util.List; - import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; /** * @author Michael J. Simons */ -public interface ReactiveThingRepository extends CrudRepository { - List findFirstByOrderByNameDesc(); - - List findTop5ByOrderByNameDesc(); +public interface ReactiveThingRepository extends ReactiveCrudRepository { } From 5220f03e9fd0e5141348dd10039556c170c9c333 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 17 Jun 2019 11:14:03 +0200 Subject: [PATCH 124/342] Add experimental support for VS Code remote containers. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e46643fc8..0b7821537 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ target/ /.nb-gradle/ /build/ +### Visual Studio Code ### +.vscode + ### Misc ### .DS_Store From eb424989f93315e28b4925d69039524628c6cd45 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 18 Jun 2019 10:58:25 +0200 Subject: [PATCH 125/342] Be consistent in the use of transaction synchronization manager. --- .../ReactiveNeo4jTransactionManager.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 2564cfae6..7c170f7f4 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -44,6 +44,7 @@ import org.springframework.util.Assert; /** * @author Gerrit Meier + * @author Michael J. Simons * @since 1.0 */ @API(status = API.Status.STABLE, since = "1.0") @@ -125,11 +126,11 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction .getResource(driver); return new ReactiveNeo4jTransactionObject(resourceHolder); } + /* * (non-Javadoc) * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#isExistingTransaction(Object) */ - @Override protected boolean isExistingTransaction(Object transaction) throws TransactionException { @@ -167,13 +168,12 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction } @Override - protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager synchronizationManager, + protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager transactionSynchronizationManager, Object transaction) { - ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); - - return Mono.defer(() -> transactionObject.getRequiredResourceHolder().close() - .doOnNext(ignoreMe -> synchronizationManager.unbindResource(driver))); + ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager + .getResource(driver); + return holder.close().doOnNext(ignoreMe -> transactionSynchronizationManager.unbindResource(driver)).then(); } @Override @@ -193,11 +193,11 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction .getResource(driver); return holder.rollback().then(); } + /* * (non-Javadoc) * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) */ - @Override protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) throws TransactionException { From 6cd033bf2b8e3781f90ed63845c5844b8eb8eb7f Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 18 Jun 2019 14:10:28 +0200 Subject: [PATCH 126/342] Fix unordered reactive tests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The queries don’t pass an order and it seems the resulting order is not stable. --- .../reactive/ReactiveRepositoryIT.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index 4ed3ce7f8..8656474d4 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -208,13 +208,16 @@ class ReactiveRepositoryIT { void findAllByExampleWithDifferentMatchers() { PersonWithAllConstructor person; Example example; - Iterable persons; person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny()); - StepVerifier.create(repository.findAll(example)).expectNext(person1, person2).verifyComplete(); + StepVerifier.create(repository.findAll(example)) + .recordWith(ArrayList::new) + .expectNextCount(2) + .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) + .verifyComplete(); person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, null, null, null, null, null, null); @@ -321,8 +324,12 @@ class ReactiveRepositoryIT { @Test void findBySimplePropertiesOred() { - StepVerifier.create(repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME)) - .expectNext(person1, person2).verifyComplete(); + repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME) + .as(StepVerifier::create) + .recordWith(ArrayList::new) + .expectNextCount(2) + .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) + .verifyComplete(); } @Test From 182eddc94f2f152b16116edcdec5b2f41ccce1af Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 18 Jun 2019 16:33:44 +0200 Subject: [PATCH 127/342] Wait for the transaction publisher to finish before asserting. This adds an explicit transactional operator around the operation under test before using the step verifier. Thus it has been made sure, that the verifier not only waits for the operation to complete, but also for the transaction publisher. The implicit transaction would have been finished in the background. --- .../reactive/ReactiveRepositoryIT.java | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index 8656474d4..b3960d063 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -56,7 +56,9 @@ import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Sort; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.reactive.TransactionalOperator; /** * @author Gerrit Meier @@ -84,6 +86,7 @@ class ReactiveRepositoryIT { @Autowired private ReactivePersonRepository repository; @Autowired private ReactiveThingRepository thingRepository; @Autowired private Driver driver; + @Autowired private ReactiveTransactionManager transactionManager; private long id1; private long id2; private PersonWithAllConstructor person1; @@ -411,17 +414,20 @@ class ReactiveRepositoryIT { PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null); - List ids = new ArrayList<>(); - repository + Mono operationUnderTest = repository .save(person) - .map(PersonWithAllConstructor::getId) + .map(PersonWithAllConstructor::getId); + + List ids = new ArrayList<>(); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) .as(StepVerifier::create) .recordWith(() -> ids) .expectNextCount(1L) .verifyComplete(); - driverWorkaround(); - Flux.usingWhen( Mono.fromSupplier(() -> driver.rxSession()), s -> s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n", parameters("ids", ids)) @@ -436,19 +442,21 @@ class ReactiveRepositoryIT { @Test void updateSingleEntity() { - repository.findById(id1) + Mono operationUnderTest = repository.findById(id1) .map(originalPerson -> { originalPerson.setFirstName("Updated first name"); originalPerson.setNullable("Updated nullable field"); return originalPerson; }) - .flatMap(repository::save) + .flatMap(repository::save); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) .as(StepVerifier::create) .expectNextCount(1L) .verifyComplete(); - driverWorkaround(); - Flux.usingWhen( Mono.fromSupplier(() -> driver.rxSession()), s -> { @@ -467,20 +475,20 @@ class ReactiveRepositoryIT { @Test void saveWithAssignedId() { - thingRepository.count().as(StepVerifier::create).expectNext(21L).verifyComplete(); + Mono operationUnderTest = + Mono.fromSupplier(() -> { + ThingWithAssignedId thing = new ThingWithAssignedId("aaBB"); + thing.setName("That's the thing."); + return thing; + }).flatMap(thingRepository::save); - Mono.fromSupplier(() -> { - ThingWithAssignedId thing = new ThingWithAssignedId("aaBB"); - thing.setName("That's the thing."); - return thing; - }) - .flatMap(thingRepository::save) + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) .as(StepVerifier::create) .expectNextCount(1L) .verifyComplete(); - driverWorkaround(); - Flux.usingWhen( Mono.fromSupplier(() -> driver.rxSession()), s -> s.run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", parameters("id", "aaBB")).records(), @@ -496,9 +504,8 @@ class ReactiveRepositoryIT { @Test void updateWithAssignedId() { - thingRepository.count().as(StepVerifier::create).expectNext(21L).verifyComplete(); - Flux.concat( + Flux operationUnderTest = Flux.concat( // Without prior selection Mono.fromSupplier(() -> { ThingWithAssignedId thing = new ThingWithAssignedId("id07"); @@ -512,13 +519,15 @@ class ReactiveRepositoryIT { thing.setName("Another updated thing"); return thingRepository.save(thing); }) - ) + ); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) .as(StepVerifier::create) .expectNextCount(2L) .verifyComplete(); - driverWorkaround(); - Flux.usingWhen( Mono.fromSupplier(() -> driver.rxSession()), s -> { @@ -536,17 +545,6 @@ class ReactiveRepositoryIT { thingRepository.count().as(StepVerifier::create).expectNext(21L).verifyComplete(); } - @Deprecated - static void driverWorkaround() { - log.warn( - "Sleeping a bit to let the driver finish it's commit. This is a workaround and needs to be removed in the future!!"); - try { - Thread.sleep(1000L); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - @Configuration @EnableReactiveNeo4jRepositories @EnableTransactionManagement From cb8833e505498459492d47f0e8f3d14af06cf86e Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 18 Jun 2019 17:06:42 +0200 Subject: [PATCH 128/342] Add missing integration tests for reactive saveAll. --- .../reactive/ReactiveRepositoryIT.java | 146 +++++++++++++++--- 1 file changed, 123 insertions(+), 23 deletions(-) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index b3960d063..53986b620 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -226,7 +226,11 @@ class ReactiveRepositoryIT { null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name")); - StepVerifier.create(repository.findAll(example)).expectNext(person1, person2).verifyComplete(); + StepVerifier.create(repository.findAll(example)) + .recordWith(ArrayList::new) + .expectNextCount(2) + .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) + .verifyComplete(); person = new PersonWithAllConstructor(null, TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(), @@ -439,6 +443,51 @@ class ReactiveRepositoryIT { .verifyComplete(); } + @Test + void saveAll() { + + Flux persons = repository + .findById(id1) + .map(existingPerson -> { + existingPerson.setFirstName("Updated first name"); + existingPerson.setNullable("Updated nullable field"); + return existingPerson; + }) + .concatWith( + Mono.fromSupplier(() -> { + PersonWithAllConstructor newPerson = new PersonWithAllConstructor( + null, "Mercury", "Freddie", "Queen", true, 1509L, + LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null); + return newPerson; + })); + + Flux operationUnderTest = repository + .saveAll(persons) + .map(PersonWithAllConstructor::getId); + + List ids = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(2L) + .verifyComplete(); + + Flux + .usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n ORDER BY n.name ASC", + parameters("ids", ids)) + .records(), + RxSession::close + ).map(r -> r.get("n").asNode().get("name").asString()) + .as(StepVerifier::create) + .expectNext("Mercury") + .expectNext(TEST_PERSON1_NAME) + .verifyComplete(); + } + @Test void updateSingleEntity() { @@ -457,14 +506,15 @@ class ReactiveRepositoryIT { .expectNextCount(1L) .verifyComplete(); - Flux.usingWhen( - Mono.fromSupplier(() -> driver.rxSession()), - s -> { - Value parameters = parameters("id", id1); - return s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", parameters).records(); - }, - RxSession::close - ) + Flux + .usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> { + Value parameters = parameters("id", id1); + return s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", parameters).records(); + }, + RxSession::close + ) .map(r -> r.get("n").asNode()) .as(StepVerifier::create) .expectNextMatches(node -> node.get("first_name").asString().equals("Updated first name") && @@ -489,11 +539,12 @@ class ReactiveRepositoryIT { .expectNextCount(1L) .verifyComplete(); - Flux.usingWhen( - Mono.fromSupplier(() -> driver.rxSession()), - s -> s.run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", parameters("id", "aaBB")).records(), - RxSession::close - ) + Flux + .usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> s.run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", parameters("id", "aaBB")).records(), + RxSession::close + ) .map(r -> r.get("n").asNode().get("name").asString()) .as(StepVerifier::create) .expectNext("That's the thing.") @@ -502,6 +553,54 @@ class ReactiveRepositoryIT { thingRepository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); } + @Test + void saveAllWithAssignedId() { + + Flux things = thingRepository + .findById("anId") + .map(existingThing -> { + existingThing.setName("Updated name."); + return existingThing; + }) + .concatWith( + Mono.fromSupplier(() -> { + ThingWithAssignedId newThing = new ThingWithAssignedId("aaBB"); + newThing.setName("That's the thing."); + return newThing; + }) + ); + + Flux operationUnderTest = thingRepository + .saveAll(things); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); + + Flux + .usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> { + Value parameters = parameters("ids", Arrays.asList("anId", "aaBB")); + return s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", + parameters) + .records(); + }, + RxSession::close + ) + .map(r -> r.get("name").asString()) + .as(StepVerifier::create) + .expectNext("That's the thing.") + .expectNext("Updated name.") + .verifyComplete(); + + // Make sure we triggered on insert, one update + thingRepository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); + } + @Test void updateWithAssignedId() { @@ -528,15 +627,16 @@ class ReactiveRepositoryIT { .expectNextCount(2L) .verifyComplete(); - Flux.usingWhen( - Mono.fromSupplier(() -> driver.rxSession()), - s -> { - Value parameters = parameters("ids", Arrays.asList("id07", "id15")); - return s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", - parameters).records(); - }, - RxSession::close - ) + Flux + .usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> { + Value parameters = parameters("ids", Arrays.asList("id07", "id15")); + return s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", + parameters).records(); + }, + RxSession::close + ) .map(r -> r.get("name").asString()) .as(StepVerifier::create) .expectNext("An updated thing", "Another updated thing") From dbe8a487db84dbc17f84347180ccb27309145508 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 19 Jun 2019 10:02:55 +0200 Subject: [PATCH 129/342] Fix naming. --- .../data/repository/support/Predicate.java | 9 +++++---- .../support/SimpleQueryByExampleExecutor.java | 10 +++++----- .../support/SimpleReactiveQueryByExampleExecutor.java | 8 ++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java index d9b0d60fe..4f099db76 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java @@ -58,7 +58,8 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; * @since 1.0 */ @Slf4j -@RequiredArgsConstructor(access = PRIVATE) final class Predicate { +@RequiredArgsConstructor(access = PRIVATE) +final class Predicate { static Predicate create(Neo4jMappingContext mappingContext, Example example) { @@ -150,9 +151,9 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; private final Map parameters = new HashMap<>(); - StatementBuilder.OngoingReadingAndWith f( - BiFunction, Optional, StatementBuilder.OngoingReadingAndWith> ff) { - return ff.apply(this.nodeDescription, Optional.of(this.condition)); + StatementBuilder.OngoingReadingAndWith useWithReadingFragment( + BiFunction, Optional, StatementBuilder.OngoingReadingAndWith> readingFragmentSupplier) { + return readingFragmentSupplier.apply(this.nodeDescription, Optional.of(this.condition)); } private void add(ExampleMatcher.MatchMode matchMode, Condition additionalCondition) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java index 36809527c..31a590c64 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java @@ -72,7 +72,7 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { public Optional findOne(Example example) { Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.f(statementBuilder::prepareMatchOf) + Statement statement = predicate.useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(asterisk()) .build(); @@ -83,7 +83,7 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { public Iterable findAll(Example example) { Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.f(statementBuilder::prepareMatchOf) + Statement statement = predicate.useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(asterisk()) .build(); @@ -94,7 +94,7 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { public Iterable findAll(Example example, Sort sort) { Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.f(statementBuilder::prepareMatchOf) + Statement statement = predicate.useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(asterisk()) .orderBy(toSortItems(predicate.getNodeDescription(), sort)).build(); @@ -105,7 +105,7 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { public long count(Example example) { Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.f(statementBuilder::prepareMatchOf) + Statement statement = predicate.useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(Functions.count(asterisk())) .build(); @@ -126,7 +126,7 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { Predicate predicate = Predicate.create(mappingContext, example); StatementBuilder.OngoingReadingAndReturn returning = predicate - .f(statementBuilder::prepareMatchOf) + .useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(asterisk()); BuildableStatement returningWithPaging = addPagingParameter(predicate.getNodeDescription(), pageable, returning); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java index 775c3c98a..4dbc5e3dd 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java @@ -67,7 +67,7 @@ class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleE public Mono findOne(Example example) { Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.f(statementBuilder::prepareMatchOf) + Statement statement = predicate.useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(asterisk()) .build(); @@ -78,7 +78,7 @@ class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleE public Flux findAll(Example example) { Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.f(statementBuilder::prepareMatchOf) + Statement statement = predicate.useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(asterisk()) .build(); @@ -89,7 +89,7 @@ class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleE public Flux findAll(Example example, Sort sort) { Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.f(statementBuilder::prepareMatchOf) + Statement statement = predicate.useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(asterisk()) .orderBy(toSortItems(predicate.getNodeDescription(), sort)).build(); @@ -100,7 +100,7 @@ class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleE public Mono count(Example example) { Predicate predicate = Predicate.create(mappingContext, example); - Statement statement = predicate.f(statementBuilder::prepareMatchOf) + Statement statement = predicate.useWithReadingFragment(statementBuilder::prepareMatchOf) .returning(Functions.count(asterisk())) .build(); From dac927b94f3465c2d965a94f17b58b9c89bc02d5 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 24 Jun 2019 15:19:39 +0200 Subject: [PATCH 130/342] Improve deleteAll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deleteAll doesn’t build up a list anymore when working with a Publisher. This comes at the price of running more queries, but gains the fact the the publisher needs not to be collected. --- .../SimpleReactiveNeo4jRepository.java | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java index c1963194d..3bf02f62c 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java @@ -19,6 +19,7 @@ package org.neo4j.springframework.data.repository.support; import static java.util.Collections.*; +import static java.util.stream.Collectors.*; import static org.neo4j.springframework.data.core.cypher.Cypher.*; import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; @@ -28,10 +29,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.neo4j.driver.summary.SummaryCounters; import org.neo4j.springframework.data.core.PreparedQuery; @@ -236,14 +238,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< Assert.notNull(idPublisher, "The given Publisher of an id must not be null!"); return Mono.from(idPublisher) - .flatMap(id -> { - String nameOfParameter = "id"; - Condition condition = this.entityInformation.getIdExpression().isEqualTo(parameter(nameOfParameter)); - - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); - return this.neo4jClient.query(() -> renderer.render(statement)) - .bind(id).to(nameOfParameter).run(); - }) + .flatMap(this::deleteByIdImpl) .then(); } @@ -257,8 +252,15 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< Assert.notNull(entities, "The given Iterable of entities must not be null!"); - return Flux.fromIterable(entities) - .as(this::deleteAll); + String nameOfParameter = "ids"; + Condition condition = entityInformation.getIdExpression().in(parameter(nameOfParameter)); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + + List ids = StreamSupport.stream(entities.spliterator(), false) + .map(this.entityInformation::getId) + .collect(toList()); + + return this.neo4jClient.query(() -> renderer.render(statement)).bind(ids).to(nameOfParameter).run().then(); } /* @@ -273,15 +275,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< return Flux.from(entitiesPublisher) .map(this.entityInformation::getId) - .collect(Collectors.toList()) - .flatMap(ids -> { - - String nameOfParameter = "ids"; - Condition condition = entityInformation.getIdExpression().in(parameter(nameOfParameter)); - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); - - return this.neo4jClient.query(() -> renderer.render(statement)).bind(ids).to(nameOfParameter).run(); - }) + .flatMap(this::deleteByIdImpl) .then(); } @@ -310,4 +304,14 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< .build(); return neo4jClient.toExecutableQuery(preparedQuery); } + + private Mono deleteByIdImpl(ID id) { + + String nameOfParameter = "id"; + Condition condition = this.entityInformation.getIdExpression().isEqualTo(parameter(nameOfParameter)); + + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + return this.neo4jClient.query(() -> renderer.render(statement)) + .bind(id).to(nameOfParameter).run().then(); + } } From f223721155a37e47c91eac597bdcfa9c2bb2748a Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 24 Jun 2019 15:44:50 +0200 Subject: [PATCH 131/342] Prevent wrong DSL usage. --- .../core/cypher/DefaultStatementBuilder.java | 12 ++++++++- .../data/core/cypher/CypherIT.java | 25 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java index a8cd438ea..35b8699f9 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java @@ -295,6 +295,14 @@ class DefaultStatementBuilder this.distinct = distinct; } + void reset() { + this.returnList.clear(); + this.sortItemList.clear(); + this.lastSortItem = null; + this.skip = null; + this.limit = null; + } + @Override public final OngoingReadingAndReturn orderBy(SortItem... sortItem) { Arrays.stream(sortItem).forEach(this.sortItemList::add); @@ -395,7 +403,9 @@ class DefaultStatementBuilder Order order = sortItemList.size() > 0 ? new Order(sortItemList) : null; Where where = conditionBuilder.buildCondition().map(Where::new).orElse(null); - return Optional.of(new With(distinct, returnItems, order, skip, limit, where)); + Optional returnedWith = Optional.of(new With(distinct, returnItems, order, skip, limit, where)); + super.reset(); + return returnedWith; } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java index 2ac81f5da..89c01e30f 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java @@ -460,6 +460,31 @@ class CypherIT { .isEqualTo("OPTIONAL MATCH (b:`Bike`) MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); } + @Test + void usingSameWithStepWithoutReassign() { + StatementBuilder.OngoingReadingAndWith firstStep = match(bikeNode).with(bikeNode); + + firstStep.optionalMatch(userNode); + firstStep.optionalMatch(Cypher.node("Trip")); + + Statement statement = firstStep.returning(Cypher.asterisk()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) WITH b OPTIONAL MATCH (u:`User`) OPTIONAL MATCH (:`Trip`) RETURN *"); + } + + @Test + void usingSameWithStepWithReassign() { + StatementBuilder.ExposesMatch firstStep = match(bikeNode).with(bikeNode); + + firstStep = firstStep.optionalMatch(userNode); + firstStep = firstStep.optionalMatch(Cypher.node("Trip")); + + Statement statement = ((StatementBuilder.ExposesReturning) firstStep).returning(Cypher.asterisk()).build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) WITH b OPTIONAL MATCH (u:`User`) OPTIONAL MATCH (:`Trip`) RETURN *"); + } + @Test void optionalNext() { Statement statement = Cypher From acc33d96b42ceda412b12c4ae9abf363d2b336ec Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 24 Jun 2019 16:53:00 +0200 Subject: [PATCH 132/342] Improve saveAll. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As with deleteAll, don’t collect the publisher into a fixed list. --- .../data/core/schema/NodeDescription.java | 1 + .../repository/query/CypherAdapterUtils.java | 2 +- .../SimpleReactiveNeo4jRepository.java | 79 ++++++++----------- .../reactive/ReactiveRepositoryIT.java | 73 +++++++++++++++++ 4 files changed, 110 insertions(+), 45 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java index 70a6309f8..f09146913 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java @@ -36,6 +36,7 @@ public interface NodeDescription { String NAME_OF_ROOT_NODE = "n"; String NAME_OF_INTERNAL_ID = "__internalNeo4jId__"; + String NAME_OF_IDS_RESULT = "__ids__"; String NAME_OF_ID_PARAM = "__id__"; String NAME_OF_PROPERTIES_PARAM = "__properties__"; String NAME_OF_ENTITY_LIST_PARAM = "__entities__"; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java index 7e6edeede..aeaefc5bf 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java @@ -206,7 +206,7 @@ public final class CypherAdapterUtils { .unwind(parameter(NAME_OF_ENTITY_LIST_PARAM)).as(row) .merge(rootNode.properties(nameOfIdProperty, property(row, NAME_OF_ID_PARAM))) .set(rootNode, property(row, NAME_OF_PROPERTIES_PARAM)) - .returning(rootNode.internalId()) + .returning(Functions.collect(rootNode.property(nameOfIdProperty)).as(NAME_OF_IDS_RESULT)) .build(); } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java index 3bf02f62c..334db0ccd 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java @@ -156,8 +156,9 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< Statement saveStatement = statementBuilder.prepareSaveOf(entityMetaData); - Mono idMono = this.neo4jClient.query(() -> renderer.render(saveStatement)).bind((T) entity) - .with(entityInformation.getBinderFunction()).fetchAs(Long.class).one(); + Mono idMono = this.neo4jClient.query(() -> renderer.render(saveStatement)) + .bind((T) entity).with(entityInformation.getBinderFunction()) + .fetchAs(Long.class).one(); if (!entityMetaData.isUsingInternalIds()) { return idMono.thenReturn(entity); @@ -173,30 +174,35 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override @Transactional public Flux saveAll(Iterable entities) { - return Flux.fromIterable(entities).as(this::saveAll); + + if (entityMetaData.isUsingInternalIds()) { + log.debug("Saving entities using single statements."); + + return Flux.fromIterable(entities).flatMap(this::save); + } + + final Function> binderFunction = entityInformation.getBinderFunction(); + List> boundedEntityList = StreamSupport.stream(entities.spliterator(), false) + .map(binderFunction) + .collect(toList()); + + return Mono.from(neo4jClient + .query(() -> renderer.render(statementBuilder.prepareSaveOfMultipleInstancesOf(entityMetaData))) + .bind(boundedEntityList).to(NAME_OF_ENTITY_LIST_PARAM).run()) + .doOnNext(resultSummary -> { + SummaryCounters counters = resultSummary.counters(); + log.debug( + "Created {} and deleted {} nodes, created {} and deleted {} relationships and set {} properties.", + counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(), + counters.relationshipsDeleted(), counters.propertiesSet()); + }).thenMany(Flux.fromIterable(entities)); } @Override @Transactional public Flux saveAll(Publisher entityStream) { - if (entityMetaData.isUsingInternalIds()) { - log.debug("Saving entities using single statements."); - - return Flux.from(entityStream).flatMap(this::save); - } - - final Function> binderFunction = entityInformation.getBinderFunction(); - return Flux.from(entityStream).map(binderFunction) - .flatMap(entities -> neo4jClient - .query(() -> renderer.render(statementBuilder.prepareSaveOfMultipleInstancesOf(entityMetaData))) - .bind(entities).to(NAME_OF_ENTITY_LIST_PARAM).run()) - .doOnNext(resultSummary -> { - SummaryCounters counters = resultSummary.counters(); - log.debug("Created {} and deleted {} nodes, created {} and deleted {} relationships and set {} properties.", - counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(), - counters.relationshipsDeleted(), counters.propertiesSet()); - }).thenMany(entityStream); + return Flux.from(entityStream).flatMap(this::save); } /* @@ -209,7 +215,12 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< Assert.notNull(id, "The given id must not be null!"); - return Mono.just(id).as(this::deleteById); + String nameOfParameter = "id"; + Condition condition = this.entityInformation.getIdExpression().isEqualTo(parameter(nameOfParameter)); + + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + return this.neo4jClient.query(() -> renderer.render(statement)) + .bind(id).to(nameOfParameter).run().then(); } /* @@ -221,10 +232,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< public Mono delete(T entity) { Assert.notNull(entity, "The given entity must not be null!"); - - return Mono.just(entity) - .map(this.entityInformation::getId) - .as(this::deleteById); + return deleteById(this.entityInformation.getId(entity)); } /* @@ -236,10 +244,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< public Mono deleteById(Publisher idPublisher) { Assert.notNull(idPublisher, "The given Publisher of an id must not be null!"); - - return Mono.from(idPublisher) - .flatMap(this::deleteByIdImpl) - .then(); + return Mono.from(idPublisher).flatMap(this::deleteById); } /* @@ -272,11 +277,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< public Mono deleteAll(Publisher entitiesPublisher) { Assert.notNull(entitiesPublisher, "The given Publisher of entities must not be null!"); - - return Flux.from(entitiesPublisher) - .map(this.entityInformation::getId) - .flatMap(this::deleteByIdImpl) - .then(); + return Flux.from(entitiesPublisher).flatMap(this::delete).then(); } /* @@ -304,14 +305,4 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< .build(); return neo4jClient.toExecutableQuery(preparedQuery); } - - private Mono deleteByIdImpl(ID id) { - - String nameOfParameter = "id"; - Condition condition = this.entityInformation.getIdExpression().isEqualTo(parameter(nameOfParameter)); - - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); - return this.neo4jClient.query(() -> renderer.render(statement)) - .bind(id).to(nameOfParameter).run().then(); - } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index 53986b620..011d64680 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -488,6 +488,39 @@ class ReactiveRepositoryIT { .verifyComplete(); } + @Test + void saveAllIterable() { + + PersonWithAllConstructor newPerson = new PersonWithAllConstructor( + null, "Mercury", "Freddie", "Queen", true, 1509L, + LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null); + + Flux operationUnderTest = repository + .saveAll(Arrays.asList(newPerson)) + .map(PersonWithAllConstructor::getId); + + List ids = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> ids) + .expectNextCount(1L) + .verifyComplete(); + + Flux + .usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n ORDER BY n.name ASC", + parameters("ids", ids)) + .records(), + RxSession::close + ).map(r -> r.get("n").asNode().get("name").asString()) + .as(StepVerifier::create) + .expectNext("Mercury") + .verifyComplete(); + } + @Test void updateSingleEntity() { @@ -601,6 +634,46 @@ class ReactiveRepositoryIT { thingRepository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); } + @Test + void saveAllIterableWithAssignedId() { + + ThingWithAssignedId existingThing = new ThingWithAssignedId("anId"); + existingThing.setName("Updated name."); + ThingWithAssignedId newThing = new ThingWithAssignedId("aaBB"); + newThing.setName("That's the thing."); + + List things = Arrays.asList(existingThing, newThing); + + Flux operationUnderTest = thingRepository.saveAll(things); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) + .as(StepVerifier::create) + .expectNextCount(2L) + .verifyComplete(); + + Flux + .usingWhen( + Mono.fromSupplier(() -> driver.rxSession()), + s -> { + Value parameters = parameters("ids", Arrays.asList("anId", "aaBB")); + return s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", + parameters) + .records(); + }, + RxSession::close + ) + .map(r -> r.get("name").asString()) + .as(StepVerifier::create) + .expectNext("That's the thing.") + .expectNext("Updated name.") + .verifyComplete(); + + // Make sure we triggered on insert, one update + thingRepository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); + } + @Test void updateWithAssignedId() { From 81dfba6f17c5d3318ced7b1e701f08b74a57dc91 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 25 Jun 2019 14:34:03 +0200 Subject: [PATCH 133/342] Add support for nested repositories. --- .../data/repository/config/EnableNeo4jRepositories.java | 6 ++++++ .../repository/config/EnableReactiveNeo4jRepositories.java | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java index 303740a2a..5a2b5cf63 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableNeo4jRepositories.java @@ -109,4 +109,10 @@ public @interface EnableNeo4jRepositories { * for {@code PersonRepositoryImpl}. */ String repositoryImplementationPostfix() default "Impl"; + + /** + * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the + * repositories infrastructure. + */ + boolean considerNestedRepositories() default false; } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java index 587207ac9..0ae67e2a1 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/EnableReactiveNeo4jRepositories.java @@ -110,4 +110,10 @@ public @interface EnableReactiveNeo4jRepositories { * for {@code PersonRepositoryImpl}. */ String repositoryImplementationPostfix() default "Impl"; + + /** + * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the + * repositories infrastructure. + */ + boolean considerNestedRepositories() default false; } From 58103ddf0fb4e849641449ab9f878d14d00b2f6b Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 25 Jun 2019 14:49:43 +0200 Subject: [PATCH 134/342] Enforce only supported @Id / type combinations. --- .../mapping/DefaultNeo4jPersistentEntity.java | 21 ++++++++++++++----- .../core/mapping/Neo4jMappingContextTest.java | 20 ++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java index 2d1497904..08b3163bb 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java @@ -19,10 +19,13 @@ package org.neo4j.springframework.data.core.mapping; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mapping.model.BasicPersistentEntity; @@ -43,6 +46,9 @@ import org.springframework.util.Assert; class DefaultNeo4jPersistentEntity extends BasicPersistentEntity implements Neo4jPersistentEntity { + private static final Set> VALID_GENERATED_ID_TYPES = Collections.unmodifiableSet(new HashSet<>( + Arrays.asList(Long.class, long.class))); + private final String primaryLabel; private final Lazy idDescription; @@ -141,11 +147,16 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity { - if (idAnnotation.strategy() == Id.Strategy.INTERNALLY_GENERATED - && idProperty.findAnnotation(Property.class) != null) { - throw new IllegalArgumentException( - "Cannot use internal id strategy with custom property " + idProperty.getPropertyName() - + " on entity class " + this.getUnderlyingClass().getName()); + if (idAnnotation.strategy() == Id.Strategy.INTERNALLY_GENERATED) { + if (idProperty.findAnnotation(Property.class) != null) { + throw new IllegalArgumentException( + "Cannot use internal id strategy with custom property " + idProperty.getPropertyName() + + " on entity class " + this.getUnderlyingClass().getName()); + } + + if (!VALID_GENERATED_ID_TYPES.contains(idProperty.getActualType())) { + throw new IllegalArgumentException("Internally generated ids can only be assigned to one of " + VALID_GENERATED_ID_TYPES); + } } return new IdDescription(idAnnotation.strategy(), idAnnotation.generator(), diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java index 7f98ed605..b5a273b9f 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java @@ -28,7 +28,6 @@ import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; -import org.springframework.data.mapping.Association; import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.Node; @@ -36,6 +35,7 @@ import org.neo4j.springframework.data.core.schema.NodeDescription; import org.neo4j.springframework.data.core.schema.Property; import org.neo4j.springframework.data.core.schema.Relationship; import org.neo4j.springframework.data.core.schema.RelationshipDescription; +import org.springframework.data.mapping.Association; /** * @author Michael J. Simons @@ -107,6 +107,16 @@ public class Neo4jMappingContextTest { .withMessageMatching("Cannot use internal id strategy with custom property getMappingFunctionFor on entity .*"); } + @Test + void shouldPreventIllegalIdTypes() { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + schema.setInitialEntitySet(new HashSet<>(Arrays.asList(InvalidIdType.class))); + assertThatIllegalArgumentException() + .isThrownBy(() -> schema.initialize()) + .withMessageMatching("Internally generated ids can only be assigned to one of .*"); + } + @Test void shouldNotProvideMappingForUnknownClasses() { @@ -157,7 +167,7 @@ public class Neo4jMappingContextTest { static class TripNode { - @Id + @Id(strategy = Id.Strategy.ASSIGNED) private String id; String name; @@ -168,4 +178,10 @@ public class Neo4jMappingContextTest { @Id @Property("getMappingFunctionFor") private String id; } + + static class InvalidIdType { + + @Id + private String id; + } } From 7946d49c69deac79ab01a4a415003901a75032e5 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 26 Jun 2019 00:11:45 +0200 Subject: [PATCH 135/342] Use extractNeo4jTransaction consistently and remove holder from tx status. --- .../transaction/Neo4jTransactionManager.java | 2 +- .../ReactiveNeo4jTransactionManager.java | 28 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java index ae3ac10d1..b6c7324d8 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java @@ -208,7 +208,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); transactionObject.getRequiredResourceHolder().close(); - + transactionObject.setResourceHolder(null); TransactionSynchronizationManager.unbindResource(driver); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 7c170f7f4..802900bdc 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -171,27 +171,33 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager transactionSynchronizationManager, Object transaction) { - ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager - .getResource(driver); - return holder.close().doOnNext(ignoreMe -> transactionSynchronizationManager.unbindResource(driver)).then(); + return Mono + .just(extractNeo4jTransaction(transaction)) + .map(r -> { + ReactiveNeo4jTransactionHolder holder = r.getRequiredResourceHolder(); + r.setResourceHolder(null); + return holder; + }) + .flatMap(ReactiveNeo4jTransactionHolder::close) + .then(Mono.fromRunnable(() -> transactionSynchronizationManager.unbindResource(driver))); } @Override protected Mono doCommit(TransactionSynchronizationManager transactionSynchronizationManager, GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { - ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager - .getResource(driver); - return holder.commit().then(); + ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction) + .getRequiredResourceHolder(); + return holder.commit(); } @Override protected Mono doRollback(TransactionSynchronizationManager transactionSynchronizationManager, GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { - ReactiveNeo4jTransactionHolder holder = (ReactiveNeo4jTransactionHolder) transactionSynchronizationManager - .getResource(driver); - return holder.rollback().then(); + ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction) + .getRequiredResourceHolder(); + return holder.rollback(); } /* @@ -200,10 +206,10 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction */ @Override protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, - GenericReactiveTransaction status) throws TransactionException { + GenericReactiveTransaction genericReactiveTransaction) throws TransactionException { return Mono.fromRunnable(() -> { - ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(status); + ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(genericReactiveTransaction); transactionObject.getRequiredResourceHolder().setRollbackOnly(); }); } From 3b058210bf67cf689f395a928888809686f15712 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 26 Jun 2019 14:20:32 +0200 Subject: [PATCH 136/342] Provide basic callback infrastructure and auditing support. (#17) * Update Spring Data commons to 2.2.0-RC1. Also update Spring Data R2DBC to a buildsnapshot as its reactive transaction manager doesn't work with the latest Spring Framework 5.2 and Spring Data Parent installments. * Provide BeforeBindCallback and related infrastructure. * Provide reactive BeforeBindCallback and related infrastructure. * Implement imperative and reactive auditing of entities. --- pom.xml | 6 +- spring-data-neo4j/pom.xml | 2 +- .../data/config/EnableNeo4jAuditing.java | 74 ++++++++ .../data/config/Neo4jAuditingRegistrar.java | 138 +++++++++++++++ .../ReactiveNeo4jTransactionManager.java | 2 +- .../event/AuditingBeforeBindCallback.java | 72 ++++++++ .../repository/event/BeforeBindCallback.java | 45 +++++ .../ReactiveAuditingBeforeBindCallback.java | 76 +++++++++ .../event/ReactiveBeforeBindCallback.java | 48 ++++++ .../data/repository/event/package-info.java | 17 ++ .../support/Neo4jEntityInformation.java | 2 +- .../data/repository/support/Neo4jEvents.java | 47 ++++++ .../support/Neo4jRepositoryFactory.java | 8 +- .../support/Neo4jRepositoryFactoryBean.java | 13 +- .../support/ReactiveNeo4jEvents.java | 51 ++++++ .../ReactiveNeo4jRepositoryFactory.java | 7 +- .../ReactiveNeo4jRepositoryFactoryBean.java | 13 +- .../support/SimpleNeo4jRepository.java | 25 ++- .../SimpleReactiveNeo4jRepository.java | 71 +++++--- .../config/Neo4jAuditingRegistrarTest.java | 54 ++++++ .../ReactiveNeo4jTransactionManagerTest.java | 10 +- .../integration/imperative/AuditingIT.java | 119 +++++++++++++ .../integration/imperative/CallbacksIT.java | 104 ++++++++++++ .../reactive/ReactiveAuditingIT.java | 146 ++++++++++++++++ .../reactive/ReactiveCallbacksIT.java | 159 ++++++++++++++++++ .../integration/shared/AuditingITBase.java | 101 +++++++++++ .../integration/shared/CallbacksITBase.java | 82 +++++++++ .../shared/ImmutableAuditableThing.java | 55 ++++++ .../event/AuditingBeforeBindCallbackTest.java | 110 ++++++++++++ .../repository/event/ImmutableSample.java | 46 +++++ ...eactiveAuditingBeforeBindCallbackTest.java | 118 +++++++++++++ .../data/repository/event/Sample.java | 39 +++++ 32 files changed, 1807 insertions(+), 53 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/EnableNeo4jAuditing.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jAuditingRegistrar.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/AuditingBeforeBindCallback.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/BeforeBindCallback.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallback.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveBeforeBindCallback.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/package-info.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEvents.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jEvents.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/config/Neo4jAuditingRegistrarTest.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/AuditingITBase.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/CallbacksITBase.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ImmutableAuditableThing.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/AuditingBeforeBindCallbackTest.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ImmutableSample.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallbackTest.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/Sample.java diff --git a/pom.xml b/pom.xml index 059312b5f..a6e72c6f8 100644 --- a/pom.xml +++ b/pom.xml @@ -21,9 +21,9 @@ 4.0.0 - spring-data-parent org.springframework.data.build - 2.2.0.M4 + spring-data-parent + 2.2.0.RC1 org.neo4j.springframework.data @@ -106,7 +106,7 @@ 1.3.8 2.2.5 1.7.26 - 2.2.0.M4 + 2.2.0.RC1 1.0.0.BUILD-SNAPSHOT 1.10.7 diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 41f14fc45..c437539dd 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -67,7 +67,7 @@ org.springframework.data spring-data-r2dbc - 1.0.0.M2 + 1.0.0.BUILD-SNAPSHOT test diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/EnableNeo4jAuditing.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/EnableNeo4jAuditing.java new file mode 100644 index 000000000..711dab877 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/EnableNeo4jAuditing.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; + +/** + * Annotation to enable auditing for SDN-RX entities via annotation configuration. + * + * @author Michael J. Simons + * @since 1.0 + * @soundtrack Iron Maiden - Killers + */ +@Inherited +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(Neo4jAuditingRegistrar.class) +public @interface EnableNeo4jAuditing { + + /** + * Configures the {@link AuditorAware} bean to be used to lookup the current principal. + * + * @return + */ + String auditorAwareRef() default ""; + + /** + * Configures whether the creation and modification dates are set. Defaults to {@literal true}. + * + * @return + */ + boolean setDates() default true; + + /** + * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}. + * + * @return + */ + boolean modifyOnCreate() default true; + + /** + * Configures a {@link DateTimeProvider} bean name that allows customizing actual date time class to be + * used for setting creation and modification dates. + * + * @return + */ + String dateTimeProviderRef() default ""; +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jAuditingRegistrar.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jAuditingRegistrar.java new file mode 100644 index 000000000..90dcf3adf --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/config/Neo4jAuditingRegistrar.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.config; + +import java.lang.annotation.Annotation; + +import org.neo4j.springframework.data.repository.event.AuditingBeforeBindCallback; +import org.neo4j.springframework.data.repository.event.ReactiveAuditingBeforeBindCallback; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; +import org.springframework.data.auditing.config.AuditingConfiguration; +import org.springframework.data.config.ParsingUtils; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * @author Michael J. Simons + * @soundtrack Iron Maiden - Killers + * @since 1.0 + */ +final class Neo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + + private static final boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono", + Neo4jAuditingRegistrar.class.getClassLoader()); + + private static final String AUDITING_HANDLER_BEAN_NAME = "neo4jAuditingHandler"; + private static final String MAPPING_CONTEXT_BEAN_NAME = "neo4jMappingContext"; + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableNeo4jAuditing.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() + */ + @Override + protected String getAuditingHandlerBeanName() { + return AUDITING_HANDLER_BEAN_NAME; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { + + Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + + super.registerBeanDefinitions(annotationMetadata, registry); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerAuditListener(org.springframework.beans.factory.config.BeanDefinition, org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @Override + protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, + BeanDefinitionRegistry registry) { + + Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + + BeanDefinitionBuilder listenerBeanDefinitionBuilder = BeanDefinitionBuilder + .rootBeanDefinition(AuditingBeforeBindCallback.class); + listenerBeanDefinitionBuilder + .addConstructorArgValue( + ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + + registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), + AuditingBeforeBindCallback.class.getName(), registry); + + if (PROJECT_REACTOR_AVAILABLE) { + registerReactiveAuditingEntityCallback(registry, auditingHandlerDefinition.getSource()); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration) + */ + @Override + protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { + + Assert.notNull(configuration, "AuditingConfiguration must not be null!"); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class); + + BeanDefinitionBuilder persistentEntities = BeanDefinitionBuilder + .genericBeanDefinition(PersistentEntities.class) + .setFactoryMethod("of"); + persistentEntities.addConstructorArgReference(MAPPING_CONTEXT_BEAN_NAME); + + builder.addConstructorArgValue(persistentEntities.getBeanDefinition()); + return configureDefaultAuditHandlerAttributes(configuration, builder); + } + + private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, Object source) { + + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .rootBeanDefinition(ReactiveAuditingBeforeBindCallback.class); + + builder.addConstructorArgValue( + ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + builder.getRawBeanDefinition().setSource(source); + + registerInfrastructureBeanWithId(builder.getBeanDefinition(), + ReactiveAuditingBeforeBindCallback.class.getName(), registry); + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 802900bdc..8cf9ec5fc 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -70,7 +70,7 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction public static Mono retrieveReactiveTransaction(final Driver driver, final String targetDatabase) { - return TransactionSynchronizationManager.currentTransaction() // Do we have a Transaction context? + return TransactionSynchronizationManager.forCurrentTransaction() // Do we have a Transaction context? // Bail out early if synchronization between transaction managers is not active .filter(TransactionSynchronizationManager::isSynchronizationActive) .flatMap(tsm -> { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/AuditingBeforeBindCallback.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/AuditingBeforeBindCallback.java new file mode 100644 index 000000000..a44a22ab5 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/AuditingBeforeBindCallback.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import org.apiguardian.api.API; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.util.Assert; + +/** + * {@link EntityCallback} to populate auditing related fields on an entity about to be bound to a record. + * + * @author Michael J. Simons + * @soundtrack Iron Maiden - Iron Maiden + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class AuditingBeforeBindCallback implements BeforeBindCallback, Ordered { + + public static final int NEO4J_AUDITING_ORDER = 100; + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link AuditingBeforeBindCallback} using the given {@link AuditingHandler} + * provided by the given {@link ObjectFactory}. + * + * @param auditingHandlerFactory must not be {@literal null}. + */ + public AuditingBeforeBindCallback(ObjectFactory auditingHandlerFactory) { + + Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + this.auditingHandlerFactory = auditingHandlerFactory; + } + + /* + * (non-Javadoc) + * @see org.neo4j.springframework.data.repository.event.BeforeBindCallback#onBeforeBind(java.lang.Object) + */ + @Override + public Object onBeforeBind(Object entity) { + return auditingHandlerFactory.getObject().markAudited(entity); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return NEO4J_AUDITING_ORDER; + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/BeforeBindCallback.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/BeforeBindCallback.java new file mode 100644 index 000000000..bc5135855 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/BeforeBindCallback.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import org.apiguardian.api.API; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Entity callback triggered before an Entity is bound to a record (represented by a {@link java.util.Map java.util.Map<String, Object>}). + * + * @author Michael J. Simons + * @param The type of the entity. + * @since 1.0 + * @soundtrack Bon Jovi - Slippery When Wet + */ +@FunctionalInterface +@API(status = API.Status.STABLE, since = "1.0") +public interface BeforeBindCallback extends EntityCallback { + + /** + * Entity callback method invoked before a domain object is saved. Can return either the same or a modified instance + * of the domain object. This method is called before converting the {@code entity} to a {@link java.util.Map}, + * so the outcome of this callback is used to create the record for the domain object. + * + * @param entity the domain object to save. + * @return the domain object to be persisted. + */ + T onBeforeBind(T entity); +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallback.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallback.java new file mode 100644 index 000000000..435bec8a3 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallback.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import reactor.core.publisher.Mono; + +import org.apiguardian.api.API; +import org.reactivestreams.Publisher; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.util.Assert; + +/** + * Reactive {@link EntityCallback} to populate auditing related fields on an entity about to be bound to a record. + * + * @author Michael J. Simons + * @soundtrack Iron Maiden - The Number Of The Beast + * @see AuditingBeforeBindCallback + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class ReactiveAuditingBeforeBindCallback implements ReactiveBeforeBindCallback, Ordered { + + public static final int NEO4J_REACTIVE_AUDITING_ORDER = 100; + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link ReactiveAuditingBeforeBindCallback} using the {@link AuditingHandler} provided by the + * given {@link ObjectFactory}. + * + * @param auditingHandlerFactory must not be {@literal null}. + */ + public ReactiveAuditingBeforeBindCallback(ObjectFactory auditingHandlerFactory) { + + Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + this.auditingHandlerFactory = auditingHandlerFactory; + } + + /* + * (non-Javadoc) + * @see org.neo4j.springframework.data.repository.event.ReactiveBeforeBindCallback#onBeforeBind(java.lang.Object) + */ + @Override + public Publisher onBeforeBind(Object entity) { + return Mono.just(auditingHandlerFactory.getObject().markAudited(entity)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return NEO4J_REACTIVE_AUDITING_ORDER; + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveBeforeBindCallback.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveBeforeBindCallback.java new file mode 100644 index 000000000..488196d47 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveBeforeBindCallback.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import org.apiguardian.api.API; +import org.reactivestreams.Publisher; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; + +/** + * Entity callback triggered before an Entity is bound to a record (represented by a {@link java.util.Map java.util.Map<String, Object>}). + * + * @param The type of the entity. + * @author Michael J. Simons + * @soundtrack Iron Maiden - Killers + * @see ReactiveEntityCallbacks + * @since 1.0 + */ +@FunctionalInterface +@API(status = API.Status.STABLE, since = "1.0") +public interface ReactiveBeforeBindCallback extends EntityCallback { + + /** + * Entity callback method invoked before a domain object is saved. Can return either the same or a modified instance + * of the domain object. This method is called before converting the {@code entity} to a {@link java.util.Map}, + * so the outcome of this callback is used to create the record for the domain object. + * + * @param entity the domain object to save. + * @return the domain object to be persisted. + */ + Publisher onBeforeBind(T entity); +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/package-info.java new file mode 100644 index 000000000..5263bade3 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/package-info.java @@ -0,0 +1,17 @@ +/** + * Contains the infrastructure for the event system. The event system comes in two flavours: Events that are based + * on Spring's application event system and callbacks that are based on Spring Data's callback system. Application + * events can be configured to run asynchronously, which make them a bad fit in transactional workloads. + *

+ * As a rule of thumb, use Entity callbacks for modifying entities before persisting and application events otherwise. + * The best option however to react in a transactional way to changes of an entity is to implement + * {@link org.springframework.data.domain.DomainEvents} on an aggregate root. + * + * @author Michael J. Simons + * @since 1.0 + * @soundtrack Bon Jovi - Slippery When Wet + */ +@NonNullApi +package org.neo4j.springframework.data.repository.event; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java index 205eabe4c..9f4a9b16d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEntityInformation.java @@ -43,7 +43,7 @@ public interface Neo4jEntityInformation extends EntityInformation * Retrieves an expression to be used in Cypher statements to retrieve the generated or internal id for a node * fitting the given {@code nodeDescription} under a symbolic name of {@code 'n'}. * - * @return An expression to address nodes corresponding to entities of the given typoe by id. + * @return An expression to address nodes corresponding to entities of the given type by id. */ Expression getIdExpression(); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEvents.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEvents.java new file mode 100644 index 000000000..da3273707 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jEvents.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.support; + +import org.neo4j.springframework.data.repository.event.BeforeBindCallback; +import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.lang.Nullable; + +/** + * Utility class that orchestrates {@link EntityCallbacks}. + * All the methods provided here check for their availability and do nothing when an event cannot be published. + * + * @author Michael J. Simons + * @since 1.0 + */ +final class Neo4jEvents { + + private final @Nullable EntityCallbacks entityCallbacks; + + Neo4jEvents(@Nullable EntityCallbacks entityCallbacks) { + this.entityCallbacks = entityCallbacks; + } + + T maybeCallBeforeBind(T object) { + if (entityCallbacks != null) { + return entityCallbacks.callback(BeforeBindCallback.class, object); + } + + return object; + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java index 6a1934b96..52b70a39c 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactory.java @@ -57,9 +57,12 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { private final SchemaBasedStatementBuilder schemaBasedStatementBuilder; - Neo4jRepositoryFactory(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext) { + private final Neo4jEvents eventSupport; + + Neo4jRepositoryFactory(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, Neo4jEvents eventSupport) { this.neo4jClient = neo4jClient; this.mappingContext = mappingContext; + this.eventSupport = eventSupport; this.schemaBasedStatementBuilder = CypherAdapterUtils.createSchemaBasedStatementBuilder(this.mappingContext); } @@ -79,8 +82,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); return getTargetRepositoryViaReflection(metadata, - neo4jClient, entityInformation, schemaBasedStatementBuilder - ); + neo4jClient, entityInformation, schemaBasedStatementBuilder, eventSupport); } @Override diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java index c07309841..335d38daf 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Neo4jRepositoryFactoryBean.java @@ -24,9 +24,12 @@ import org.apiguardian.api.API; import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; +import org.springframework.lang.Nullable; /** * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of @@ -47,6 +50,8 @@ public final class Neo4jRepositoryFactoryBean, S, ID private Neo4jMappingContext neo4jMappingContext; + private @Nullable EntityCallbacks entityCallbacks; + /** * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface. * @@ -67,7 +72,13 @@ public final class Neo4jRepositoryFactoryBean, S, ID @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { - return new Neo4jRepositoryFactory(neo4jClient, neo4jMappingContext); + return new Neo4jRepositoryFactory(neo4jClient, neo4jMappingContext, new Neo4jEvents(entityCallbacks)); } + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + + this.entityCallbacks = EntityCallbacks.create(beanFactory); + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jEvents.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jEvents.java new file mode 100644 index 000000000..64bfae62e --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jEvents.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.support; + +import reactor.core.publisher.Mono; + +import org.neo4j.springframework.data.repository.event.ReactiveBeforeBindCallback; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; +import org.springframework.lang.Nullable; + +/** + * Utility class that orchestrates {@link ReactiveEntityCallbacks}. + * All the methods provided here check for their availability and do nothing when an event cannot be published. + * + * @author Michael J. Simons + * @soundtrack Iron Maiden - Killers + * @since 1.0 + */ +final class ReactiveNeo4jEvents { + + private final @Nullable ReactiveEntityCallbacks entityCallbacks; + + ReactiveNeo4jEvents(@Nullable ReactiveEntityCallbacks entityCallbacks) { + this.entityCallbacks = entityCallbacks; + } + + Mono maybeCallBeforeBind(T object) { + + if (entityCallbacks != null) { + return entityCallbacks.callback(ReactiveBeforeBindCallback.class, object); + } + + return Mono.just(object); + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java index 173d62131..500a73a60 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -56,9 +56,12 @@ final class ReactiveNeo4jRepositoryFactory extends RepositoryFactorySupport { private final SchemaBasedStatementBuilder schemaBasedStatementBuilder; - ReactiveNeo4jRepositoryFactory(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext) { + private final ReactiveNeo4jEvents eventSupport; + + ReactiveNeo4jRepositoryFactory(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext, ReactiveNeo4jEvents eventSupport) { this.neo4jClient = neo4jClient; this.mappingContext = mappingContext; + this.eventSupport = eventSupport; this.schemaBasedStatementBuilder = CypherAdapterUtils.createSchemaBasedStatementBuilder(this.mappingContext); } @@ -78,7 +81,7 @@ final class ReactiveNeo4jRepositoryFactory extends RepositoryFactorySupport { Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); return getTargetRepositoryViaReflection(metadata, - neo4jClient, entityInformation, schemaBasedStatementBuilder + neo4jClient, entityInformation, schemaBasedStatementBuilder, eventSupport ); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index 889fc08b4..66839bb64 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -24,9 +24,12 @@ import org.apiguardian.api.API; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; +import org.springframework.lang.Nullable; /** * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of @@ -47,6 +50,8 @@ public final class ReactiveNeo4jRepositoryFactoryBean implements PagingAndSortingRepository private final SchemaBasedStatementBuilder statementBuilder; + private final Neo4jEvents eventSupport; + SimpleNeo4jRepository(Neo4jClient neo4jClient, Neo4jEntityInformation entityInformation, - SchemaBasedStatementBuilder statementBuilder) { + SchemaBasedStatementBuilder statementBuilder, + Neo4jEvents eventSupport) { this.neo4jClient = neo4jClient; this.entityInformation = entityInformation; this.entityMetaData = this.entityInformation.getEntityMetaData(); this.statementBuilder = statementBuilder; + this.eventSupport = eventSupport; } @Override @@ -116,16 +119,17 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository @Transactional public S save(S entity) { + S entityToBeSaved = eventSupport.maybeCallBeforeBind(entity); Long internalId = neo4jClient .query(() -> renderer.render(statementBuilder.prepareSaveOf(entityMetaData))) - .bind((T) entity) + .bind((T) entityToBeSaved) .with(entityInformation.getBinderFunction()) .fetchAs(Long.class).one().get(); if (!entityMetaData.isUsingInternalIds()) { - return entity; + return entityToBeSaved; } else { - PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entity); + PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId); return (S) propertyAccessor.getBean(); } @@ -137,14 +141,17 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository if (entityMetaData.isUsingInternalIds()) { log.debug("Saving entities using single statements."); + return StreamSupport.stream(entities.spliterator(), false) .map(this::save) .collect(toList()); } - final Function> binderFunction = entityInformation.getBinderFunction(); - List> entityList = StreamSupport.stream(entities.spliterator(), false) - .map(binderFunction).collect(toList()); + List entitiesToBeSaved = StreamSupport.stream(entities.spliterator(), false) + .map(eventSupport::maybeCallBeforeBind) + .collect(toList()); + List> entityList = entitiesToBeSaved.stream() + .map(entityInformation.getBinderFunction()).collect(toList()); ResultSummary resultSummary = neo4jClient .query(() -> renderer.render(statementBuilder.prepareSaveOfMultipleInstancesOf(entityMetaData))) @@ -156,7 +163,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(), counters.relationshipsDeleted(), counters.propertiesSet()); - return entities; + return entitiesToBeSaved; } @Override diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java index 334db0ccd..3e50a3863 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java @@ -74,12 +74,16 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< private final SchemaBasedStatementBuilder statementBuilder; + private final ReactiveNeo4jEvents eventSupport; + SimpleReactiveNeo4jRepository(ReactiveNeo4jClient neo4jClient, Neo4jEntityInformation entityInformation, - SchemaBasedStatementBuilder statementBuilder) { + SchemaBasedStatementBuilder statementBuilder, + ReactiveNeo4jEvents eventSupport) { this.neo4jClient = neo4jClient; this.entityInformation = entityInformation; this.entityMetaData = this.entityInformation.getEntityMetaData(); this.statementBuilder = statementBuilder; + this.eventSupport = eventSupport; } @Override @@ -154,21 +158,26 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Transactional public Mono save(S entity) { - Statement saveStatement = statementBuilder.prepareSaveOf(entityMetaData); + return Mono.just(entity) + .flatMap(eventSupport::maybeCallBeforeBind) + .flatMap(it -> { + Statement saveStatement = statementBuilder.prepareSaveOf(entityMetaData); - Mono idMono = this.neo4jClient.query(() -> renderer.render(saveStatement)) - .bind((T) entity).with(entityInformation.getBinderFunction()) - .fetchAs(Long.class).one(); + Mono idMono = + this.neo4jClient.query(() -> renderer.render(saveStatement)) + .bind((T) it).with(entityInformation.getBinderFunction()) + .fetchAs(Long.class).one(); - if (!entityMetaData.isUsingInternalIds()) { - return idMono.thenReturn(entity); - } else { - return idMono.map(internalId -> { - PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entity); - propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId); - return (S) propertyAccessor.getBean(); + if (!entityMetaData.isUsingInternalIds()) { + return idMono.thenReturn(it); + } else { + return idMono.map(internalId -> { + PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(it); + propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId); + return (S) propertyAccessor.getBean(); + }); + } }); - } } @Override @@ -182,20 +191,30 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< } final Function> binderFunction = entityInformation.getBinderFunction(); - List> boundedEntityList = StreamSupport.stream(entities.spliterator(), false) - .map(binderFunction) - .collect(toList()); + return Flux.fromIterable(entities) + .flatMap(eventSupport::maybeCallBeforeBind) + .collectList() + .flatMapMany( + entitiesToBeSaved -> Mono + .defer(() -> { // Defer the actual save statement until the previous flux completes + List> boundedEntityList = entitiesToBeSaved.stream() + .map(binderFunction) + .collect(toList()); - return Mono.from(neo4jClient - .query(() -> renderer.render(statementBuilder.prepareSaveOfMultipleInstancesOf(entityMetaData))) - .bind(boundedEntityList).to(NAME_OF_ENTITY_LIST_PARAM).run()) - .doOnNext(resultSummary -> { - SummaryCounters counters = resultSummary.counters(); - log.debug( - "Created {} and deleted {} nodes, created {} and deleted {} relationships and set {} properties.", - counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(), - counters.relationshipsDeleted(), counters.propertiesSet()); - }).thenMany(Flux.fromIterable(entities)); + return neo4jClient + .query(() -> renderer + .render(statementBuilder.prepareSaveOfMultipleInstancesOf(entityMetaData))) + .bind(boundedEntityList).to(NAME_OF_ENTITY_LIST_PARAM).run(); + }) + .doOnNext(resultSummary -> { + SummaryCounters counters = resultSummary.counters(); + log.debug( + "Created {} and deleted {} nodes, created {} and deleted {} relationships and set {} properties.", + counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(), + counters.relationshipsDeleted(), counters.propertiesSet()); + }) + .thenMany(Flux.fromIterable(entitiesToBeSaved)) + ); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/config/Neo4jAuditingRegistrarTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/config/Neo4jAuditingRegistrarTest.java new file mode 100644 index 000000000..495f4b5ce --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/config/Neo4jAuditingRegistrarTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.config; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.type.AnnotationMetadata; + +/** + * @author Michael J. Simons + */ +@ExtendWith(MockitoExtension.class) +class Neo4jAuditingRegistrarTest { + + @Mock AnnotationMetadata metadata; + @Mock BeanDefinitionRegistry registry; + + Neo4jAuditingRegistrar registrar = new Neo4jAuditingRegistrar(); + + @Test + public void rejectsNullAnnotationMetadata() { + + assertThatIllegalArgumentException() + .isThrownBy(() -> registrar.registerBeanDefinitions(null, registry)); + } + + @Test + public void rejectsNullBeanDefinitionRegistry() { + + assertThatIllegalArgumentException() + .isThrownBy(() -> registrar.registerBeanDefinitions(metadata, null)); + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java index 93a9976d0..f6b6d4b78 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java @@ -97,7 +97,7 @@ class ReactiveNeo4jTransactionManagerTest { transactionalOperator .execute(transactionStatus -> TransactionSynchronizationManager - .currentTransaction().doOnNext(tsm -> { + .forCurrentTransaction().doOnNext(tsm -> { assertThat(tsm.hasResource(driver)).isTrue(); transactionStatus.setRollbackOnly(); }).then(retrieveReactiveTransaction(driver, databaseName)) @@ -155,9 +155,9 @@ class ReactiveNeo4jTransactionManagerTest { transactionalOperator .execute(transactionStatus -> TransactionSynchronizationManager - .currentTransaction().doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isFalse()) + .forCurrentTransaction().doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isFalse()) .then(retrieveReactiveTransaction(driver, databaseName)) - .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.currentTransaction() + .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.forCurrentTransaction() .doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isTrue())) ) .as(StepVerifier::create) @@ -182,13 +182,13 @@ class ReactiveNeo4jTransactionManagerTest { transactionalOperator .execute(transactionStatus -> TransactionSynchronizationManager - .currentTransaction() + .forCurrentTransaction() .doOnNext(tsm -> { assertThat(tsm.hasResource(driver)).isFalse(); transactionStatus.setRollbackOnly(); }) .then(retrieveReactiveTransaction(driver, databaseName)) - .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.currentTransaction() + .flatMap(ignoredNativeTx -> TransactionSynchronizationManager.forCurrentTransaction() .doOnNext(tsm -> assertThat(tsm.hasResource(driver)).isTrue())) ) .as(StepVerifier::create) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java new file mode 100644 index 000000000..08a266ecc --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.imperative; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.config.EnableNeo4jAuditing; +import org.neo4j.springframework.data.integration.shared.AuditingITBase; +import org.neo4j.springframework.data.integration.shared.ImmutableAuditableThing; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Michael J. Simons + */ +@ContextConfiguration(classes = AuditingIT.Config.class) +class AuditingIT extends AuditingITBase { + + private final TestRepository thingRepository; + + @Autowired + AuditingIT(TestRepository thingRepository, Driver driver) { + super(driver); + this.thingRepository = thingRepository; + } + + @Test + void auditingOfCreationShouldWork() { + + ImmutableAuditableThing thing = new ImmutableAuditableThing("A thing"); + thing = thingRepository.save(thing); + + assertThat(thing.getCreatedAt()).isEqualTo(DEFAULT_CREATION_AND_MODIFICATION_DATE); + assertThat(thing.getCreatedBy()).isEqualTo("A user"); + + assertThat(thing.getModifiedAt()).isNull(); + assertThat(thing.getModifiedBy()).isNull(); + + verifyDatabase(thing.getId(), thing); + } + + @Test + void auditingOfModificationShouldWork() { + + ImmutableAuditableThing thing = thingRepository.findById(idOfExistingThing).get(); + thing = thing.withName("A new name"); + thing = thingRepository.save(thing); + + assertThat(thing.getCreatedAt()).isEqualTo(EXISTING_THING_CREATED_AT); + assertThat(thing.getCreatedBy()).isEqualTo(EXISTING_THING_CREATED_BY); + + assertThat(thing.getModifiedAt()).isEqualTo(DEFAULT_CREATION_AND_MODIFICATION_DATE); + assertThat(thing.getModifiedBy()).isEqualTo("A user"); + + assertThat(thing.getName()).isEqualTo("A new name"); + + verifyDatabase(idOfExistingThing, thing); + } + + public interface TestRepository extends CrudRepository { + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories(considerNestedRepositories = true) + @EnableNeo4jAuditing(modifyOnCreate = false, auditorAwareRef = "auditorProvider", dateTimeProviderRef = "fixedDateTimeProvider") + static class Config extends AbstractNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(ImmutableAuditableThing.class.getPackage().getName()); + } + + @Bean + public AuditorAware auditorProvider() { + return () -> Optional.of("A user"); + } + + @Bean + public DateTimeProvider fixedDateTimeProvider() { + return () -> Optional.of(DEFAULT_CREATION_AND_MODIFICATION_DATE); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java new file mode 100644 index 000000000..5f2c31186 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.imperative; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.CallbacksITBase; +import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.neo4j.springframework.data.repository.event.BeforeBindCallback; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Michael J. Simons + */ +@ContextConfiguration(classes = CallbacksIT.Config.class) +class CallbacksIT extends CallbacksITBase { + + private final ThingRepository thingRepository; + + @Autowired CallbacksIT(ThingRepository thingRepository, Driver driver) { + super(driver); + this.thingRepository = thingRepository; + } + + @Test + void onBeforeBindShouldBeCalledForSingleEntity() { + + ThingWithAssignedId thing = new ThingWithAssignedId("aaBB"); + thing.setName("A name"); + thing = thingRepository.save(thing); + + assertThat(thing.getName()).isEqualTo("A name (Edited)"); + + verifyDatabase(Collections.singletonList(thing)); + } + + @Test + void onBeforeBindShouldBeCalledForAllEntities() { + + ThingWithAssignedId thing1 = new ThingWithAssignedId("id1"); + thing1.setName("A name"); + ThingWithAssignedId thing2 = new ThingWithAssignedId("id2"); + thing2.setName("Another name"); + Iterable savedThings = thingRepository.saveAll(Arrays.asList(thing1, thing2)); + + assertThat(savedThings).extracting(ThingWithAssignedId::getName) + .containsExactlyInAnyOrder("A name (Edited)", "Another name (Edited)"); + + verifyDatabase(savedThings); + } + + @Configuration + @EnableNeo4jRepositories + @EnableTransactionManagement + static class Config extends AbstractNeo4jConfig { + + @Bean + BeforeBindCallback nameChanger() { + return entity -> { + ThingWithAssignedId updatedThing = new ThingWithAssignedId(entity.getTheId()); + updatedThing.setName(entity.getName() + " (Edited)"); + return updatedThing; + }; + } + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(ThingWithAssignedId.class.getPackage().getName()); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java new file mode 100644 index 000000000..f8e0d822d --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.reactive; + +import static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; +import org.neo4j.springframework.data.config.EnableNeo4jAuditing; +import org.neo4j.springframework.data.integration.shared.AuditingITBase; +import org.neo4j.springframework.data.integration.shared.ImmutableAuditableThing; +import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.reactive.TransactionalOperator; + +/** + * @author Michael J. Simons + */ +@ContextConfiguration(classes = ReactiveAuditingIT.Config.class) +class ReactiveAuditingIT extends AuditingITBase { + + private final ReactiveTransactionManager transactionManager; + private final ReactiveTestRepository thingRepository; + + @Autowired + ReactiveAuditingIT(ReactiveTransactionManager transactionManager, ReactiveTestRepository thingRepository, + Driver driver) { + super(driver); + this.thingRepository = thingRepository; + this.transactionManager = transactionManager; + } + + @Test + void auditingOfCreationShouldWork() { + + List newThings = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> thingRepository.save(new ImmutableAuditableThing("A thing"))) + .as(StepVerifier::create) + .recordWith(() -> newThings) + .expectNextCount(1L) + .verifyComplete(); + + ImmutableAuditableThing savedThing = newThings.get(0); + assertThat(savedThing.getCreatedAt()).isEqualTo(DEFAULT_CREATION_AND_MODIFICATION_DATE); + assertThat(savedThing.getCreatedBy()).isEqualTo("A user"); + + assertThat(savedThing.getModifiedAt()).isNull(); + assertThat(savedThing.getModifiedBy()).isNull(); + + verifyDatabase(savedThing.getId(), savedThing); + } + + @Test + void auditingOfModificationShouldWork() { + + Mono findAndUpdateAThing = thingRepository.findById(idOfExistingThing) + .flatMap(thing -> thingRepository.save(thing.withName("A new name"))); + + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> findAndUpdateAThing) + .as(StepVerifier::create) + .consumeNextWith(savedThing -> { + + assertThat(savedThing.getCreatedAt()).isEqualTo(EXISTING_THING_CREATED_AT); + assertThat(savedThing.getCreatedBy()).isEqualTo(EXISTING_THING_CREATED_BY); + + assertThat(savedThing.getModifiedAt()).isEqualTo(DEFAULT_CREATION_AND_MODIFICATION_DATE); + assertThat(savedThing.getModifiedBy()).isEqualTo("A user"); + + assertThat(savedThing.getName()).isEqualTo("A new name"); + }) + .verifyComplete(); + + // Need to happen outside the reactive flow, as we use the blocking session to verify the database + verifyDatabase(idOfExistingThing, + new ImmutableAuditableThing(null, EXISTING_THING_CREATED_AT, EXISTING_THING_CREATED_BY, + DEFAULT_CREATION_AND_MODIFICATION_DATE, "A user", "A new name")); + } + + public interface ReactiveTestRepository extends ReactiveCrudRepository { + } + + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + @EnableNeo4jAuditing(modifyOnCreate = false, auditorAwareRef = "auditorProvider", dateTimeProviderRef = "fixedDateTimeProvider") + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(ImmutableAuditableThing.class.getPackage().getName()); + } + + @Bean + public AuditorAware auditorProvider() { + return () -> Optional.of("A user"); + } + + @Bean + public DateTimeProvider fixedDateTimeProvider() { + return () -> Optional.of(DEFAULT_CREATION_AND_MODIFICATION_DATE); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java new file mode 100644 index 000000000..0322202af --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.reactive; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.CallbacksITBase; +import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; +import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; +import org.neo4j.springframework.data.repository.event.ReactiveBeforeBindCallback; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.reactive.TransactionalOperator; + +/** + * @author Michael J. Simons + */ +@ContextConfiguration(classes = ReactiveCallbacksIT.Config.class) +class ReactiveCallbacksIT extends CallbacksITBase { + + private static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private final ReactiveTransactionManager transactionManager; + private final ReactiveThingRepository thingRepository; + + @Autowired + ReactiveCallbacksIT(ReactiveTransactionManager transactionManager, + ReactiveThingRepository thingRepository, Driver driver) { + + super(driver); + this.transactionManager = transactionManager; + this.thingRepository = thingRepository; + } + + @Test + void onBeforeBindShouldBeCalledForSingleEntity() { + + ThingWithAssignedId thing = new ThingWithAssignedId("aaBB"); + thing.setName("A name"); + + Mono operationUnderTest = Mono.just(thing).flatMap(thingRepository::save); + + List savedThings = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .expectNextMatches(t -> t.getName().equals("A name (Edited)")) + .verifyComplete(); + + verifyDatabase(savedThings); + } + + @Test + void onBeforeBindShouldBeCalledForAllEntitiesUsingIterable() { + + ThingWithAssignedId thing1 = new ThingWithAssignedId("id1"); + thing1.setName("A name"); + ThingWithAssignedId thing2 = new ThingWithAssignedId("id2"); + thing2.setName("Another name"); + thingRepository.saveAll(Arrays.asList(thing1, thing2)); + + Flux operationUnderTest = thingRepository.saveAll(Arrays.asList(thing1, thing2)); + + List savedThings = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .expectNextMatches(t -> t.getName().equals("A name (Edited)")) + .expectNextMatches(t -> t.getName().equals("Another name (Edited)")) + .verifyComplete(); + + verifyDatabase(savedThings); + } + + @Test + void onBeforeBindShouldBeCalledForAllEntitiesUsingPublisher() { + + ThingWithAssignedId thing1 = new ThingWithAssignedId("id1"); + thing1.setName("A name"); + ThingWithAssignedId thing2 = new ThingWithAssignedId("id2"); + thing2.setName("Another name"); + thingRepository.saveAll(Arrays.asList(thing1, thing2)); + + Flux operationUnderTest = thingRepository.saveAll(Flux.just(thing1, thing2)); + + List savedThings = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> operationUnderTest) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .expectNextMatches(t -> t.getName().equals("A name (Edited)")) + .expectNextMatches(t -> t.getName().equals("Another name (Edited)")) + .verifyComplete(); + + verifyDatabase(savedThings); + } + + @Configuration + @EnableReactiveNeo4jRepositories + @EnableTransactionManagement + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + ReactiveBeforeBindCallback nameChanger() { + return entity -> { + ThingWithAssignedId updatedThing = new ThingWithAssignedId(entity.getTheId()); + updatedThing.setName(entity.getName() + " (Edited)"); + return Mono.just(updatedThing); + }; + } + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(ThingWithAssignedId.class.getPackage().getName()); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/AuditingITBase.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/AuditingITBase.java new file mode 100644 index 000000000..fa1d4b83a --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/AuditingITBase.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.shared; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.Value; +import org.neo4j.driver.Values; +import org.neo4j.driver.types.Node; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Shared information for both imperative and reactive auditing tests. + * + * @author Michael J. Simons + */ +@ExtendWith(SpringExtension.class) +@ExtendWith(Neo4jExtension.class) +public abstract class AuditingITBase { + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + protected static final String EXISTING_THING_NAME = "An old name"; + protected static final String EXISTING_THING_CREATED_BY = "The creator"; + protected static final LocalDateTime EXISTING_THING_CREATED_AT = LocalDateTime.of(2013, 5, 6, 8, 0); + protected static final LocalDateTime DEFAULT_CREATION_AND_MODIFICATION_DATE = LocalDateTime.of(2018, 7, 1, 8, 0); + + private final Driver driver; + + protected Long idOfExistingThing; + + protected AuditingITBase(Driver driver) { + this.driver = driver; + } + + @BeforeEach + protected void setupData() { + try (Transaction transaction = driver.session().beginTransaction()) { + transaction.run("MATCH (n) detach delete n"); + idOfExistingThing = transaction + .run( + "CREATE (t:ImmutableAuditableThing {name: $name, createdBy: $createdBy, createdAt: $createdAt}) RETURN id(t) as id", + Values.parameters("name", EXISTING_THING_NAME, "createdBy", EXISTING_THING_CREATED_BY, "createdAt", + EXISTING_THING_CREATED_AT)) + .single().get("id").asLong(); + + transaction.success(); + } + } + + protected void verifyDatabase(long id, ImmutableAuditableThing expectedValues) { + + try (Session session = driver.session()) { + Node node = session + .run("MATCH (t:ImmutableAuditableThing) WHERE id(t) = $id RETURN t", Values.parameters("id", id)) + .single().get("t").asNode(); + + assertThat(node.get("name").asString()).isEqualTo(expectedValues.getName()); + assertThat(node.get("createdAt").asLocalDateTime()).isEqualTo(expectedValues.getCreatedAt()); + assertThat(node.get("createdBy").asString()).isEqualTo(expectedValues.getCreatedBy()); + + Value modifiedAt = node.get("modifiedAt"); + Value modifiedBy = node.get("modifiedBy"); + + if (expectedValues.getModifiedAt() == null) { + assertThat(modifiedAt.isNull()).isTrue(); + } else { + assertThat(modifiedAt.asLocalDateTime()).isEqualTo(expectedValues.getModifiedAt()); + } + + if (expectedValues.getModifiedBy() == null) { + assertThat(modifiedBy.isNull()).isTrue(); + } else { + assertThat(modifiedBy.asString()).isEqualTo(expectedValues.getModifiedBy()); + } + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/CallbacksITBase.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/CallbacksITBase.java new file mode 100644 index 000000000..188610047 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/CallbacksITBase.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.shared; + +import static java.util.stream.Collectors.*; +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.Value; +import org.neo4j.driver.Values; +import org.neo4j.driver.types.Node; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Shared information for both imperative and reactive callbacks tests. + * + * @author Michael J. Simons + */ +@ExtendWith(SpringExtension.class) +@ExtendWith(Neo4jExtension.class) +public abstract class CallbacksITBase { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private final Driver driver; + + protected CallbacksITBase(Driver driver) { + this.driver = driver; + } + + @BeforeEach + protected void setupData() { + + try (Transaction transaction = driver.session().beginTransaction()) { + transaction.run("MATCH (n) detach delete n"); + transaction.success(); + } + } + + protected void verifyDatabase(Iterable expectedValues) { + + List ids = StreamSupport.stream(expectedValues.spliterator(), false) + .map(ThingWithAssignedId::getTheId).collect(toList()); + List names = StreamSupport.stream(expectedValues.spliterator(), false) + .map(ThingWithAssignedId::getName).collect(toList()); + try (Session session = driver.session()) { + Record record = session + .run("MATCH (n:Thing) WHERE n.theId in $ids RETURN COLLECT(n) as things", Values.parameters("ids", ids)) + .single(); + + List nodes = record.get("things").asList(Value::asNode); + assertThat(nodes).extracting(n -> n.get("theId").asString()).containsAll(ids); + assertThat(nodes).extracting(n -> n.get("name").asString()) + .containsAll(names); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ImmutableAuditableThing.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ImmutableAuditableThing.java new file mode 100644 index 000000000..f36ef60aa --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ImmutableAuditableThing.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.shared; + +import lombok.AllArgsConstructor; +import lombok.Value; +import lombok.experimental.Wither; + +import java.time.LocalDateTime; + +import org.neo4j.springframework.data.core.schema.Node; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.PersistenceConstructor; + +/** + * @author Michael J. Simons + */ +@Value +@Wither +@AllArgsConstructor(onConstructor = @__(@PersistenceConstructor)) +@Node +public class ImmutableAuditableThing { + + @Id Long id; + @CreatedDate LocalDateTime createdAt; + @CreatedBy String createdBy; + @LastModifiedDate LocalDateTime modifiedAt; + @LastModifiedBy String modifiedBy; + + String name; + + public ImmutableAuditableThing(String name) { + this(null, null, null, null, null, name); + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/AuditingBeforeBindCallbackTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/AuditingBeforeBindCallbackTest.java new file mode 100644 index 000000000..96beef9a9 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/AuditingBeforeBindCallbackTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.context.PersistentEntities; + +/** + * @author Michael J. Simons + */ +class AuditingBeforeBindCallbackTest { + + IsNewAwareAuditingHandler spyOnHandler; + + AuditingBeforeBindCallback callback; + + @BeforeEach + public void setUp() { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(Sample.class, ImmutableSample.class))); + mappingContext.initialize(); + + IsNewAwareAuditingHandler originalHandler = new IsNewAwareAuditingHandler( + new PersistentEntities(Arrays.asList(mappingContext))); + spyOnHandler = spy(originalHandler); + callback = new AuditingBeforeBindCallback(() -> spyOnHandler); + } + + @Test + void rejectsNullAuditingHandler() { + + assertThatIllegalArgumentException().isThrownBy(() -> new AuditingBeforeBindCallback(null)); + } + + @Test + void triggersCreationMarkForObjectWithEmptyId() { + + Sample sample = new Sample(); + sample = (Sample) callback.onBeforeBind(sample); + assertThat(sample.created).isNotNull(); + assertThat(sample.modified).isNotNull(); + + verify(spyOnHandler, times(1)).markCreated(sample); + verify(spyOnHandler, times(0)).markModified(any()); + } + + @Test + void triggersModificationMarkForObjectWithSetId() { + + Sample sample = new Sample(); + sample.id = "id"; + sample.version = 1L; + sample = (Sample) callback.onBeforeBind(sample); + assertThat(sample.created).isNull(); + assertThat(sample.modified).isNotNull(); + + verify(spyOnHandler, times(0)).markCreated(any()); + verify(spyOnHandler, times(1)).markModified(sample); + } + + @Test + void hasExplicitOrder() { + + assertThat(callback).isInstanceOf(Ordered.class); + assertThat(callback.getOrder()).isEqualTo(100); + } + + @Test + void propagatesChangedInstanceToEvent() { + + ImmutableSample sample = new ImmutableSample(); + + ImmutableSample newSample = new ImmutableSample(); + IsNewAwareAuditingHandler handler = mock(IsNewAwareAuditingHandler.class); + doReturn(newSample).when(handler).markAudited(eq(sample)); + + Object result = new AuditingBeforeBindCallback(() -> handler).onBeforeBind(sample); + + assertThat(result).isSameAs(newSample); + } + +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ImmutableSample.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ImmutableSample.java new file mode 100644 index 000000000..e7f588bcd --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ImmutableSample.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import static org.neo4j.springframework.data.core.schema.Id.Strategy.*; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Wither; + +import java.util.Date; + +import org.neo4j.springframework.data.core.schema.Id; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; + +/** + * @author Michael J. Simons + */ +@Value +@Wither +@AllArgsConstructor +@NoArgsConstructor(force = true) +public class ImmutableSample { + + @Id(strategy = ASSIGNED) String id; + @CreatedDate Date created; + @LastModifiedDate Date modified; +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallbackTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallbackTest.java new file mode 100644 index 000000000..bfb106b38 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallbackTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.context.PersistentEntities; + +/** + * @author Michael J. Simons + */ +class ReactiveAuditingBeforeBindCallbackTest { + + IsNewAwareAuditingHandler spyOnHandler; + + ReactiveAuditingBeforeBindCallback callback; + + @BeforeEach + public void setUp() { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(Sample.class, ImmutableSample.class))); + mappingContext.initialize(); + + IsNewAwareAuditingHandler originalHandler = new IsNewAwareAuditingHandler( + new PersistentEntities(Arrays.asList(mappingContext))); + spyOnHandler = spy(originalHandler); + callback = new ReactiveAuditingBeforeBindCallback(() -> spyOnHandler); + } + + @Test + void rejectsNullAuditingHandler() { + + assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveAuditingBeforeBindCallback(null)); + } + + @Test + void triggersCreationMarkForObjectWithEmptyId() { + + Sample sample = new Sample(); + StepVerifier + .create(callback.onBeforeBind(sample)) + .expectNextMatches(s -> { + Sample auditedObject = (Sample) s; + return auditedObject.created != null && auditedObject.modified != null; + }); + + verify(spyOnHandler, times(1)).markCreated(sample); + verify(spyOnHandler, times(0)).markModified(any()); + } + + @Test + void triggersModificationMarkForObjectWithSetId() { + + Sample sample = new Sample(); + sample.id = "id"; + sample.version = 1L; + + StepVerifier + .create(callback.onBeforeBind(sample)) + .expectNextMatches(s -> { + Sample auditedObject = (Sample) s; + return auditedObject.created == null && auditedObject.modified != null; + }); + + verify(spyOnHandler, times(0)).markCreated(any()); + verify(spyOnHandler, times(1)).markModified(sample); + } + + @Test + void hasExplicitOrder() { + + assertThat(callback).isInstanceOf(Ordered.class); + assertThat(callback.getOrder()).isEqualTo(100); + } + + @Test + void propagatesChangedInstanceToEvent() { + + ImmutableSample sample = new ImmutableSample(); + + ImmutableSample newSample = new ImmutableSample(); + IsNewAwareAuditingHandler handler = mock(IsNewAwareAuditingHandler.class); + doReturn(newSample).when(handler).markAudited(eq(sample)); + + ReactiveAuditingBeforeBindCallback localCallback = new ReactiveAuditingBeforeBindCallback(() -> handler); + StepVerifier.create(localCallback.onBeforeBind(sample)).expectNext(newSample); + } + +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/Sample.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/Sample.java new file mode 100644 index 000000000..06e76ccac --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/Sample.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import static org.neo4j.springframework.data.core.schema.Id.Strategy.*; + +import java.util.Date; + +import org.neo4j.springframework.data.core.schema.Id; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Version; + +/** + * @author Michael J. Simons + */ +class Sample { + + @Id(strategy = ASSIGNED) String id; + @Version Long version; + @CreatedDate Date created; + @LastModifiedDate Date modified; +} From 3ad95c29498d0726d28beb1050b9c25e4a8b9b60 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 26 Jun 2019 15:52:59 +0200 Subject: [PATCH 137/342] Add reactive suspend/resume, fix imperative resume. --- .../transaction/Neo4jTransactionManager.java | 3 +++ .../ReactiveNeo4jTransactionManager.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java index b6c7324d8..5786970f7 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java @@ -179,6 +179,9 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager @Override protected void doResume(@Nullable Object transaction, Object suspendedResources) { + Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); + transactionObject.setResourceHolder((Neo4jTransactionHolder) suspendedResources); + TransactionSynchronizationManager.bindResource(driver, suspendedResources); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 8cf9ec5fc..84618de65 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -200,6 +200,24 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction return holder.rollback(); } + @Override + protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) throws TransactionException { + + return Mono + .just(extractNeo4jTransaction(transaction)) + .doOnNext(r -> r.setResourceHolder(null)) + .then(Mono.fromSupplier(() -> synchronizationManager.unbindResource(driver))); + } + + @Override + protected Mono doResume(TransactionSynchronizationManager synchronizationManager, Object transaction, Object suspendedResources) throws TransactionException { + + return Mono + .just(extractNeo4jTransaction(transaction)) + .doOnNext(r -> r.setResourceHolder((ReactiveNeo4jTransactionHolder) suspendedResources)) + .then(Mono.fromRunnable(() -> synchronizationManager.bindResource(driver, suspendedResources))); + } + /* * (non-Javadoc) * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) From c82493092b5124e33818c2a94c86142dcc51049d Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 27 Jun 2019 16:23:15 +0200 Subject: [PATCH 138/342] Remove unused version and dependency. This is the starters responsibility. --- pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pom.xml b/pom.xml index a6e72c6f8..8cbe03afe 100644 --- a/pom.xml +++ b/pom.xml @@ -107,7 +107,6 @@ 2.2.5 1.7.26 2.2.0.RC1 - 1.0.0.BUILD-SNAPSHOT 1.10.7 ${skipTests} @@ -183,11 +182,6 @@ neo4j-java-driver ${neo4j-java-driver.version} - - org.neo4j.driver - neo4j-java-driver-spring-boot-starter - ${neo4j-java-driver-spring-boot-starter.version} - org.neo4j.test neo4j-harness From af3fdd059d3b0b0c36f1db0badf627b817167c73 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 1 Jul 2019 10:15:48 +0200 Subject: [PATCH 139/342] Make sure we use a JDK12 compatible delombok version. --- pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pom.xml b/pom.xml index 8cbe03afe..697eeed7f 100644 --- a/pom.xml +++ b/pom.xml @@ -306,6 +306,18 @@ maven-failsafe-plugin ${maven-failsafe-plugin.version} + + org.projectlombok + lombok-maven-plugin + + + + org.projectlombok + lombok + ${lombok} + + + From 3e5e4901f6a6f1266526ad5f6ca626f69b6f4c11 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 1 Jul 2019 11:12:56 +0200 Subject: [PATCH 140/342] Make sure JavaDoc generation works on JDK11+. Discovered during research whether delombok works. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 697eeed7f..903422f9c 100644 --- a/pom.xml +++ b/pom.xml @@ -91,7 +91,7 @@ 3.0.0-M2 3.0.0-M3 3.0.0-M1 - 3.0.1 + 3.1.0 3.7.1 3.0.1 3.0.0-M2 From 28234112ec0899e4642079c4c0fcea9111c16073 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 1 Jul 2019 11:14:11 +0200 Subject: [PATCH 141/342] Remove unnessary logger. --- .../data/core/transaction/Neo4jTransactionManager.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java index 5786970f7..7f9bc2bfd 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java @@ -20,8 +20,6 @@ package org.neo4j.springframework.data.core.transaction; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; -import lombok.extern.slf4j.Slf4j; - import java.util.Collections; import java.util.List; import java.util.Optional; @@ -50,7 +48,6 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -@Slf4j @API(status = API.Status.STABLE, since = "1.0") public class Neo4jTransactionManager extends AbstractPlatformTransactionManager { From 484d93689125a31ccfbbb555fa93e942c7f8cfac Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 1 Jul 2019 11:55:47 +0200 Subject: [PATCH 142/342] Restructure examples folder. This reassembles the structure the driver and the driver-starter has. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 903422f9c..ab66976dd 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ spring-data-neo4j-rx spring-data-neo4j-rx-spring-boot-starter-parent - spring-data-neo4j-rx-examples-parent + examples From 990cc91cd708cd01fedd411ca8deb4bffe56f5d8 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 1 Jul 2019 16:02:23 +0200 Subject: [PATCH 143/342] Support CREATE after UNWIND. --- .../data/core/cypher/StatementBuilder.java | 2 +- .../data/core/cypher/CypherIT.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java index 441ab3137..c22447e3f 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java @@ -125,7 +125,7 @@ public interface StatementBuilder { * where part has been used or note. * @since 1.0 */ - interface OngoingReading extends ExposesReturning, ExposesWith, ExposesUpdatingClause, ExposesUnwind { + interface OngoingReading extends ExposesReturning, ExposesWith, ExposesUpdatingClause, ExposesUnwind, ExposesCreate { } /** diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java index 89c01e30f..fe8a87f4e 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java @@ -1693,6 +1693,21 @@ class CypherIT { "UNWIND [1, true, false] AS n MERGE (b:`Bike` {b: n}) RETURN b"); } + @Test + void shouldRenderLeadingUnwindWithCreate() { + + Statement statement; + statement = Cypher.unwind(Cypher.literalOf(1), Cypher.literalTrue(), Cypher.literalFalse()) + .as("n") + .create(bikeNode.properties("b", name("n"))) + .returning(bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "UNWIND [1, true, false] AS n CREATE (b:`Bike` {b: n}) RETURN b"); + } + @Test void shouldRenderUnwind() { From 9de55b4b4da650cb32d3b2eb5991bd27264fcd81 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 1 Jul 2019 17:25:04 +0200 Subject: [PATCH 144/342] Fix possible ClassCastException. --- .../core/cypher/DefaultStatementBuilder.java | 43 +++++++++++-------- .../data/core/cypher/StatementBuilder.java | 8 ++-- .../data/core/cypher/CypherIT.java | 26 +++++++++++ 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java index 35b8699f9..cde4561b9 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/DefaultStatementBuilder.java @@ -26,9 +26,11 @@ import java.util.EnumSet; import java.util.List; import java.util.Optional; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingMatchAndUpdate; import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReading; import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingWithWhere; import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingWithoutWhere; +import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingUpdate; import org.neo4j.springframework.data.core.cypher.support.Visitable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -41,9 +43,9 @@ import org.springframework.util.Assert; class DefaultStatementBuilder implements StatementBuilder, OngoingReading, - StatementBuilder.OngoingUpdate, + OngoingUpdate, OngoingReadingWithWhere, - OngoingReadingWithoutWhere { + OngoingReadingWithoutWhere, OngoingMatchAndUpdate { /** * Current list of reading or update clauses to be generated. @@ -107,24 +109,28 @@ class DefaultStatementBuilder return new DefaultUnwindBuilder(expression); } - private OngoingUpdate update(UpdateType updateType, PatternElement... pattern) { + private T update(UpdateType updateType, Object[] pattern) { Assert.notNull(pattern, "Patterns to create are required."); Assert.notEmpty(pattern, "At least one pattern to create is required."); - Assert.isTrue(MERGE_OR_CREATE.contains(updateType), - "Only CREATE and MERGE clauses can be used without a preceding reading clause."); if (this.currentOngoingMatch != null) { this.currentSinglePartElements.add(this.currentOngoingMatch.buildMatch()); } + this.currentOngoingMatch = null; if (this.currentOngoingUpdate != null) { this.currentSinglePartElements.add(this.currentOngoingUpdate.buildUpdatingClause()); } - this.currentOngoingUpdate = new DefaultStatementWithUpdateBuilder(updateType, pattern); - return this; + if (pattern.getClass().getComponentType() == PatternElement.class) { + this.currentOngoingUpdate = new DefaultStatementWithUpdateBuilder(updateType, (PatternElement[]) pattern); + } else if (pattern.getClass().getComponentType() == Expression.class) { + this.currentOngoingUpdate = new DefaultStatementWithUpdateBuilder(updateType, (Expression[]) pattern); + } + + return (T) this; } @Override @@ -170,15 +176,15 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndUpdate delete(Expression... expressions) { + public OngoingUpdate delete(Expression... expressions) { - return new DefaultStatementWithUpdateBuilder(DELETE, expressions); + return update(DELETE, expressions); } @Override - public OngoingMatchAndUpdate detachDelete(Expression... expressions) { + public OngoingUpdate detachDelete(Expression... expressions) { - return new DefaultStatementWithUpdateBuilder(DETACH_DELETE, expressions); + return update(DETACH_DELETE, expressions); } @Override @@ -436,7 +442,7 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndUpdate delete(Expression... expressions) { + public OngoingUpdate delete(Expression... expressions) { return DefaultStatementBuilder.this .addWith(buildWith()) @@ -444,9 +450,9 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndUpdate detachDelete(Expression... expressions) { - return DefaultStatementBuilder.this + public OngoingUpdate detachDelete(Expression... expressions) { + return DefaultStatementBuilder.this .addWith(buildWith()) .detachDelete(expressions); } @@ -654,12 +660,12 @@ class DefaultStatementBuilder } @Override - public OngoingMatchAndUpdate delete(Expression... deletedExpressions) { + public OngoingUpdate delete(Expression... deletedExpressions) { return delete(false, deletedExpressions); } @Override - public OngoingMatchAndUpdate detachDelete(Expression... deletedExpressions) { + public OngoingUpdate detachDelete(Expression... deletedExpressions) { return delete(true, deletedExpressions); } @@ -668,10 +674,9 @@ class DefaultStatementBuilder throw new UnsupportedOperationException("Not supported yet"); } - private OngoingMatchAndUpdate delete(boolean nextDetach, Expression... deletedExpressions) { + private OngoingUpdate delete(boolean nextDetach, Expression... deletedExpressions) { DefaultStatementBuilder.this.addUpdatingClause(buildUpdatingClause()); - return DefaultStatementBuilder.this.new DefaultStatementWithUpdateBuilder( - nextDetach ? DETACH_DELETE : DELETE, deletedExpressions); + return DefaultStatementBuilder.this.update(nextDetach ? DETACH_DELETE : DELETE, deletedExpressions); } @Override diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java index c22447e3f..aa90fc45f 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/StatementBuilder.java @@ -67,7 +67,7 @@ public interface StatementBuilder { * An ongoing update statement that can be used to chain more update statements or add a with or return clause. * @since 1.0 */ - interface OngoingUpdate extends BuildableStatement, ExposesCreate, ExposesMerge, ExposesReturning, ExposesWith { + interface OngoingUpdate extends BuildableStatement, ExposesCreate, ExposesMerge, ExposesDelete, ExposesReturning, ExposesWith { } /** @@ -355,7 +355,7 @@ public interface StatementBuilder { */ interface ExposesDelete { - default OngoingReadingAndWithWithoutWhere delete(String... variables) { + default T delete(String... variables) { return delete(createSymbolicNames(variables)); } @@ -365,7 +365,7 @@ public interface StatementBuilder { * @param expressions The expressions to be deleted. * @return A match with a delete clause that can be build now */ - T delete(Expression... expressions); + T delete(Expression... expressions); default OngoingReadingAndWithWithoutWhere detachDelete(String... variables) { return detachDelete(createSymbolicNames(variables)); @@ -377,7 +377,7 @@ public interface StatementBuilder { * @param expressions The expressions to be deleted. * @return A match with a delete clause that can be build now */ - T detachDelete(Expression... expressions); + T detachDelete(Expression... expressions); } /** diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java index fe8a87f4e..3ffa35254 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/cypher/CypherIT.java @@ -473,6 +473,20 @@ class CypherIT { .isEqualTo("MATCH (b:`Bike`) WITH b OPTIONAL MATCH (u:`User`) OPTIONAL MATCH (:`Trip`) RETURN *"); } + @Test + void usingSameWithStepWithoutReassignThenUpdate() { + StatementBuilder.OngoingReadingAndWith firstStep = match(bikeNode).with(bikeNode); + + firstStep.optionalMatch(userNode); + firstStep.optionalMatch(Cypher.node("Trip")); + firstStep.delete("u"); + + Statement statement = firstStep.returning(Cypher.asterisk()).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) WITH b OPTIONAL MATCH (u:`User`) OPTIONAL MATCH (:`Trip`) DELETE u RETURN *"); + } + @Test void usingSameWithStepWithReassign() { StatementBuilder.ExposesMatch firstStep = match(bikeNode).with(bikeNode); @@ -497,6 +511,18 @@ class CypherIT { assertThat(cypherRenderer.render(statement)) .isEqualTo("MATCH (b:`Bike`) OPTIONAL MATCH (u:`User`), (o:`U`) WHERE u.a IS NULL RETURN b"); } + + @Test + void optionalMatchThenDelete() { + Statement statement = Cypher + .match(bikeNode) + .optionalMatch(userNode, Cypher.node("U").named("o")) + .delete(userNode, bikeNode) + .build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo("MATCH (b:`Bike`) OPTIONAL MATCH (u:`User`), (o:`U`) DELETE u, b"); + } } @Nested From d531b98693808c59053eed1803090fe9e07daa31 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 4 Jul 2019 11:16:56 +0200 Subject: [PATCH 145/342] Remove usage of Project Lombok in main. --- spring-data-neo4j/pom.xml | 8 +++ .../data/core/DefaultNeo4jClient.java | 30 +++++++---- .../data/core/DefaultReactiveNeo4jClient.java | 52 +++++++++++++----- .../data/core/SingleValueMappingFunction.java | 7 +-- .../data/core/cypher/Node.java | 11 ++-- .../data/core/cypher/NodeLabel.java | 10 ++-- .../data/core/cypher/SymbolicName.java | 10 ++-- .../cypher/support/ReflectiveVisitor.java | 22 ++++++-- .../mapping/DefaultNeo4jBinderFunction.java | 11 ++-- .../mapping/DefaultNeo4jIsNewStrategy.java | 16 +++--- .../mapping/DefaultNeo4jMappingFunction.java | 16 +++--- .../DefaultRelationshipDescription.java | 54 ++++++++++++++++--- .../repository/query/CypherQueryCreator.java | 29 ++++++---- .../repository/query/Neo4jQueryExecution.java | 12 +++-- .../query/Neo4jQueryLookupStrategy.java | 11 ++-- .../repository/query/PartTreeNeo4jQuery.java | 5 +- .../ReactiveNeo4jQueryLookupStrategy.java | 12 +++-- .../query/ReactivePartTreeNeo4jQuery.java | 6 +-- .../data/repository/support/Predicate.java | 18 ++++--- .../support/SimpleNeo4jRepository.java | 7 +-- .../support/SimpleQueryByExampleExecutor.java | 26 +++++---- .../SimpleReactiveNeo4jRepository.java | 6 ++- .../SimpleReactiveQueryByExampleExecutor.java | 21 +++++--- 23 files changed, 276 insertions(+), 124 deletions(-) diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index c437539dd..c66fa68d3 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -147,6 +147,14 @@ neo4j test + + + org.projectlombok + lombok + ${lombok} + true + test + diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java index 1b62b6619..72d964b32 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java @@ -22,10 +22,6 @@ import static java.util.stream.Collectors.*; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager.*; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.RequiredArgsConstructor; - import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationHandler; @@ -48,8 +44,8 @@ import org.neo4j.driver.StatementRunner; import org.neo4j.driver.exceptions.NoSuchRecordException; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; -import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.neo4j.springframework.data.repository.NoResultException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -206,12 +202,15 @@ class DefaultNeo4jClient implements Neo4jClient { return this; } - @RequiredArgsConstructor class DefaultOngoingBindSpec implements OngoingBindSpec { @Nullable private final T value; + DefaultOngoingBindSpec(@Nullable T value) { + this.value = value; + } + @Override public RunnableSpecTightToDatabase to(String name) { @@ -326,14 +325,21 @@ class DefaultNeo4jClient implements Neo4jClient { } } - @AllArgsConstructor - @RequiredArgsConstructor class DefaultRunnableDelegation implements RunnableDelegation, OngoingDelegation { private final Function> callback; private String targetDatabase; + DefaultRunnableDelegation(Function> callback) { + this(callback, null); + } + + DefaultRunnableDelegation(Function> callback, @Nullable String targetDatabase) { + this.callback = callback; + this.targetDatabase = targetDatabase; + } + @Override public RunnableDelegation in(@SuppressWarnings("HiddenField") String targetDatabase) { this.targetDatabase = targetDatabase; @@ -348,12 +354,17 @@ class DefaultNeo4jClient implements Neo4jClient { } } - @RequiredArgsConstructor(access = AccessLevel.PACKAGE) final class DefaultExecutableQuery implements ExecutableQuery { private final PreparedQuery preparedQuery; private final Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec; + DefaultExecutableQuery(PreparedQuery preparedQuery, + RecordFetchSpec, Collection, T> fetchSpec) { + this.preparedQuery = preparedQuery; + this.fetchSpec = fetchSpec; + } + public List getResults() { return fetchSpec.all().stream().collect(toList()); } @@ -373,5 +384,4 @@ class DefaultNeo4jClient implements Neo4jClient { .orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery())); } } - } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java index 1e30d9dfb..02b58f911 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java @@ -21,10 +21,6 @@ package org.neo4j.springframework.data.core; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; import static org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -41,11 +37,11 @@ import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; -import org.reactivestreams.Publisher; -import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.neo4j.springframework.data.core.Neo4jClient.MappingSpec; import org.neo4j.springframework.data.core.Neo4jClient.OngoingBindSpec; import org.neo4j.springframework.data.core.Neo4jClient.RecordFetchSpec; +import org.reactivestreams.Publisher; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -57,7 +53,6 @@ import org.springframework.util.Assert; * @soundtrack Die Toten Hosen - Im Auftrag des Herrn * @since 1.0 */ -@Slf4j class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { private final Driver driver; @@ -129,7 +124,6 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return new DefaultReactiveExecutableQuery<>(fetchSpec); } - @RequiredArgsConstructor class DefaultReactiveRunnableSpec implements ReactiveRunnableSpec { private final Supplier cypherSupplier; @@ -138,18 +132,25 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { private final NamedParameters parameters = new NamedParameters(); + DefaultReactiveRunnableSpec(Supplier cypherSupplier) { + this.cypherSupplier = cypherSupplier; + } + @Override public ReactiveRunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) { this.targetDatabase = targetDatabase; return this; } - @RequiredArgsConstructor class DefaultOngoingBindSpec implements OngoingBindSpec { @Nullable private final T value; + DefaultOngoingBindSpec(@Nullable T value) { + this.value = value; + } + @Override public ReactiveRunnableSpecTightToDatabase to(String name) { @@ -201,8 +202,6 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } } - @AllArgsConstructor - @RequiredArgsConstructor class DefaultReactiveRecordFetchSpec implements RecordFetchSpec, Flux, T>, MappingSpec, Flux, T> { @@ -214,6 +213,20 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { private BiFunction mappingFunction; + DefaultReactiveRecordFetchSpec(String targetDatabase, Supplier cypherSupplier, + NamedParameters parameters) { + this(targetDatabase, cypherSupplier, parameters, null); + } + + DefaultReactiveRecordFetchSpec( + String targetDatabase, Supplier cypherSupplier, NamedParameters parameters, + @Nullable BiFunction mappingFunction) { + this.targetDatabase = targetDatabase; + this.cypherSupplier = cypherSupplier; + this.parameters = parameters; + this.mappingFunction = mappingFunction; + } + @Override public RecordFetchSpec, Flux, T> mappedBy(BiFunction mappingFunction) { @@ -271,14 +284,22 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } } - @AllArgsConstructor - @RequiredArgsConstructor class DefaultReactiveRunnableDelegation implements ReactiveRunnableDelegation, OngoingReactiveDelegation { private final Function> callback; private String targetDatabase; + DefaultReactiveRunnableDelegation(Function> callback) { + this(callback, null); + } + + DefaultReactiveRunnableDelegation(Function> callback, + @Nullable String targetDatabase) { + this.callback = callback; + this.targetDatabase = targetDatabase; + } + @Override public ReactiveRunnableDelegation in(@SuppressWarnings("HiddenField") String targetDatabase) { this.targetDatabase = targetDatabase; @@ -296,11 +317,14 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } } - @RequiredArgsConstructor(access = AccessLevel.PACKAGE) final class DefaultReactiveExecutableQuery implements ExecutableQuery { private final Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec; + DefaultReactiveExecutableQuery(RecordFetchSpec, Flux, T> fetchSpec) { + this.fetchSpec = fetchSpec; + } + /** * @return All results returned by this query. */ diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java index c61f4ae7b..730c1df28 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/SingleValueMappingFunction.java @@ -18,8 +18,6 @@ */ package org.neo4j.springframework.data.core; -import lombok.RequiredArgsConstructor; - import java.util.function.BiFunction; import org.neo4j.driver.Record; @@ -34,11 +32,14 @@ import org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes; * @param type of the domain class to map * @since 1.0 */ -@RequiredArgsConstructor final class SingleValueMappingFunction implements BiFunction { private final Class targetClass; + SingleValueMappingFunction(Class targetClass) { + this.targetClass = targetClass; + } + @Override public T apply(TypeSystem typeSystem, Record record) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java index b5d863444..dc77c4beb 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java @@ -20,8 +20,6 @@ package org.neo4j.springframework.data.core.cypher; import static java.util.stream.Collectors.*; -import lombok.ToString; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -41,7 +39,6 @@ import org.springframework.util.Assert; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -@ToString(of = { "symbolicName", "labels" }) public final class Node implements PatternElement, Named, Expression, ExposesRelationships { static Node create(String primaryLabel, String... additionalLabels) { @@ -179,4 +176,12 @@ public final class Node implements PatternElement, Named, Expression, ExposesRel Visitable.visitIfNotNull(this.properties, visitor); visitor.leave(this); } + + @Override + public String toString() { + return "Node{" + + "symbolicName=" + symbolicName + + ", labels=" + labels + + '}'; + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabel.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabel.java index b5fe90c4c..30b03e72d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabel.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/NodeLabel.java @@ -18,8 +18,6 @@ */ package org.neo4j.springframework.data.core.cypher; -import lombok.ToString; - import org.apiguardian.api.API; import org.neo4j.springframework.data.core.cypher.support.Visitable; @@ -31,7 +29,6 @@ import org.neo4j.springframework.data.core.cypher.support.Visitable; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -@ToString(of = { "value" }) public final class NodeLabel implements Visitable { private final String value; @@ -43,4 +40,11 @@ public final class NodeLabel implements Visitable { public String getValue() { return value; } + + @Override + public String toString() { + return "NodeLabel{" + + "value='" + value + '\'' + + '}'; + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java index 69466d6f1..5c233a14e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java @@ -18,8 +18,6 @@ */ package org.neo4j.springframework.data.core.cypher; -import lombok.ToString; - import org.apiguardian.api.API; /** @@ -32,7 +30,6 @@ import org.apiguardian.api.API; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -@ToString(of = { "name" }) public final class SymbolicName implements Expression { private final String name; @@ -44,4 +41,11 @@ public final class SymbolicName implements Expression { public String getName() { return name; } + + @Override + public String toString() { + return "SymbolicName{" + + "name='" + name + '\'' + + '}'; + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java index c3869f148..5d7433ac6 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java @@ -18,8 +18,6 @@ */ package org.neo4j.springframework.data.core.cypher.support; -import lombok.EqualsAndHashCode; - import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; @@ -28,6 +26,7 @@ import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -137,7 +136,6 @@ public abstract class ReflectiveVisitor implements Visitor { return Optional.empty(); } - @EqualsAndHashCode private static class TargetAndPhase { private final Class visitorClass; @@ -157,5 +155,23 @@ public abstract class ReflectiveVisitor implements Visitor { classOfVisitable = classOfVisitable.getSuperclass(); } while (classOfVisitable != null); } + + @Override public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TargetAndPhase)) { + return false; + } + TargetAndPhase that = (TargetAndPhase) o; + return visitorClass.equals(that.visitorClass) && + classHierarchyOfVisitable.equals(that.classHierarchyOfVisitable) && + phase == that.phase; + } + + @Override + public int hashCode() { + return Objects.hash(visitorClass, classHierarchyOfVisitable, phase); + } } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java index 61eb8573e..88a77a4ba 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java @@ -20,9 +20,6 @@ package org.neo4j.springframework.data.core.mapping; import static org.neo4j.springframework.data.core.schema.NodeDescription.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.HashMap; import java.util.Map; import java.util.function.Function; @@ -30,16 +27,18 @@ import java.util.function.Function; import org.springframework.data.mapping.PersistentPropertyAccessor; /** - * @param type that should get mapped by the binder function * @author Michael J. Simons + * @param type that should get mapped by the binder function * @since 1.0 */ -@RequiredArgsConstructor -@Slf4j final class DefaultNeo4jBinderFunction implements Function> { private final Neo4jPersistentEntity nodeDescription; + DefaultNeo4jBinderFunction(Neo4jPersistentEntity nodeDescription) { + this.nodeDescription = nodeDescription; + } + @Override public Map apply(T entity) { Map properties = new HashMap<>(); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java index ed07ecd3d..6bfceb47e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java @@ -18,13 +18,11 @@ */ package org.neo4j.springframework.data.core.mapping; -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.function.Function; import org.neo4j.springframework.data.core.schema.Id; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.support.IsNewStrategy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -48,10 +46,10 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -@Slf4j class DefaultNeo4jIsNewStrategy implements IsNewStrategy { + private static final Logger log = LoggerFactory.getLogger(DefaultNeo4jIsNewStrategy.class); + static IsNewStrategy basedOn(Neo4jPersistentEntity entityMetaData) { Assert.notNull(entityMetaData, "Entity meta data must not be null."); @@ -89,6 +87,12 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy { private @Nullable final Function valueLookup; + private DefaultNeo4jIsNewStrategy(Id.Strategy strategy, Class valueType, Function valueLookup) { + this.strategy = strategy; + this.valueType = valueType; + this.valueLookup = valueLookup; + } + /* * (non-Javadoc) * @see IsNewStrategy#isNew(Object) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java index 260546414..d4b17e69a 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java @@ -20,9 +20,6 @@ package org.neo4j.springframework.data.core.mapping; import static java.util.stream.Collectors.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.List; import java.util.Optional; import java.util.function.BiFunction; @@ -33,12 +30,14 @@ import org.neo4j.driver.Value; import org.neo4j.driver.types.Entity; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.TypeSystem; +import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.model.ParameterValueProvider; -import org.neo4j.springframework.data.core.schema.NodeDescription; /** * The central logic of mapping Neo4j's {@link org.neo4j.driver.Record records} to entities based on the Spring @@ -49,14 +48,19 @@ import org.neo4j.springframework.data.core.schema.NodeDescription; * @author Michael J. Simons * @since 1.0 */ -@RequiredArgsConstructor -@Slf4j final class DefaultNeo4jMappingFunction implements BiFunction { + private static final Logger log = LoggerFactory.getLogger(DefaultNeo4jMappingFunction.class); + private final EntityInstantiators instantiators; private final Neo4jPersistentEntity nodeDescription; + DefaultNeo4jMappingFunction(EntityInstantiators instantiators, Neo4jPersistentEntity nodeDescription) { + this.instantiators = instantiators; + this.nodeDescription = nodeDescription; + } + @Override public T apply(TypeSystem typeSystem, Record record) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java index c84964d40..cb19430f7 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java @@ -18,10 +18,7 @@ */ package org.neo4j.springframework.data.core.mapping; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.ToString; +import java.util.Objects; import org.neo4j.springframework.data.core.schema.RelationshipDescription; @@ -29,10 +26,6 @@ import org.neo4j.springframework.data.core.schema.RelationshipDescription; * @author Michael J. Simons * @since 1.0 */ -@RequiredArgsConstructor -@Getter -@ToString -@EqualsAndHashCode(of = { "type", "target" }) class DefaultRelationshipDescription implements RelationshipDescription { private final String type; @@ -44,4 +37,49 @@ class DefaultRelationshipDescription implements RelationshipDescription { * from the map key. */ private final boolean dynamic; + + DefaultRelationshipDescription(String type, String target, boolean dynamic) { + this.type = type; + this.target = target; + this.dynamic = dynamic; + } + + public String getType() { + return type; + } + + public String getTarget() { + return target; + } + + public boolean isDynamic() { + return dynamic; + } + + @Override + public String toString() { + return "DefaultRelationshipDescription{" + + "type='" + type + '\'' + + ", target='" + target + '\'' + + ", dynamic=" + dynamic + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DefaultRelationshipDescription)) { + return false; + } + DefaultRelationshipDescription that = (DefaultRelationshipDescription) o; + return type.equals(that.type) && + target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(type, target); + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java index cbc7d3e6f..accb1e30a 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java @@ -18,16 +18,12 @@ */ package org.neo4j.springframework.data.repository.query; -import static lombok.AccessLevel.*; import static org.neo4j.springframework.data.core.cypher.Cypher.*; import static org.neo4j.springframework.data.core.cypher.Functions.*; import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; import static org.neo4j.springframework.data.repository.query.PartTreeNeo4jQuery.*; -import lombok.RequiredArgsConstructor; -import lombok.ToString; - import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; @@ -37,11 +33,6 @@ import java.util.Queue; import java.util.stream.Stream; import org.neo4j.driver.types.Point; -import org.springframework.data.domain.Range; -import org.springframework.data.domain.Sort; -import org.springframework.data.geo.Circle; -import org.springframework.data.geo.Distance; -import org.springframework.data.mapping.PersistentPropertyPath; import org.neo4j.springframework.data.core.cypher.Condition; import org.neo4j.springframework.data.core.cypher.Conditions; import org.neo4j.springframework.data.core.cypher.Cypher; @@ -55,6 +46,11 @@ import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty; import org.neo4j.springframework.data.core.schema.NodeDescription; import org.neo4j.springframework.data.repository.query.Neo4jQueryMethod.Neo4jParameter; import org.neo4j.springframework.data.repository.query.Neo4jQueryMethod.Neo4jParameters; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.parser.AbstractQueryCreator; @@ -396,16 +392,27 @@ final class CypherQueryCreator extends AbstractQueryCreator { } } - @RequiredArgsConstructor(access = PACKAGE) - @ToString static class Parameter { final String nameOrIndex; final Object value; + Parameter(String nameOrIndex, Object value) { + this.nameOrIndex = nameOrIndex; + this.value = value; + } + boolean hasValueOfType(Class type) { return type.isInstance(value); } + + @Override + public String toString() { + return "Parameter{" + + "nameOrIndex='" + nameOrIndex + '\'' + + ", value=" + value + + '}'; + } } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryExecution.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryExecution.java index fd360ce7a..e2c6eb07b 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryExecution.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryExecution.java @@ -18,8 +18,6 @@ */ package org.neo4j.springframework.data.repository.query; -import lombok.RequiredArgsConstructor; - import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.PreparedQuery; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; @@ -38,11 +36,14 @@ interface Neo4jQueryExecution { Object execute(PreparedQuery description, boolean asCollectionQuery); - @RequiredArgsConstructor class DefaultQueryExecution implements Neo4jQueryExecution { private final Neo4jClient neo4jClient; + DefaultQueryExecution(Neo4jClient neo4jClient) { + this.neo4jClient = neo4jClient; + } + @Override public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { @@ -55,11 +56,14 @@ interface Neo4jQueryExecution { } } - @RequiredArgsConstructor class ReactiveQueryExecution implements Neo4jQueryExecution { private final ReactiveNeo4jClient neo4jClient; + ReactiveQueryExecution(ReactiveNeo4jClient neo4jClient) { + this.neo4jClient = neo4jClient; + } + @Override public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryLookupStrategy.java index 293f4fe5b..d566758f1 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryLookupStrategy.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQueryLookupStrategy.java @@ -18,8 +18,6 @@ */ package org.neo4j.springframework.data.repository.query; -import lombok.RequiredArgsConstructor; - import java.lang.reflect.Method; import org.apiguardian.api.API; @@ -40,13 +38,20 @@ import org.springframework.data.repository.query.RepositoryQuery; * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -@RequiredArgsConstructor public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy { private final Neo4jClient neo4jClient; private final Neo4jMappingContext mappingContext; private final QueryMethodEvaluationContextProvider evaluationContextProvider; + public Neo4jQueryLookupStrategy(Neo4jClient neo4jClient, + Neo4jMappingContext mappingContext, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + this.neo4jClient = neo4jClient; + this.mappingContext = mappingContext; + this.evaluationContextProvider = evaluationContextProvider; + } + /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) */ diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java index b67ed514e..b8e2fb18e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java @@ -20,8 +20,6 @@ package org.neo4j.springframework.data.repository.query; import static java.util.stream.Collectors.*; -import lombok.extern.slf4j.Slf4j; - import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetTime; @@ -36,9 +34,9 @@ import java.util.Set; import org.neo4j.driver.types.Point; import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.PreparedQuery; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.repository.query.Neo4jQueryMethod.Neo4jParameters; -import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; @@ -55,7 +53,6 @@ import org.springframework.util.Assert; * @author Michael J. Simons * @since 1.0 */ -@Slf4j final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveNeo4jQueryLookupStrategy.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveNeo4jQueryLookupStrategy.java index 090e32934..d3d512bc2 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveNeo4jQueryLookupStrategy.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveNeo4jQueryLookupStrategy.java @@ -18,8 +18,6 @@ */ package org.neo4j.springframework.data.repository.query; -import lombok.RequiredArgsConstructor; - import java.lang.reflect.Method; import org.apiguardian.api.API; @@ -36,16 +34,24 @@ import org.springframework.data.repository.query.RepositoryQuery; * Lookup strategy for queries. This is the internal api of the {@code query package}. * * @author Gerrit Meier + * @author Michael J. Simons * @since 1.0 */ @API(status = API.Status.INTERNAL, since = "1.0") -@RequiredArgsConstructor public final class ReactiveNeo4jQueryLookupStrategy implements QueryLookupStrategy { private final ReactiveNeo4jClient neo4jClient; private final Neo4jMappingContext mappingContext; private final QueryMethodEvaluationContextProvider evaluationContextProvider; + public ReactiveNeo4jQueryLookupStrategy(ReactiveNeo4jClient neo4jClient, + Neo4jMappingContext mappingContext, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + this.neo4jClient = neo4jClient; + this.mappingContext = mappingContext; + this.evaluationContextProvider = evaluationContextProvider; + } + /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) */ diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java index 7d3b4a833..33c7ccc89 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java @@ -20,8 +20,6 @@ package org.neo4j.springframework.data.repository.query; import static java.util.stream.Collectors.*; -import lombok.extern.slf4j.Slf4j; - import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetTime; @@ -35,10 +33,10 @@ import java.util.Map; import java.util.Set; import org.neo4j.driver.types.Point; +import org.neo4j.springframework.data.core.PreparedQuery; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.repository.query.Neo4jQueryMethod.Neo4jParameters; -import org.neo4j.springframework.data.core.PreparedQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; @@ -52,9 +50,9 @@ import org.springframework.util.Assert; * Implementation of {@link RepositoryQuery} for derived finder methods. * * @author Gerrit Meier + * @author Michael J. Simons * @since 1.0 */ -@Slf4j final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java index 4f099db76..da4666c18 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java @@ -18,14 +18,10 @@ */ package org.neo4j.springframework.data.repository.support; -import static lombok.AccessLevel.*; import static org.neo4j.springframework.data.core.cypher.Cypher.*; import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -33,8 +29,6 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.ExampleMatcher; import org.neo4j.springframework.data.core.cypher.Condition; import org.neo4j.springframework.data.core.cypher.Conditions; import org.neo4j.springframework.data.core.cypher.Cypher; @@ -45,6 +39,10 @@ import org.neo4j.springframework.data.core.cypher.SymbolicName; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; @@ -57,10 +55,10 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; * @author Michael J. Simons * @since 1.0 */ -@Slf4j -@RequiredArgsConstructor(access = PRIVATE) final class Predicate { + private static final Logger log = LoggerFactory.getLogger(Predicate.class); + static Predicate create(Neo4jMappingContext mappingContext, Example example) { NodeDescription probeNodeDescription = mappingContext.getRequiredNodeDescription(example.getProbeType()); @@ -151,6 +149,10 @@ final class Predicate { private final Map parameters = new HashMap<>(); + private Predicate(NodeDescription nodeDescription) { + this.nodeDescription = nodeDescription; + } + StatementBuilder.OngoingReadingAndWith useWithReadingFragment( BiFunction, Optional, StatementBuilder.OngoingReadingAndWith> readingFragmentSupplier) { return readingFragmentSupplier.apply(this.nodeDescription, Optional.of(this.condition)); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java index 8a72536e4..9710c4a9b 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java @@ -24,8 +24,6 @@ import static org.neo4j.springframework.data.core.cypher.Cypher.*; import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; -import lombok.extern.slf4j.Slf4j; - import java.util.Collections; import java.util.List; import java.util.Map; @@ -45,6 +43,8 @@ import org.neo4j.springframework.data.core.cypher.StatementBuilder; import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingAndReturn; import org.neo4j.springframework.data.core.cypher.renderer.Renderer; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -63,9 +63,10 @@ import org.springframework.transaction.annotation.Transactional; */ @Repository @Transactional(readOnly = true) -@Slf4j class SimpleNeo4jRepository implements PagingAndSortingRepository { + private static final Logger log = LoggerFactory.getLogger(SimpleNeo4jRepository.class); + private static final Renderer renderer = Renderer.getDefaultRenderer(); private final Neo4jClient neo4jClient; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java index 31a590c64..f1ced3d11 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java @@ -18,13 +18,9 @@ */ package org.neo4j.springframework.data.repository.support; -import static lombok.AccessLevel.*; import static org.neo4j.springframework.data.core.cypher.Cypher.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.List; import java.util.Map; import java.util.Optional; @@ -33,10 +29,6 @@ import java.util.function.LongSupplier; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.Neo4jClient.ExecutableQuery; import org.neo4j.springframework.data.core.PreparedQuery; @@ -46,6 +38,12 @@ import org.neo4j.springframework.data.core.cypher.StatementBuilder; import org.neo4j.springframework.data.core.cypher.StatementBuilder.BuildableStatement; import org.neo4j.springframework.data.core.cypher.renderer.Renderer; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.repository.support.PageableExecutionUtils; @@ -56,10 +54,10 @@ import org.springframework.data.repository.support.PageableExecutionUtils; * @param type of the domain class * @since 1.0 */ -@Slf4j -@RequiredArgsConstructor(access = PACKAGE) class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { + private static final Logger log = LoggerFactory.getLogger(SimpleQueryByExampleExecutor.class); + private static final Renderer renderer = Renderer.getDefaultRenderer(); private final Neo4jClient neo4jClient; @@ -68,6 +66,14 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { private final SchemaBasedStatementBuilder statementBuilder; + SimpleQueryByExampleExecutor(Neo4jClient neo4jClient, + Neo4jMappingContext mappingContext, + SchemaBasedStatementBuilder statementBuilder) { + this.neo4jClient = neo4jClient; + this.mappingContext = mappingContext; + this.statementBuilder = statementBuilder; + } + @Override public Optional findOne(Example example) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java index 3e50a3863..01c458371 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java @@ -24,7 +24,6 @@ import static org.neo4j.springframework.data.core.cypher.Cypher.*; import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -45,6 +44,8 @@ import org.neo4j.springframework.data.core.cypher.Statement; import org.neo4j.springframework.data.core.cypher.renderer.Renderer; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.repository.reactive.ReactiveSortingRepository; @@ -61,9 +62,10 @@ import org.springframework.util.Assert; */ @Repository @Transactional(readOnly = true) -@Slf4j class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository { + private static final Logger log = LoggerFactory.getLogger(SimpleReactiveNeo4jRepository.class); + private static final Renderer renderer = Renderer.getDefaultRenderer(); private final ReactiveNeo4jClient neo4jClient; diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java index 4dbc5e3dd..4967b78c8 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java @@ -18,12 +18,9 @@ */ package org.neo4j.springframework.data.repository.support; -import static lombok.AccessLevel.*; import static org.neo4j.springframework.data.core.cypher.Cypher.*; import static org.neo4j.springframework.data.repository.query.CypherAdapterUtils.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,8 +29,6 @@ import java.util.function.BiFunction; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.Sort; import org.neo4j.springframework.data.core.PreparedQuery; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; import org.neo4j.springframework.data.core.ReactiveNeo4jClient.ExecutableQuery; @@ -41,6 +36,10 @@ import org.neo4j.springframework.data.core.cypher.Functions; import org.neo4j.springframework.data.core.cypher.Statement; import org.neo4j.springframework.data.core.cypher.renderer.Renderer; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; /** @@ -51,10 +50,11 @@ import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; * @param type of the domain class * @since 1.0 */ -@Slf4j -@RequiredArgsConstructor(access = PACKAGE) class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleExecutor { + private static final Logger log = LoggerFactory + .getLogger(SimpleReactiveQueryByExampleExecutor.class); + private static final Renderer renderer = Renderer.getDefaultRenderer(); private final ReactiveNeo4jClient neo4jClient; @@ -63,6 +63,13 @@ class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleE private final SchemaBasedStatementBuilder statementBuilder; + SimpleReactiveQueryByExampleExecutor(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext mappingContext, + SchemaBasedStatementBuilder statementBuilder) { + this.neo4jClient = neo4jClient; + this.mappingContext = mappingContext; + this.statementBuilder = statementBuilder; + } + @Override public Mono findOne(Example example) { From dcffeb4f781ac9786873c1d45ecb69de3eb75dce Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 9 Jul 2019 10:45:11 +0200 Subject: [PATCH 146/342] Update Neo4j driver to 2.0.0-alpha03. --- pom.xml | 2 +- .../data/core/DefaultNeo4jClient.java | 2 +- .../data/core/DefaultReactiveNeo4jClient.java | 2 +- .../transaction/Neo4jTransactionManager.java | 10 ++--- .../transaction/Neo4jTransactionUtils.java | 26 ++++++++----- .../ReactiveNeo4jTransactionManager.java | 9 ++--- .../data/core/Neo4jClientTest.java | 27 ++++--------- .../data/core/ReactiveNeo4jClientTest.java | 28 +++++--------- .../data/core/TransactionHandlingTest.java | 38 +++++++------------ .../Neo4jTransactionManagerTest.java | 16 ++++---- .../ReactiveNeo4jTransactionManagerTest.java | 13 +++---- 11 files changed, 72 insertions(+), 101 deletions(-) diff --git a/pom.xml b/pom.xml index ab66976dd..4da593feb 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ ${java.version} ${java.version} 2.23.4 - 2.0.0-alpha02 + 2.0.0-alpha03 3.5.2 UTF-8 1.2.1 diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java index 72d964b32..9518de531 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java @@ -72,7 +72,7 @@ class DefaultNeo4jClient implements Neo4jClient { StatementRunner statementRunner = retrieveTransaction(driver, targetDatabase) .map(StatementRunner.class::cast) - .orElseGet(() -> driver.session(defaultSessionParameters(targetDatabase))); + .orElseGet(() -> driver.session(defaultSessionConfig(targetDatabase))); return (AutoCloseableStatementRunner) Proxy.newProxyInstance(StatementRunner.class.getClassLoader(), new Class[] { AutoCloseableStatementRunner.class }, diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java index 02b58f911..bf85b11c7 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java @@ -69,7 +69,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return retrieveReactiveTransaction(driver, targetDatabase) .map(rxTransaction -> new RxStatementRunnerHolder(rxTransaction, Mono.empty(), Mono.empty())) // .switchIfEmpty( - Mono.using(() -> driver.rxSession(defaultSessionParameters(targetDatabase)), + Mono.using(() -> driver.rxSession(defaultSessionConfig(targetDatabase)), session -> Mono.from(session.beginTransaction()) .map(tx -> new RxStatementRunnerHolder(tx, tx.commit(), tx.rollback())), RxSession::close) ); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java index 7f9bc2bfd..9d4567fe7 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Optional; import org.apiguardian.api.API; -import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; @@ -96,7 +95,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager } // Otherwise we open a session and synchronize it. - Session session = driver.session(defaultSessionParameters(targetDatabase)); + Session session = driver.session(defaultSessionConfig(targetDatabase)); Transaction transaction = session.beginTransaction(TransactionConfig.empty()); // Manually create a new synchronization connectionHolder = new Neo4jTransactionHolder(targetDatabase, session, transaction); @@ -144,14 +143,13 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager TransactionConfig transactionConfig = createTransactionConfigFrom(definition); boolean readOnly = definition.isReadOnly(); - AccessMode accessMode = readOnly ? AccessMode.READ : AccessMode.WRITE; - List bookmarks = Collections.emptyList(); // TODO Bookmarksupport + TransactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); try { - Session session = this.driver - .session(t -> t.withDefaultAccessMode(accessMode).withBookmarks(bookmarks).withDatabase(databaseName)); + List bookmarks = Collections.emptyList(); // TODO Bookmarksupport; + Session session = this.driver.session(sessionConfig(readOnly, bookmarks, databaseName)); Transaction nativeTransaction = session.beginTransaction(transactionConfig); Neo4jTransactionHolder transactionHolder = diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java index c99ecc7a4..8fa0bfe71 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java @@ -20,11 +20,11 @@ package org.neo4j.springframework.data.core.transaction; import java.time.Duration; import java.util.Collections; -import java.util.function.Consumer; +import java.util.List; import org.neo4j.driver.AccessMode; -import org.neo4j.driver.SessionParametersTemplate; import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.internal.SessionConfig; import org.springframework.lang.Nullable; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.InvalidIsolationLevelException; @@ -43,15 +43,21 @@ public final class Neo4jTransactionUtils { * @param databaseName The database to use. May be null, which then designates the default database. * @return Session parameters to configure the default session used */ - public static Consumer defaultSessionParameters(@Nullable String databaseName) { - return (SessionParametersTemplate t) -> { - t.withDefaultAccessMode(AccessMode.WRITE) - .withBookmarks(Collections.EMPTY_LIST); + public static SessionConfig defaultSessionConfig(@Nullable String databaseName) { + return sessionConfig(false, Collections.emptyList(), databaseName); + } - if (databaseName != null) { - t.withDatabase(databaseName); - } - }; + public static SessionConfig sessionConfig(boolean readOnly, List bookmarks, + @Nullable String databaseName) { + SessionConfig.Builder builder = SessionConfig.builder() + .withDefaultAccessMode(readOnly ? AccessMode.READ : AccessMode.WRITE) + .withBookmarks(bookmarks); + + if (databaseName != null) { + builder.withDatabase(databaseName); + } + + return builder.build(); } /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 84618de65..83b4651c4 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -26,7 +26,6 @@ import java.util.Collections; import java.util.List; import org.apiguardian.api.API; -import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.reactive.RxSession; @@ -85,7 +84,7 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction // Otherwise open up a new native transaction return Mono.defer(() -> { - RxSession session = driver.rxSession(defaultSessionParameters(targetDatabase)); + RxSession session = driver.rxSession(defaultSessionConfig(targetDatabase)); return Mono.from(session.beginTransaction(TransactionConfig.empty())).map(tx -> { ReactiveNeo4jTransactionHolder newConnectionHolder = new ReactiveNeo4jTransactionHolder( @@ -147,13 +146,11 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction TransactionConfig transactionConfig = createTransactionConfigFrom(transactionDefinition); boolean readOnly = transactionDefinition.isReadOnly(); - AccessMode accessMode = readOnly ? AccessMode.READ : AccessMode.WRITE; - List bookmarks = Collections.emptyList(); // TODO Bookmarksupport transactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly); - RxSession session = this.driver - .rxSession(t -> t.withDefaultAccessMode(accessMode).withBookmarks(bookmarks).withDatabase(databaseName)); + List bookmarks = Collections.emptyList(); // TODO Bookmarksupport; + RxSession session = this.driver.rxSession(sessionConfig(readOnly, bookmarks, databaseName)); return Mono.from(session.beginTransaction(transactionConfig)).doOnNext(nativeTransaction -> { diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java index 9c88e464f..16ed606d6 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java @@ -32,7 +32,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; @@ -46,13 +45,12 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; import org.neo4j.driver.Values; -import org.neo4j.driver.internal.SessionParameters; +import org.neo4j.driver.internal.SessionConfig; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; @@ -65,10 +63,7 @@ class Neo4jClientTest { @Mock private Driver driver; - private ArgumentCaptor sessionTemplateCaptor = ArgumentCaptor.forClass(Consumer.class); - - @Mock - private SessionParameters.Template sessionParametersTemplate; + private ArgumentCaptor configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class); @Mock private Session session; @@ -91,11 +86,7 @@ class Neo4jClientTest { @BeforeEach void prepareMocks() { - when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); - when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) - .thenReturn(sessionParametersTemplate); - - when(driver.session(any(Consumer.class))).thenReturn(session); + when(driver.session(any(SessionConfig.class))).thenReturn(session); when(driver.defaultTypeSystem()).thenReturn(typeSystem); } @@ -150,7 +141,6 @@ class Neo4jClientTest { @Test void databaseSelectionShouldBePossibleOnlyOnce() { - when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.stream()).thenReturn(Stream.of(record1, record2)); @@ -200,8 +190,6 @@ class Neo4jClientTest { @Test void withDatabase() { - when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); - Neo4jClient client = Neo4jClient.create(driver); Optional result = client .delegateTo(runner -> Optional.of(42)) @@ -419,12 +407,13 @@ class Neo4jClientTest { void verifyDatabaseSelection(String targetDatabase) { - verify(driver).session(sessionTemplateCaptor.capture()); - sessionTemplateCaptor.getValue().accept(sessionParametersTemplate); + verify(driver).session(configArgumentCaptor.capture()); + SessionConfig config = configArgumentCaptor.getValue(); + if (targetDatabase != null) { - verify(sessionParametersTemplate).withDatabase(targetDatabase); + assertThat(config.database()).isPresent().contains(targetDatabase); } else { - verify(sessionParametersTemplate, never()).withDatabase(any()); + assertThat(config.database()).isEmpty(); } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java index 1f81c773f..edbc53fa7 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java @@ -18,6 +18,7 @@ */ package org.neo4j.springframework.data.core; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.mockito.hamcrest.MockitoHamcrest.argThat; @@ -31,7 +32,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,11 +42,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Values; -import org.neo4j.driver.internal.SessionParameters; +import org.neo4j.driver.internal.SessionConfig; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementResult; import org.neo4j.driver.reactive.RxTransaction; @@ -70,10 +69,7 @@ class ReactiveNeo4jClientTest { @Mock private TypeSystem typeSystem; - private ArgumentCaptor sessionTemplateCaptor = ArgumentCaptor.forClass(Consumer.class); - - @Mock - private SessionParameters.Template sessionParametersTemplate; + private ArgumentCaptor configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class); @Mock private RxSession session; @@ -98,11 +94,7 @@ class ReactiveNeo4jClientTest { when(driver.defaultTypeSystem()).thenReturn(typeSystem); - when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); - when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) - .thenReturn(sessionParametersTemplate); - - when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(driver.rxSession(any(SessionConfig.class))).thenReturn(session); when(session.beginTransaction()).thenReturn(Mono.just(transaction)); when(session.close()).thenReturn(Mono.empty()); @@ -164,7 +156,6 @@ class ReactiveNeo4jClientTest { @Test void databaseSelectionShouldBePossibleOnlyOnce() { - when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); @@ -224,7 +215,6 @@ class ReactiveNeo4jClientTest { @Test void withDatabase() { - when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); when(transaction.commit()).thenReturn(Mono.empty()); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -421,12 +411,14 @@ class ReactiveNeo4jClientTest { } void verifyDatabaseSelection(String targetDatabase) { - verify(driver).rxSession(sessionTemplateCaptor.capture()); - sessionTemplateCaptor.getValue().accept(sessionParametersTemplate); + + verify(driver).rxSession(configArgumentCaptor.capture()); + SessionConfig config = configArgumentCaptor.getValue(); + if (targetDatabase != null) { - verify(sessionParametersTemplate).withDatabase(targetDatabase); + assertThat(config.database()).isPresent().contains(targetDatabase); } else { - verify(sessionParametersTemplate, never()).withDatabase(any()); + assertThat(config.database()).isEmpty(); } } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java index a4775f3ac..8c762d01c 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/TransactionHandlingTest.java @@ -18,6 +18,7 @@ */ package org.neo4j.springframework.data.core; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -25,7 +26,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -35,12 +35,11 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; -import org.neo4j.driver.internal.SessionParameters; +import org.neo4j.driver.internal.SessionConfig; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.driver.types.TypeSystem; @@ -64,16 +63,12 @@ class TransactionHandlingTest { @Mock private TypeSystem typeSystem; - @Mock - private SessionParameters.Template sessionParametersTemplate; - @BeforeEach void prepareMocks() { when(driver.defaultTypeSystem()).thenReturn(typeSystem); } - @AfterEach void verifyTypeSystemOnSession() { @@ -92,13 +87,9 @@ class TransactionHandlingTest { @Test void shouldCallCloseOnSession() { - ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); + ArgumentCaptor configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class); - when(driver.session(any(Consumer.class))).thenReturn(session); - when(sessionParametersTemplate.withDatabase(anyString())).thenReturn(sessionParametersTemplate); - when(sessionParametersTemplate.withBookmarks(anyList())).thenReturn(sessionParametersTemplate); - when(sessionParametersTemplate.withDefaultAccessMode(any(AccessMode.class))) - .thenReturn(sessionParametersTemplate); + when(driver.session(any(SessionConfig.class))).thenReturn(session); // Make template acquire session DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient(driver); @@ -106,14 +97,14 @@ class TransactionHandlingTest { s.run("MATCH (n) RETURN n"); } - verify(driver).session(consumerCaptor.capture()); - consumerCaptor.getValue().accept(sessionParametersTemplate); - verify(sessionParametersTemplate).withDatabase("aDatabase"); + verify(driver).session(configArgumentCaptor.capture()); + SessionConfig sessionConfig = configArgumentCaptor.getValue(); + assertThat(sessionConfig.database()).isPresent().contains("aDatabase"); verify(session).run(any(String.class)); verify(session).close(); - verifyNoMoreInteractions(driver, sessionParametersTemplate, session, transaction); + verifyNoMoreInteractions(driver, session, transaction); } @Test @@ -121,7 +112,7 @@ class TransactionHandlingTest { AtomicBoolean transactionIsOpen = new AtomicBoolean(true); - when(driver.session(any(Consumer.class))).thenReturn(session); + when(driver.session(any(SessionConfig.class))).thenReturn(session); when(session.isOpen()).thenReturn(true); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); // Mock closing of the transaction @@ -168,14 +159,14 @@ class TransactionHandlingTest { DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver); neo4jClient.query("RETURN 1").in("aDatabase").fetch().one(); - verify(driver, never()).rxSession(any(Consumer.class)); + verify(driver, never()).rxSession(any(SessionConfig.class)); verifyZeroInteractions(driver, session); } @Test void shouldCloseUnmanagedSessionOnComplete() { - when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(driver.rxSession(any(SessionConfig.class))).thenReturn(session); when(session.beginTransaction()).thenReturn(Mono.just(transaction)); when(transaction.commit()).thenReturn(Mono.empty()); when(session.close()).thenReturn(Mono.empty()); @@ -188,7 +179,7 @@ class TransactionHandlingTest { .expectNext("1") .verifyComplete(); - verify(driver).rxSession(any(Consumer.class)); + verify(driver).rxSession(any(SessionConfig.class)); verify(session).beginTransaction(); verify(transaction).commit(); verify(transaction).rollback(); @@ -199,8 +190,7 @@ class TransactionHandlingTest { @Test void shouldCloseUnmanagedSessionOnError() { - - when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(driver.rxSession(any(SessionConfig.class))).thenReturn(session); when(session.beginTransaction()).thenReturn(Mono.just(transaction)); when(transaction.rollback()).thenReturn(Mono.empty()); when(session.close()).thenReturn(Mono.empty()); @@ -214,7 +204,7 @@ class TransactionHandlingTest { .expectError(SomeException.class) .verify(); - verify(driver).rxSession(any(Consumer.class)); + verify(driver).rxSession(any(SessionConfig.class)); verify(session).beginTransaction(); verify(transaction).commit(); verify(transaction).rollback(); diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java index 929efab97..ed0ed467b 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java @@ -24,7 +24,6 @@ import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionMa import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; import javax.transaction.Status; import javax.transaction.UserTransaction; @@ -41,6 +40,7 @@ import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.internal.SessionConfig; import org.neo4j.driver.types.TypeSystem; import org.neo4j.springframework.data.core.Neo4jClient; import org.springframework.transaction.TransactionStatus; @@ -85,7 +85,7 @@ class Neo4jTransactionManagerTest { void triggerCommitCorrectly() { when(driver.defaultTypeSystem()).thenReturn(typeSystem); - when(driver.session(any(Consumer.class))).thenReturn(session); + when(driver.session(any(SessionConfig.class))).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(session.isOpen()).thenReturn(true); @@ -99,7 +99,7 @@ class Neo4jTransactionManagerTest { txManager.commit(txStatus); - verify(driver).session(any(Consumer.class)); + verify(driver).session(any(SessionConfig.class)); verify(session).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -120,7 +120,7 @@ class Neo4jTransactionManagerTest { AtomicBoolean sessionIsOpen = new AtomicBoolean(true); AtomicBoolean transactionIsOpen = new AtomicBoolean(true); - when(driver.session(any(Consumer.class))).thenReturn(session); + when(driver.session(any(SessionConfig.class))).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction); doAnswer(invocation -> { @@ -172,7 +172,7 @@ class Neo4jTransactionManagerTest { } }); - verify(driver).session(any(Consumer.class)); + verify(driver).session(any(SessionConfig.class)); verify(session).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -214,7 +214,7 @@ class Neo4jTransactionManagerTest { } }); - verify(driver).session(any(Consumer.class)); + verify(driver).session(any(SessionConfig.class)); verify(session).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -257,7 +257,7 @@ class Neo4jTransactionManagerTest { verify(userTransaction).begin(); - verify(driver).session(any(Consumer.class)); + verify(driver).session(any(SessionConfig.class)); verify(session, times(2)).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); @@ -298,7 +298,7 @@ class Neo4jTransactionManagerTest { verify(userTransaction).begin(); verify(userTransaction).rollback(); - verify(driver).session(any(Consumer.class)); + verify(driver).session(any(SessionConfig.class)); verify(session, times(2)).isOpen(); verify(session).beginTransaction(any(TransactionConfig.class)); diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java index f6b6d4b78..f6e239296 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManagerTest.java @@ -28,8 +28,6 @@ import io.r2dbc.h2.H2ConnectionFactory; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.util.function.Consumer; - import javax.transaction.UserTransaction; import org.junit.jupiter.api.BeforeEach; @@ -42,6 +40,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.neo4j.driver.Driver; import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.internal.SessionConfig; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxTransaction; import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager; @@ -71,7 +70,7 @@ class ReactiveNeo4jTransactionManagerTest { @BeforeEach void setUp() { - when(driver.rxSession(any(Consumer.class))).thenReturn(session); + when(driver.rxSession(any(SessionConfig.class))).thenReturn(session); when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(Mono.just(transaction)); when(transaction.rollback()).thenReturn(Mono.empty()); when(transaction.commit()).thenReturn(Mono.empty()); @@ -106,7 +105,7 @@ class ReactiveNeo4jTransactionManagerTest { .expectNextCount(1L) .verifyComplete(); - verify(driver).rxSession(any(Consumer.class)); + verify(driver).rxSession(any(SessionConfig.class)); verify(session).beginTransaction(any(TransactionConfig.class)); verify(session).close(); @@ -133,7 +132,7 @@ class ReactiveNeo4jTransactionManagerTest { .expectNextCount(1L) .verifyComplete(); - verify(driver).rxSession(any(Consumer.class)); + verify(driver).rxSession(any(SessionConfig.class)); verify(session).beginTransaction(any(TransactionConfig.class)); verify(session).close(); @@ -164,7 +163,7 @@ class ReactiveNeo4jTransactionManagerTest { .expectNextCount(1L) .verifyComplete(); - verify(driver).rxSession(any(Consumer.class)); + verify(driver).rxSession(any(SessionConfig.class)); verify(session).beginTransaction(any(TransactionConfig.class)); verify(session).close(); @@ -195,7 +194,7 @@ class ReactiveNeo4jTransactionManagerTest { .expectNextCount(1L) .verifyComplete(); - verify(driver).rxSession(any(Consumer.class)); + verify(driver).rxSession(any(SessionConfig.class)); verify(session).beginTransaction(any(TransactionConfig.class)); verify(session).close(); From 88338650c68945483009ae4823fbe020bf94ffc7 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 9 Jul 2019 12:36:12 +0200 Subject: [PATCH 147/342] Add support for externally generated ids. This includes an optimised notion for declaring id fields: * `@Id`: Use assigned ids * `@Id @GeneratedValue`: Use database internal ids * `@Id @GeneratedValue(Generator.class)`: Use externally generated, assiged ids. `@Id` can be either `org.neo4j.springframework.data.core.schema.Id` or `@org.springframework.data.annotation.Id`. All declared `IdGenerator` classes will be instantiated via Spring facilities and thus will be subject to injection etc and can profit from Spring mechanism. Furthermore, generators can be referred via a name. This will be pulled as beans from the context, which opens many new possibilities, i.e. an id generator using current state retrieved from the database before first usage. --- .../mapping/DefaultNeo4jIsNewStrategy.java | 23 +-- .../mapping/DefaultNeo4jPersistentEntity.java | 54 ++--- .../core/mapping/Neo4jMappingContext.java | 88 ++++++++- .../data/core/schema/GeneratedValue.java | 77 ++++++++ .../springframework/data/core/schema/Id.java | 111 +++++------ .../data/core/schema/IdDescription.java | 83 ++++++-- .../data/core/schema/IdGenerator.java | 2 +- .../data/core/schema/Node.java | 1 + .../data/core/schema/NodeDescription.java | 2 +- .../data/core/schema/Schema.java | 11 ++ .../core/support/UUIDStringGenerator.java | 40 ++++ ...Neo4jRepositoryConfigurationExtension.java | 21 ++ ...Neo4jRepositoryConfigurationExtension.java | 21 ++ .../event/IdGeneratingBeforeBindCallback.java | 51 +++++ .../data/repository/event/IdPopulator.java | 86 ++++++++ .../ReactiveAuditingBeforeBindCallback.java | 3 +- ...eactiveIdGeneratingBeforeBindCallback.java | 54 +++++ .../repository/query/CypherAdapterUtils.java | 21 +- .../DefaultNeo4jIsNewStrategyTest.java | 26 ++- .../core/mapping/Neo4jMappingContextTest.java | 38 +++- .../data/core/schema/IdDescriptionTest.java | 69 +++++++ .../imperative/IdGeneratorsIT.java | 147 ++++++++++++++ .../reactive/ReactiveIdGeneratorsIT.java | 187 ++++++++++++++++++ .../shared/AbstractNamedThing.java | 35 ++++ .../shared/IdGeneratorsITBase.java | 74 +++++++ .../shared/ImmutableAuditableThing.java | 7 +- .../shared/PersonWithAllConstructor.java | 3 +- .../shared/PersonWithNoConstructor.java | 4 +- .../integration/shared/PersonWithWither.java | 4 +- .../shared/TestSequenceGenerator.java | 39 ++++ .../shared/ThingWithAssignedId.java | 18 +- .../shared/ThingWithGeneratedId.java | 41 ++++ .../shared/ThingWithIdGeneratedByBean.java | 41 ++++ .../repository/event/IdPopulatorTest.java | 114 +++++++++++ .../repository/event/ImmutableSample.java | 6 +- ...eactiveAuditingBeforeBindCallbackTest.java | 6 +- .../data/repository/event/Sample.java | 6 +- .../repository/query/RepositoryQueryTest.java | 3 +- .../data/integration/shared/KotlinPerson.kt | 3 +- 39 files changed, 1430 insertions(+), 190 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/GeneratedValue.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/support/UUIDStringGenerator.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/IdGeneratingBeforeBindCallback.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/IdPopulator.java create mode 100644 spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveIdGeneratingBeforeBindCallback.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/schema/IdDescriptionTest.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/AbstractNamedThing.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/IdGeneratorsITBase.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/TestSequenceGenerator.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithGeneratedId.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithIdGeneratedByBean.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/IdPopulatorTest.java diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java index 6bfceb47e..70936f73a 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java @@ -20,7 +20,7 @@ package org.neo4j.springframework.data.core.mapping; import java.util.function.Function; -import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.IdDescription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.support.IsNewStrategy; @@ -54,17 +54,17 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy { Assert.notNull(entityMetaData, "Entity meta data must not be null."); - Id.Strategy idStrategy = entityMetaData.getIdDescription().getIdStrategy(); + IdDescription idDescription = entityMetaData.getIdDescription(); Class valueType = entityMetaData.getRequiredIdProperty().getType(); - if (idStrategy == Id.Strategy.EXTERNALLY_GENERATED && valueType.isPrimitive()) { + if (idDescription.isExternallyGeneratedId() && valueType.isPrimitive()) { throw new IllegalArgumentException(String.format("Cannot use %s with externally generated, primitive ids.", DefaultNeo4jIsNewStrategy.class.getName())); } Function valueLookup; Neo4jPersistentProperty versionProperty = entityMetaData.getVersionProperty(); - if (idStrategy == Id.Strategy.ASSIGNED) { + if (idDescription.isAssignedId()) { if (versionProperty == null) { log.warn("Instances of " + entityMetaData.getType() + " with an assigned id will always be treated as new without version property!"); @@ -78,17 +78,18 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy { valueLookup = source -> entityMetaData.getIdentifierAccessor(source).getIdentifier(); } - return new DefaultNeo4jIsNewStrategy(idStrategy, valueType, valueLookup); + return new DefaultNeo4jIsNewStrategy(idDescription, valueType, valueLookup); } - private final Id.Strategy strategy; + private final IdDescription idDescription; private final Class valueType; private @Nullable final Function valueLookup; - private DefaultNeo4jIsNewStrategy(Id.Strategy strategy, Class valueType, Function valueLookup) { - this.strategy = strategy; + private DefaultNeo4jIsNewStrategy(IdDescription idDescription, Class valueType, + Function valueLookup) { + this.idDescription = idDescription; this.valueType = valueType; this.valueLookup = valueLookup; } @@ -101,7 +102,7 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy { public boolean isNew(Object entity) { Object value = valueLookup.apply(entity); - if (strategy.isInternal()) { + if (idDescription.isInternallyGeneratedId()) { boolean isNew = false; if (value != null && valueType.isPrimitive() && Number.class.isInstance(value)) { @@ -111,9 +112,9 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy { } return isNew; - } else if (strategy == Id.Strategy.EXTERNALLY_GENERATED) { + } else if (idDescription.isExternallyGeneratedId()) { return value == null; - } else if (strategy == Id.Strategy.ASSIGNED) { + } else if (idDescription.isAssignedId()) { if (valueType != null && !valueType.isPrimitive()) { return value == null; } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java index 08b3163bb..02ea0feae 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentEntity.java @@ -27,13 +27,12 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.neo4j.springframework.data.core.schema.GeneratedValue; import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; -import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.IdDescription; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.core.schema.Property; +import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.support.IsNewStrategy; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; @@ -129,7 +128,7 @@ class DefaultNeo4jPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity optionalIdAnnotation = Optional - .ofNullable(AnnotatedElementUtils.findMergedAnnotation(idProperty.getField(), Id.class)); - return optionalIdAnnotation - .map(idAnnotation -> { + Neo4jPersistentProperty idProperty = this.getRequiredIdProperty(); + GeneratedValue generatedValueAnnotation = idProperty.findAnnotation(GeneratedValue.class); - if (idAnnotation.strategy() == Id.Strategy.INTERNALLY_GENERATED) { - if (idProperty.findAnnotation(Property.class) != null) { - throw new IllegalArgumentException( - "Cannot use internal id strategy with custom property " + idProperty.getPropertyName() - + " on entity class " + this.getUnderlyingClass().getName()); - } + // Assigned ids + if (generatedValueAnnotation == null) { + return IdDescription.forAssignedIds(idProperty.getPropertyName()); + } - if (!VALID_GENERATED_ID_TYPES.contains(idProperty.getActualType())) { - throw new IllegalArgumentException("Internally generated ids can only be assigned to one of " + VALID_GENERATED_ID_TYPES); - } - } + // Internally generated ids. + if (generatedValueAnnotation.generatorClass() == GeneratedValue.InternalIdGenerator.class && generatedValueAnnotation.generatorRef().isEmpty()) { + if (idProperty.findAnnotation(Property.class) != null) { + throw new IllegalArgumentException( + "Cannot use internal id strategy with custom property " + idProperty.getPropertyName() + + " on entity class " + this.getUnderlyingClass().getName()); + } - return new IdDescription(idAnnotation.strategy(), idAnnotation.generator(), - idAnnotation.strategy() == Id.Strategy.INTERNALLY_GENERATED ? null : idProperty.getPropertyName()); - }) - .orElseGet(() -> new IdDescription()); + if (!VALID_GENERATED_ID_TYPES.contains(idProperty.getActualType())) { + throw new IllegalArgumentException("Internally generated ids can only be assigned to one of " + VALID_GENERATED_ID_TYPES); + } + + return IdDescription.forInternallyGeneratedIds(); + } + + // Externally generated ids. + return IdDescription + .forExternallyGeneratedIds(generatedValueAnnotation.generatorClass(), + generatedValueAnnotation.generatorRef(), idProperty.getPropertyName()); } - Collection computeGraphProperties() { + private Collection computeGraphProperties() { final List computedGraphProperties = new ArrayList<>(); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java index 18bcbe450..fe315c65d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java @@ -32,22 +32,36 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import org.apiguardian.api.API; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; +import org.neo4j.springframework.data.core.schema.IdDescription; +import org.neo4j.springframework.data.core.schema.IdGenerator; +import org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes; +import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.neo4j.springframework.data.core.schema.Relationship; +import org.neo4j.springframework.data.core.schema.RelationshipDescription; +import org.neo4j.springframework.data.core.schema.Schema; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes; -import org.neo4j.springframework.data.core.schema.NodeDescription; -import org.neo4j.springframework.data.core.schema.Relationship; -import org.neo4j.springframework.data.core.schema.RelationshipDescription; -import org.neo4j.springframework.data.core.schema.Schema; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; /** * An implementation of both a {@link Schema} as well as a Neo4j version of Spring Datas @@ -59,7 +73,8 @@ import org.springframework.data.util.TypeInformation; */ @API(status = API.Status.INTERNAL, since = "1.0") public final class Neo4jMappingContext - extends AbstractMappingContext, Neo4jPersistentProperty> implements Schema { + extends AbstractMappingContext, Neo4jPersistentProperty> implements Schema, + BeanDefinitionRegistryPostProcessor { /** * The shared entity instantiators of this context. Those should not be recreated for each entity or even not for @@ -76,6 +91,13 @@ public final class Neo4jMappingContext private final ConcurrentMap> relationshipsByPrimaryLabel = new ConcurrentHashMap<>(); + /** + * A map of fallback id generators, that have not been added to the application context + */ + private final Map>, IdGenerator> fallbackIdGenerators = new ConcurrentHashMap<>(); + + private @Nullable ListableBeanFactory beanFactory; + public Neo4jMappingContext() { super.setSimpleTypeHolder(Neo4jSimpleTypes.SIMPLE_TYPE_HOLDER); } @@ -205,4 +227,58 @@ public final class Neo4jMappingContext return Collections.unmodifiableCollection(relationships); } + + @Override + public > T getOrCreateIdGeneratorOfType(Class idGeneratorType) { + Supplier fallbackSupplier = () -> (T) this.fallbackIdGenerators + .computeIfAbsent(idGeneratorType, BeanUtils::instantiateClass); + + if (this.beanFactory == null) { + return fallbackSupplier.get(); + } else { + return this.beanFactory + .getBeanProvider(idGeneratorType) + .getIfUnique(fallbackSupplier); + } + } + + @Override + public > Optional getIdGenerator(String reference) { + try { + return Optional.of((T) this.beanFactory.getBean(reference)); + } catch (NoSuchBeanDefinitionException e) { + return Optional.empty(); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + super.setApplicationContext(applicationContext); + + this.beanFactory = applicationContext; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + + // Register all the known id generators + this.getPersistentEntities().stream() + .map(Neo4jPersistentEntity::getIdDescription) + .filter(IdDescription::isExternallyGeneratedId).map(IdDescription::getIdGeneratorClass) + .filter(Optional::isPresent).map(Optional::get) + .distinct() + .map(generatorClass -> { + RootBeanDefinition definition = new RootBeanDefinition(generatorClass); + definition.setRole(RootBeanDefinition.ROLE_INFRASTRUCTURE); + return definition; + }) + .forEach(definition -> { + String beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry); + registry.registerBeanDefinition(beanName, definition); + }); + } + + @Override + public void postProcessBeanFactory(@SuppressWarnings({ "HiddenField" }) ConfigurableListableBeanFactory beanFactory) throws BeansException { + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/GeneratedValue.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/GeneratedValue.java new file mode 100644 index 000000000..e27b6cb71 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/GeneratedValue.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.core.schema; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.springframework.core.annotation.AliasFor; + +/** + * Indicates a generated id. Ids can be generated internally. by the database itself or by an external generator. This annotation + * defaults to the internally generated ids. + *

+ * An internal id has no corresponding property on a node. It can only retrieved via the built-in Cypher function {@code id()}. + *

+ * To use an external id generator, specify on the + * + * @author Michael J. Simons + * @since 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +@Inherited +@API(status = API.Status.STABLE, since = "1.0") +public @interface GeneratedValue { + + /** + * @return The generator to use. + * @see #generatorClass() + */ + @AliasFor("generatorClass") + Class> value() default GeneratedValue.InternalIdGenerator.class; + + /** + * @return The generator to use. Defaults to {@link InternalIdGenerator}, which indicates database generated values. + */ + @AliasFor("value") + Class> generatorClass() default GeneratedValue.InternalIdGenerator.class; + + /** + * @return An optional reference to a bean to be used as ID generator. + */ + String generatorRef() default ""; + + /** + * This {@link IdGenerator} does nothing. It is used for relying on the internal, database-side created id. + */ + final class InternalIdGenerator implements IdGenerator { + + @Override + public Void generateId(String primaryLabel, Object entity) { + return null; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Id.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Id.java index db9121c96..283b46204 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Id.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Id.java @@ -24,12 +24,57 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.EnumSet; import org.apiguardian.api.API; /** - * Annotation to configure assigment of ids. + * This annotation is included here for completeness. It marks an attribute as the primary id of a node entity. It can + * be used as an alternative to {@link org.springframework.data.annotation.Id} and it may provide additional features + * in the future. + * + *

+ * + * To use assigned ids, annotate an arbitrary attribute of your domain class with {@link org.springframework.data.annotation.Id} + * or this annotation: + *

+ * @Node
+ * public class MyEntity {
+ *     @Id
+ *     String theId;
+ * }
+ * 
+ * You can combine {@code @Id} with {@code @Property} with assigned ids to rename the node property in which the assigned id is stored. + * + *

+ * + * To use internally generated ids, annotate an arbitrary attribute of type {@code java.lang.long} or {@code java.lang.Long} + * with {@code @Id} and {@link GeneratedValue @GeneratedValue}. + *

+ * @Node
+ * public class MyEntity {
+ *     @Id @GeneratedValue
+ *     Long id;
+ * }
+ * 
+ * + * It does not need to be named {@code id}, but most people chose this as the attribute in the class. As the attribute + * does not correspond to a node property, it cannot be renamed via {@code @Property}. + * + *

+ * + * To use externally generated ids, annotate an arbitrary attribute with a type that your generated returns + * with {@code @Id} and {@link GeneratedValue @GeneratedValue} and specify the generator class. + * + *

+ * @Node
+ * public class MyEntity {
+ *     @Id @GeneratedValue(UUIDStringGenerator.class)
+ *     String theId;
+ * }
+ * 
+ * + * Externally generated ids are indistinguishable to assigned ids from the database perspective and thus can be arbitrarily + * named via {@code @Property}. * * @author Michael J. Simons * @since 1.0 @@ -41,66 +86,4 @@ import org.apiguardian.api.API; @org.springframework.data.annotation.Id @API(status = API.Status.STABLE, since = "1.0") public @interface Id { - - /** - * Enumerating available strategies for providing ids for new entities. - * @since 1.0 - */ - enum Strategy { - - /** - * The default, use Neo4js internal ids. - */ - INTERNALLY_GENERATED, - - /** - * Use assigned values. - */ - ASSIGNED, - - /** - * Use externally generated values, a generator class is required. - */ - EXTERNALLY_GENERATED; - - /** - * @return True, if the database generated the ID. - */ - public boolean isInternal() { - return this == INTERNALLY_GENERATED; - } - - /** - * @return True, if the ID is assigned to the entity before the entity hits the database - */ - public boolean isExternal() { - return EnumSet.of(ASSIGNED, EXTERNALLY_GENERATED).contains(this); - } - } - - /** - * Configure the strategy for generating ids. Some strategies require a {@link #generator()}. - * - * @return The strategy applied for generating ids for new entities. Defaults to use Neo4j internal database identifiers. - */ - Strategy strategy() default Strategy.INTERNALLY_GENERATED; - - /** - * - * @return The generator that fits the selected {@link #strategy()}. Defaults to a Noop generator. - */ - Class generator() default NoopIdGenerator.class; - - /** - * This {@link IdGenerator} does nothing. It is used for relying on the internal, database-side created id. - * @since 1.0 - */ - // Needed to make the generator attribute defaultable (null is not a constant) - class NoopIdGenerator implements IdGenerator { - - @Override - public Object generateId(Object entity) { - return null; - } - } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/IdDescription.java index 48c4178a1..ff1e0bd32 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/IdDescription.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/IdDescription.java @@ -22,6 +22,7 @@ import java.util.Optional; import org.apiguardian.api.API; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Description how to generate Ids for entities. @@ -33,42 +34,92 @@ import org.springframework.lang.Nullable; public final class IdDescription { /** - * The selected strategy. + * The class representing a generator for new ids or null for assigned ids. */ - private final Id.Strategy idStrategy; + private @Nullable final Class> idGeneratorClass; /** - * The class representing a generator for new ids. + * A reference to an ID generator. */ - private @Nullable final Class idGeneratorClass; + private @Nullable final String idGeneratorRef; /** * The property that stores the id if applicable. */ private @Nullable final String graphPropertyName; - public IdDescription() { - this(Id.Strategy.INTERNALLY_GENERATED, null, null); + public static IdDescription forAssignedIds(String graphPropertyName) { + + Assert.notNull(graphPropertyName, "Graph property name is required."); + return new IdDescription(null, null, graphPropertyName); } - public IdDescription(Id.Strategy idStrategy, - @Nullable Class idGeneratorClass, @Nullable String graphPropertyName) { - this.idStrategy = idStrategy; + public static IdDescription forInternallyGeneratedIds() { + return new IdDescription(GeneratedValue.InternalIdGenerator.class, null, null); + } + + public static IdDescription forExternallyGeneratedIds( + @Nullable Class> idGeneratorClass, + @Nullable String idGeneratorRef, + String graphPropertyName) { + + Assert.notNull(graphPropertyName, "Graph property name is required."); + try { + Assert.hasText(idGeneratorRef, "Reference to an ID generator has precedence."); + + return new IdDescription(null, idGeneratorRef, graphPropertyName); + } catch (IllegalArgumentException e) { + Assert.notNull(idGeneratorClass, "Class of id generator is required."); + Assert.isTrue(idGeneratorClass != GeneratedValue.InternalIdGenerator.class, + "Cannot use InternalIdGenerator for externally generated ids."); + + return new IdDescription(idGeneratorClass, null, graphPropertyName); + } + } + + public IdDescription( + @Nullable Class> idGeneratorClass, + @Nullable String idGeneratorRef, + @Nullable String graphPropertyName + ) { this.idGeneratorClass = idGeneratorClass; + this.idGeneratorRef = idGeneratorRef != null && idGeneratorRef.isEmpty() ? null : idGeneratorRef; this.graphPropertyName = graphPropertyName; } - public Id.Strategy getIdStrategy() { - return idStrategy; - } - - public Optional> getIdGeneratorClass() { + public Optional>> getIdGeneratorClass() { return Optional.ofNullable(idGeneratorClass); } + public Optional getIdGeneratorRef() { + return Optional.ofNullable(idGeneratorRef); + } + /** - * An ID description has only a corresponding graph property name when it's based on either {@link Id.Strategy#ASSIGNED} - * or {@link Id.Strategy#EXTERNALLY_GENERATED}. An internal id has no corresponding graph property and therefor this method + * @return True, if the ID is assigned to the entity before the entity hits the database, either manually or through a generator. + */ + public boolean isAssignedId() { + return this.idGeneratorClass == null && this.idGeneratorRef == null; + } + + /** + * @return True, if the database generated the ID. + */ + public boolean isInternallyGeneratedId() { + return this.idGeneratorClass == GeneratedValue.InternalIdGenerator.class; + } + + /** + * @return True, if the ID is externally generated. + */ + public boolean isExternallyGeneratedId() { + return (this.idGeneratorClass != null && this.idGeneratorClass != GeneratedValue.InternalIdGenerator.class) + || this.idGeneratorRef != null; + } + + /** + * An ID description has only a corresponding graph property name when it's bas on an external assigment. + * An internal id has no corresponding graph property and therefor this method * will return an empty {@link Optional} in such cases. * * @return The name of an optional graph property. diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/IdGenerator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/IdGenerator.java index ac5de376a..c62108495 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/IdGenerator.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/IdGenerator.java @@ -37,5 +37,5 @@ public interface IdGenerator { * @param entity the entity to be saved * @return id to be assigned to the entity */ - T generateId(Object entity); + T generateId(String primaryLabel, Object entity); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Node.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Node.java index 52aa568e9..9260f9d94 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Node.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Node.java @@ -38,6 +38,7 @@ import org.springframework.core.annotation.AliasFor; @Target(ElementType.TYPE) @Documented @Inherited +@org.springframework.data.annotation.Persistent @API(status = API.Status.STABLE, since = "1.0") public @interface Node { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java index f09146913..623d65f99 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/NodeDescription.java @@ -73,6 +73,6 @@ public interface NodeDescription { * @return True if entities for this node use Neo4j internal ids. */ default boolean isUsingInternalIds() { - return this.getIdDescription().getIdStrategy() == Id.Strategy.INTERNALLY_GENERATED; + return this.getIdDescription().isInternallyGeneratedId(); } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java index f656509dd..4ac3546e7 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java @@ -118,4 +118,15 @@ public interface Schema { return getBinderFunctionFor(sourceClass).orElseThrow(() -> new UnknownEntityException(sourceClass)); } + + /** + * Creates or retrieves an instance of the given id generator class. During the lifetime of the schema, + * this method returns the same instance of reoccurring requests of the same type. + * + * @param idGeneratorType The type of the ID generator to return + * @return The id generator. + */ + > T getOrCreateIdGeneratorOfType(Class idGeneratorType); + + > Optional getIdGenerator(String reference); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/support/UUIDStringGenerator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/support/UUIDStringGenerator.java new file mode 100644 index 000000000..9f6d1c951 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/support/UUIDStringGenerator.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.core.support; + +import java.util.UUID; + +import org.apiguardian.api.API; +import org.neo4j.springframework.data.core.schema.IdGenerator; + +/** + * A generator providing UUIDs. + * + * @author Michael J. Simons + * @soundtrack Various - Kung Fury (Original Motion Picture Soundtrack) + * @since 1.0 + */ +@API(status = API.Status.STABLE, since = "1.0") +public final class UUIDStringGenerator implements IdGenerator { + + @Override + public String generateId(String primaryLabel, Object entity) { + return UUID.randomUUID().toString(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java index f25b8e7e3..cba48ab0e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -25,9 +25,14 @@ import java.util.Collections; import org.apiguardian.api.API; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.repository.Neo4jRepository; +import org.neo4j.springframework.data.repository.event.IdGeneratingBeforeBindCallback; import org.neo4j.springframework.data.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; @@ -105,4 +110,20 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi builder.addPropertyReference("neo4jMappingContext", source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#registerBeansForRoot(org.springframework.beans.factory.support.BeanDefinitionRegistry, org.springframework.data.repository.config.RepositoryConfigurationSource) + */ + @Override + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) { + + RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(IdGeneratingBeforeBindCallback.class); + rootBeanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + String beanName = BeanDefinitionReaderUtils.generateBeanName(rootBeanDefinition, registry); + registry.registerBeanDefinition(beanName, rootBeanDefinition); + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java index 78ed57013..64ea0d036 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/config/ReactiveNeo4jRepositoryConfigurationExtension.java @@ -25,9 +25,14 @@ import java.util.Collections; import org.apiguardian.api.API; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.repository.Neo4jRepository; +import org.neo4j.springframework.data.repository.event.ReactiveIdGeneratingBeforeBindCallback; import org.neo4j.springframework.data.repository.support.Neo4jRepositoryFactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; @@ -104,4 +109,20 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit builder.addPropertyReference("neo4jMappingContext", source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#registerBeansForRoot(org.springframework.beans.factory.support.BeanDefinitionRegistry, org.springframework.data.repository.config.RepositoryConfigurationSource) + */ + @Override + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) { + + RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(ReactiveIdGeneratingBeforeBindCallback.class); + rootBeanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + String beanName = BeanDefinitionReaderUtils.generateBeanName(rootBeanDefinition, registry); + registry.registerBeanDefinition(beanName, rootBeanDefinition); + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/IdGeneratingBeforeBindCallback.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/IdGeneratingBeforeBindCallback.java new file mode 100644 index 000000000..105ce9d41 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/IdGeneratingBeforeBindCallback.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import org.apiguardian.api.API; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.springframework.core.Ordered; + +/** + * Callback used to call the ID generator configured for an entity just before binding. + * + * @author Michael J. Simons + * @soundtrack Various - Kung Fury (Original Motion Picture Soundtrack) + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class IdGeneratingBeforeBindCallback implements BeforeBindCallback, Ordered { + + private final IdPopulator idPopulator; + + public IdGeneratingBeforeBindCallback(Neo4jMappingContext neo4jMappingContext) { + this.idPopulator = new IdPopulator(neo4jMappingContext); + } + + @Override + public Object onBeforeBind(Object entity) { + + return idPopulator.populateIfNecessary(entity); + } + + @Override + public int getOrder() { + return AuditingBeforeBindCallback.NEO4J_AUDITING_ORDER - 10; + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/IdPopulator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/IdPopulator.java new file mode 100644 index 000000000..46b40552f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/IdPopulator.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import java.util.Optional; + +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty; +import org.neo4j.springframework.data.core.schema.IdDescription; +import org.neo4j.springframework.data.core.schema.IdGenerator; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.util.Assert; + +/** + * Support for populating id properties with user specified id generators. + * + * @author Michael J. Simons + */ +final class IdPopulator { + + private final Neo4jMappingContext neo4jMappingContext; + + IdPopulator(Neo4jMappingContext neo4jMappingContext) { + + Assert.notNull(neo4jMappingContext, "A mapping context is required."); + + this.neo4jMappingContext = neo4jMappingContext; + } + + Object populateIfNecessary(Object entity) { + + Assert.notNull(entity, "Entity may not be null!"); + + Neo4jPersistentEntity nodeDescription = neo4jMappingContext.getRequiredPersistentEntity(entity.getClass()); + IdDescription idDescription = nodeDescription.getIdDescription(); + + // Filter in two steps to avoid unnecessary object creation. + if (!idDescription.isExternallyGeneratedId()) { + return entity; + } + + PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(entity); + Neo4jPersistentProperty idProperty = nodeDescription.getRequiredIdProperty(); + + // Check existing ID + if (propertyAccessor.getProperty(idProperty) != null) { + return entity; + } + + IdGenerator idGenerator; + + // Get or create the shared generator + // Ref has precedence over class + Optional optionalIdGeneratorRef = idDescription.getIdGeneratorRef(); + if (optionalIdGeneratorRef.isPresent()) { + + idGenerator = neo4jMappingContext + .getIdGenerator(optionalIdGeneratorRef.get()).orElseThrow(() -> new IllegalStateException( + "Id generator named " + optionalIdGeneratorRef.get() + " not found!")); + } else { + + // At this point, the class must be present, so we don't check the optional not anymore + idGenerator = neo4jMappingContext.getOrCreateIdGeneratorOfType(idDescription.getIdGeneratorClass().get()); + } + + propertyAccessor.setProperty(idProperty, idGenerator.generateId(nodeDescription.getPrimaryLabel(), entity)); + return propertyAccessor.getBean(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallback.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallback.java index 435bec8a3..7ac5fb623 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallback.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallback.java @@ -62,7 +62,8 @@ public final class ReactiveAuditingBeforeBindCallback implements ReactiveBeforeB */ @Override public Publisher onBeforeBind(Object entity) { - return Mono.just(auditingHandlerFactory.getObject().markAudited(entity)); + + return Mono.fromSupplier(() -> auditingHandlerFactory.getObject().markAudited(entity)); } /* diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveIdGeneratingBeforeBindCallback.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveIdGeneratingBeforeBindCallback.java new file mode 100644 index 000000000..2fa17eb21 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/event/ReactiveIdGeneratingBeforeBindCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import reactor.core.publisher.Mono; + +import org.apiguardian.api.API; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.reactivestreams.Publisher; +import org.springframework.core.Ordered; + +/** + * Callback used to call the ID generator configured for an entity just before binding. + * + * @author Michael J. Simons + * @soundtrack Various - Kung Fury (Original Motion Picture Soundtrack) + * @since 1.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class ReactiveIdGeneratingBeforeBindCallback implements ReactiveBeforeBindCallback, Ordered { + + private final IdPopulator idPopulator; + + public ReactiveIdGeneratingBeforeBindCallback(Neo4jMappingContext neo4jMappingContext) { + this.idPopulator = new IdPopulator(neo4jMappingContext); + } + + @Override + public Publisher onBeforeBind(Object entity) { + + return Mono.fromSupplier(() -> idPopulator.populateIfNecessary(entity)); + } + + @Override + public int getOrder() { + return ReactiveAuditingBeforeBindCallback.NEO4J_REACTIVE_AUDITING_ORDER - 10; + } +} diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java index aeaefc5bf..a131f1b18 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java @@ -31,7 +31,6 @@ import org.neo4j.springframework.data.core.cypher.*; import org.neo4j.springframework.data.core.cypher.StatementBuilder.BuildableStatement; import org.neo4j.springframework.data.core.cypher.StatementBuilder.OngoingReadingAndWith; import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; -import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.IdDescription; import org.neo4j.springframework.data.core.schema.NodeDescription; import org.neo4j.springframework.data.core.schema.Schema; @@ -140,7 +139,7 @@ public final class CypherAdapterUtils { List expressions = new ArrayList<>(); expressions.add(rootNode); - if (idDescription.getIdStrategy() == Id.Strategy.INTERNALLY_GENERATED) { + if (idDescription.isInternallyGeneratedId()) { expressions.add(Functions.id(rootNode).as(NAME_OF_INTERNAL_ID)); } return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())) @@ -159,7 +158,7 @@ public final class CypherAdapterUtils { Node rootNode = node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); IdDescription idDescription = nodeDescription.getIdDescription(); Parameter idParameter = parameter(NAME_OF_ID_PARAM); - if (idDescription.getIdStrategy().isExternal()) { + if (!idDescription.isInternallyGeneratedId()) { String nameOfIdProperty = idDescription.getOptionalGraphPropertyName() .orElseThrow(() -> new MappingException("External id does not correspond to a graph property!")); @@ -235,17 +234,11 @@ public final class CypherAdapterUtils { final SymbolicName rootNode = Cypher.name(NAME_OF_ROOT_NODE); final IdDescription idDescription = nodeDescription.getIdDescription(); Expression idExpression; - switch (idDescription.getIdStrategy()) { - case INTERNALLY_GENERATED: - idExpression = Functions.id(rootNode); - break; - case ASSIGNED: - case EXTERNALLY_GENERATED: - idExpression = idDescription.getOptionalGraphPropertyName() - .map(propertyName -> property(rootNode.getName(), propertyName)).get(); - break; - default: - throw new IllegalStateException("Unsupported ID strategy: %s" + idDescription.getIdStrategy()); + if (idDescription.isInternallyGeneratedId()) { + idExpression = Functions.id(rootNode); + } else { + idExpression = idDescription.getOptionalGraphPropertyName() + .map(propertyName -> property(rootNode.getName(), propertyName)).get(); } return idExpression; diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategyTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategyTest.java index f65952b14..0160463b5 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategyTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategyTest.java @@ -26,10 +26,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.springframework.data.core.schema.IdDescription; +import org.neo4j.springframework.data.core.schema.IdGenerator; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.neo4j.springframework.data.core.schema.Id; -import org.neo4j.springframework.data.core.schema.IdDescription; import org.springframework.data.support.IsNewStrategy; /** @@ -54,7 +54,7 @@ class DefaultNeo4jIsNewStrategyTest { Object a = new Object(); Object b = new Object(); - IdDescription idDescription = new IdDescription(); + IdDescription idDescription = IdDescription.forInternallyGeneratedIds(); doReturn(Long.class).when(idProperty).getType(); doReturn(idDescription).when(entityMetaData).getIdDescription(); doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); @@ -72,7 +72,7 @@ class DefaultNeo4jIsNewStrategyTest { Object b = new Object(); Object c = new Object(); - IdDescription idDescription = new IdDescription(); + IdDescription idDescription = IdDescription.forInternallyGeneratedIds(); doReturn(long.class).when(idProperty).getType(); doReturn(idDescription).when(entityMetaData).getIdDescription(); doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); @@ -94,7 +94,7 @@ class DefaultNeo4jIsNewStrategyTest { Object a = new Object(); Object b = new Object(); - IdDescription idDescription = new IdDescription(Id.Strategy.EXTERNALLY_GENERATED, null, null); + IdDescription idDescription = IdDescription.forExternallyGeneratedIds(DummyIdGenerator.class, null, "na"); doReturn(String.class).when(idProperty).getType(); doReturn(idDescription).when(entityMetaData).getIdDescription(); doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); @@ -109,7 +109,7 @@ class DefaultNeo4jIsNewStrategyTest { @Test void doesntNeedToDealWithPrimitives() { - IdDescription idDescription = new IdDescription(Id.Strategy.EXTERNALLY_GENERATED, null, null); + IdDescription idDescription = IdDescription.forExternallyGeneratedIds(DummyIdGenerator.class, null, "na"); doReturn(long.class).when(idProperty).getType(); doReturn(idDescription).when(entityMetaData).getIdDescription(); doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); @@ -126,7 +126,7 @@ class DefaultNeo4jIsNewStrategyTest { @Test void shouldAlwaysTreatEntitiesAsNewWithoutVersion() { Object a = new Object(); - IdDescription idDescription = new IdDescription(Id.Strategy.ASSIGNED, null, null); + IdDescription idDescription = IdDescription.forAssignedIds("na"); doReturn(String.class).when(idProperty).getType(); doReturn(idDescription).when(entityMetaData).getIdDescription(); doReturn(idProperty).when(entityMetaData).getRequiredIdProperty(); @@ -139,7 +139,7 @@ class DefaultNeo4jIsNewStrategyTest { void shouldDealWithVersion() { Object a = new Object(); Object b = new Object(); - IdDescription idDescription = new IdDescription(Id.Strategy.ASSIGNED, null, null); + IdDescription idDescription = IdDescription.forAssignedIds("na"); doReturn(String.class).when(idProperty).getType(); doReturn(String.class).when(versionProperty).getType(); @@ -166,7 +166,7 @@ class DefaultNeo4jIsNewStrategyTest { void shouldDealWithPrimitiveVersion() { Object a = new Object(); Object b = new Object(); - IdDescription idDescription = new IdDescription(Id.Strategy.ASSIGNED, null, null); + IdDescription idDescription = IdDescription.forAssignedIds("na"); doReturn(String.class).when(idProperty).getType(); doReturn(int.class).when(versionProperty).getType(); @@ -189,4 +189,12 @@ class DefaultNeo4jIsNewStrategyTest { assertThat(strategy.isNew(b)).isFalse(); } } + + static class DummyIdGenerator implements IdGenerator { + + @Override + public Void generateId(String primaryLabel, Object entity) { + return null; + } + } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java index b5a273b9f..e0398fa7d 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java @@ -28,8 +28,10 @@ import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.neo4j.springframework.data.core.schema.GeneratedValue; import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.IdGenerator; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.core.schema.NodeDescription; import org.neo4j.springframework.data.core.schema.Property; @@ -55,8 +57,7 @@ public class Neo4jMappingContextTest { .hasValueSatisfying(description -> { assertThat(description.getUnderlyingClass()).isEqualTo(UserNode.class); - assertThat(description.getIdDescription().getIdStrategy()) - .isEqualTo(Id.Strategy.INTERNALLY_GENERATED); + assertThat(description.getIdDescription().isInternallyGeneratedId()).isTrue(); assertThat(description.getGraphProperties()) .extracting(GraphPropertyDescription::getFieldName) @@ -73,8 +74,7 @@ public class Neo4jMappingContextTest { .hasValueSatisfying(description -> { assertThat(description.getUnderlyingClass()).isEqualTo(BikeNode.class); - assertThat(description.getIdDescription().getIdStrategy()) - .isEqualTo(Id.Strategy.ASSIGNED); + assertThat(description.getIdDescription().isAssignedId()).isTrue(); Collection expectedRelationships = Arrays .asList("[:owner] -> (:User)", "[:renter] -> (:User)", "[:dynamicRelationships] -> (:User)"); @@ -133,10 +133,28 @@ public class Neo4jMappingContextTest { assertThat(schema.getMappingFunctionFor(association.getInverse().getAssociationTargetType())).isPresent()); } + @Test + void shouldCacheIdGenerators() { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + IdGenerator dummyIdGenerator1 = schema.getOrCreateIdGeneratorOfType(DummyIdGenerator.class); + IdGenerator dummyIdGenerator2 = schema.getOrCreateIdGeneratorOfType(DummyIdGenerator.class); + + assertThat(dummyIdGenerator1).isSameAs(dummyIdGenerator2); + } + + static class DummyIdGenerator implements IdGenerator { + + @Override + public Void generateId(String primaryLabel, Object entity) { + return null; + } + } + @Node("User") static class UserNode { - @org.springframework.data.annotation.Id + @org.springframework.data.annotation.Id @GeneratedValue private long id; @Relationship(type = "OWNS", inverse = "owner") @@ -150,7 +168,7 @@ public class Neo4jMappingContextTest { static class BikeNode { - @Id(strategy = Id.Strategy.ASSIGNED) + @Id private String id; UserNode owner; @@ -167,7 +185,7 @@ public class Neo4jMappingContextTest { static class TripNode { - @Id(strategy = Id.Strategy.ASSIGNED) + @Id private String id; String name; @@ -175,13 +193,15 @@ public class Neo4jMappingContextTest { static class InvalidId { - @Id @Property("getMappingFunctionFor") + @Id + @GeneratedValue + @Property("getMappingFunctionFor") private String id; } static class InvalidIdType { - @Id + @Id @GeneratedValue private String id; } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/schema/IdDescriptionTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/schema/IdDescriptionTest.java new file mode 100644 index 000000000..1eb2ef498 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/schema/IdDescriptionTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.core.schema; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class IdDescriptionTest { + + @Test + void isAssignedShouldWork() { + + assertThat(IdDescription.forAssignedIds("foobar").isAssignedId()).isTrue(); + assertThat(IdDescription.forAssignedIds("foobar").isExternallyGeneratedId()).isFalse(); + assertThat(IdDescription.forAssignedIds("foobar").isInternallyGeneratedId()).isFalse(); + } + + @Test + void idIsGeneratedInternallyShouldWork() { + + assertThat(IdDescription.forInternallyGeneratedIds().isAssignedId()).isFalse(); + assertThat(IdDescription.forInternallyGeneratedIds().isExternallyGeneratedId()).isFalse(); + assertThat(IdDescription.forInternallyGeneratedIds().isInternallyGeneratedId()).isTrue(); + } + + @Test + void idIsGeneratedExternally() { + + assertThat(IdDescription.forExternallyGeneratedIds(DummyIdGenerator.class, null, "foobar").isAssignedId()).isFalse(); + assertThat(IdDescription.forExternallyGeneratedIds(DummyIdGenerator.class, null, "foobar").isExternallyGeneratedId()) + .isTrue(); + assertThat(IdDescription.forExternallyGeneratedIds(DummyIdGenerator.class, null, "foobar").isInternallyGeneratedId()) + .isFalse(); + + assertThat(IdDescription.forExternallyGeneratedIds(null, "someId", "foobar").isAssignedId()).isFalse(); + assertThat(IdDescription.forExternallyGeneratedIds(null, "someId", "foobar").isExternallyGeneratedId()) + .isTrue(); + assertThat(IdDescription.forExternallyGeneratedIds(null, "someId", "foobar").isInternallyGeneratedId()) + .isFalse(); + } + + private static class DummyIdGenerator implements IdGenerator { + + @Override + public Void generateId(String primaryLabel, Object entity) { + return null; + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java new file mode 100644 index 000000000..f9feb4162 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.imperative; + +import static java.util.stream.Collectors.*; +import static org.assertj.core.api.Assertions.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.core.schema.IdGenerator; +import org.neo4j.springframework.data.integration.shared.IdGeneratorsITBase; +import org.neo4j.springframework.data.integration.shared.ThingWithGeneratedId; +import org.neo4j.springframework.data.integration.shared.ThingWithIdGeneratedByBean; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Michael J. Simons + */ +@ContextConfiguration(classes = IdGeneratorsIT.Config.class) +class IdGeneratorsIT extends IdGeneratorsITBase { + + private final ThingsWithGeneratedIds thingsWithGeneratedIds; + + private final ThingsWithBeanGeneratedIds thingsWithBeanGeneratedIds; + + @Autowired IdGeneratorsIT( + ThingsWithGeneratedIds thingsWithGeneratedIds, ThingsWithBeanGeneratedIds thingsWithBeanGeneratedIds, + Driver driver) { + super(driver); + this.thingsWithGeneratedIds = thingsWithGeneratedIds; + this.thingsWithBeanGeneratedIds = thingsWithBeanGeneratedIds; + } + + @Test + void idGenerationWithNewEntityShouldWork() { + + ThingWithGeneratedId t = new ThingWithGeneratedId("Foobar"); + t.setName("Foobar"); + t = thingsWithGeneratedIds.save(t); + assertThat(t.getTheId()) + .isNotBlank() + .matches("thingWithGeneratedId-\\d+"); + + verifyDatabase(t.getTheId(), t.getName()); + } + + @Test + void idGenerationByBeansShouldWorkWork() { + + ThingWithIdGeneratedByBean t = new ThingWithIdGeneratedByBean("Foobar"); + t.setName("Foobar"); + t = thingsWithBeanGeneratedIds.save(t); + assertThat(t.getTheId()).isEqualTo("ImperativeID."); + + verifyDatabase(t.getTheId(), t.getName()); + } + + @Test + void idGenerationWithNewEntitiesShouldWork() { + + List things = IntStream.rangeClosed(1, 10) + .mapToObj(i -> new ThingWithGeneratedId("name" + i)) + .collect(toList()); + + Iterable savedThings = thingsWithGeneratedIds.saveAll(things); + assertThat(savedThings) + .hasSize(things.size()) + .extracting(ThingWithGeneratedId::getTheId) + .allMatch(s -> s.matches("thingWithGeneratedId-\\d+")); + + Set distinctIds = StreamSupport.stream(savedThings.spliterator(), false) + .map(ThingWithGeneratedId::getTheId).collect(toSet()); + + assertThat(distinctIds).hasSize(things.size()); + } + + @Test + void shouldNotOverwriteExistingId() { + + ThingWithGeneratedId t = thingsWithGeneratedIds.findById(ID_OF_EXISTING_THING).get(); + t.setName("changed"); + t = thingsWithGeneratedIds.save(t); + + assertThat(t.getTheId()) + .isNotBlank() + .isEqualTo(ID_OF_EXISTING_THING); + + verifyDatabase(t.getTheId(), t.getName()); + } + + public interface ThingsWithGeneratedIds extends CrudRepository { + } + + public interface ThingsWithBeanGeneratedIds extends CrudRepository { + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(ThingWithGeneratedId.class.getPackage().getName()); + } + + @Bean + public IdGenerator aFancyIdGenerator() { + return (label, entity) -> "ImperativeID."; + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java new file mode 100644 index 000000000..bccf02cba --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.reactive; + +import static java.util.stream.Collectors.*; +import static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; +import org.neo4j.springframework.data.core.schema.IdGenerator; +import org.neo4j.springframework.data.integration.shared.IdGeneratorsITBase; +import org.neo4j.springframework.data.integration.shared.ThingWithGeneratedId; +import org.neo4j.springframework.data.integration.shared.ThingWithIdGeneratedByBean; +import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.reactive.TransactionalOperator; + +/** + * @author Michael J. Simons + */ +@ContextConfiguration(classes = ReactiveIdGeneratorsIT.Config.class) +class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { + + private final ReactiveTransactionManager transactionManager; + private final ThingsWithGeneratedIds thingsWithGeneratedIds; + private final ThingsWithBeanGeneratedIds thingsWithBeanGeneratedIds; + + @Autowired ReactiveIdGeneratorsIT(ReactiveTransactionManager transactionManager, + ThingsWithGeneratedIds thingsWithGeneratedIds, + ThingsWithBeanGeneratedIds thingsWithBeanGeneratedIds, + Driver driver) { + super(driver); + this.transactionManager = transactionManager; + this.thingsWithGeneratedIds = thingsWithGeneratedIds; + this.thingsWithBeanGeneratedIds = thingsWithBeanGeneratedIds; + } + + @Test + void idGenerationWithNewEntityShouldWork() { + + List savedThings = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> thingsWithGeneratedIds.save(new ThingWithGeneratedId("Foobar"))) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .consumeNextWith(savedThing -> { + + assertThat(savedThing.getName()).isEqualTo("Foobar"); + assertThat(savedThing.getTheId()) + .isNotBlank() + .matches("thingWithGeneratedId-\\d+"); + }) + .verifyComplete(); + + verifyDatabase(savedThings.get(0).getTheId(), savedThings.get(0).getName()); + } + + @Test + void idGenerationByBeansShouldWorkWork() { + + List savedThings = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> thingsWithBeanGeneratedIds.save(new ThingWithIdGeneratedByBean("Foobar"))) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .consumeNextWith(savedThing -> { + + assertThat(savedThing.getName()).isEqualTo("Foobar"); + assertThat(savedThing.getTheId()).isEqualTo("ReactiveID."); + }) + .verifyComplete(); + + verifyDatabase(savedThings.get(0).getTheId(), savedThings.get(0).getName()); + } + + @Test + void idGenerationWithNewEntitiesShouldWork() { + + List things = IntStream.rangeClosed(1, 10) + .mapToObj(i -> new ThingWithGeneratedId("name" + i)) + .collect(toList()); + + Set generatedIds = new HashSet<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> thingsWithGeneratedIds.saveAll(things)) + .map(ThingWithGeneratedId::getTheId) + .as(StepVerifier::create) + .recordWith(() -> generatedIds) + .expectNextCount(things.size()) + .expectRecordedMatches(recorded -> { + assertThat(recorded) + .hasSize(things.size()) + .allMatch(generatedId -> generatedId.matches("thingWithGeneratedId-\\d+")); + return true; + }) + .verifyComplete(); + } + + @Test + void shouldNotOverwriteExistingId() { + + Mono findAndUpdateAThing = thingsWithGeneratedIds.findById(ID_OF_EXISTING_THING) + .flatMap(thing -> { + thing.setName("changed"); + return thingsWithGeneratedIds.save(thing); + }); + + List savedThings = new ArrayList<>(); + TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); + transactionalOperator + .execute(t -> findAndUpdateAThing) + .as(StepVerifier::create) + .recordWith(() -> savedThings) + .consumeNextWith(savedThing -> { + + assertThat(savedThing.getName()).isEqualTo("changed"); + assertThat(savedThing.getTheId()).isEqualTo(ID_OF_EXISTING_THING); + }) + .verifyComplete(); + + verifyDatabase(savedThings.get(0).getTheId(), savedThings.get(0).getName()); + } + + public interface ThingsWithGeneratedIds extends ReactiveCrudRepository { + } + + public interface ThingsWithBeanGeneratedIds extends ReactiveCrudRepository { + } + + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(ThingWithGeneratedId.class.getPackage().getName()); + } + + @Bean + public IdGenerator aFancyIdGenerator() { + return (label, entity) -> "ReactiveID."; + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/AbstractNamedThing.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/AbstractNamedThing.java new file mode 100644 index 000000000..ba8d92d9c --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/AbstractNamedThing.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.shared; + +/** + * @author Michael J. Simons + */ +abstract class AbstractNamedThing { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/IdGeneratorsITBase.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/IdGeneratorsITBase.java new file mode 100644 index 000000000..f731ceb86 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/IdGeneratorsITBase.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.shared; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; +import org.neo4j.driver.types.Node; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Michael J. Simons + */ +@ExtendWith(SpringExtension.class) +@ExtendWith(Neo4jExtension.class) +public abstract class IdGeneratorsITBase { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + protected static final String EXISTING_THING_NAME = "An old name"; + + protected static final String ID_OF_EXISTING_THING = "not-generated."; + + private final Driver driver; + + protected IdGeneratorsITBase(Driver driver) { + this.driver = driver; + } + + @BeforeEach + protected void setupData() { + try (Transaction transaction = driver.session().beginTransaction()) { + transaction.run("MATCH (n) detach delete n"); + transaction.run("CREATE (t:ThingWithGeneratedId {name: $name, theId: $theId}) RETURN id(t) as id", + Values.parameters("name", EXISTING_THING_NAME, "theId", ID_OF_EXISTING_THING)); + transaction.success(); + } + } + + protected void verifyDatabase(String id, String name) { + + try (Session session = driver.session()) { + Node node = session + .run("MATCH (t) WHERE t.theId = $theId RETURN t", + Values.parameters("theId", id)) + .single().get("t").asNode(); + + assertThat(node.get("name").asString()).isEqualTo(name); + assertThat(node.get("theId").asString()).isEqualTo(id); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ImmutableAuditableThing.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ImmutableAuditableThing.java index f36ef60aa..2f587fa61 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ImmutableAuditableThing.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ImmutableAuditableThing.java @@ -24,13 +24,14 @@ import lombok.experimental.Wither; import java.time.LocalDateTime; -import org.neo4j.springframework.data.core.schema.Node; +import org.neo4j.springframework.data.core.schema.GeneratedValue; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.Persistent; /** * @author Michael J. Simons @@ -38,10 +39,10 @@ import org.springframework.data.annotation.PersistenceConstructor; @Value @Wither @AllArgsConstructor(onConstructor = @__(@PersistenceConstructor)) -@Node +@Persistent public class ImmutableAuditableThing { - @Id Long id; + @Id @GeneratedValue Long id; @CreatedDate LocalDateTime createdAt; @CreatedBy String createdBy; @LastModifiedDate LocalDateTime modifiedAt; diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithAllConstructor.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithAllConstructor.java index ec632baf3..73e126f4f 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithAllConstructor.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithAllConstructor.java @@ -29,6 +29,7 @@ import java.time.LocalDate; import java.util.List; import org.neo4j.driver.types.Point; +import org.neo4j.springframework.data.core.schema.GeneratedValue; import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.core.schema.Property; @@ -45,7 +46,7 @@ import org.neo4j.springframework.data.core.schema.Property; @EqualsAndHashCode public class PersonWithAllConstructor { - @Id + @Id @GeneratedValue @Wither private final Long id; diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java index 1c42b37ed..9f777ed39 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithNoConstructor.java @@ -22,6 +22,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.neo4j.springframework.data.core.schema.GeneratedValue; import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.Node; import org.neo4j.springframework.data.core.schema.Property; @@ -35,7 +36,8 @@ import org.neo4j.springframework.data.core.schema.Property; @ToString public class PersonWithNoConstructor { - @Id private Long id; + @Id @GeneratedValue + private Long id; private String name; diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java index 2031be2a6..2de1334b7 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithWither.java @@ -22,6 +22,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.neo4j.springframework.data.core.schema.GeneratedValue; import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.Node; @@ -35,7 +36,8 @@ import org.neo4j.springframework.data.core.schema.Node; @ToString public class PersonWithWither { - @Id private final Long id; + @Id @GeneratedValue + private final Long id; private final String name; diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/TestSequenceGenerator.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/TestSequenceGenerator.java new file mode 100644 index 000000000..166dc2264 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/TestSequenceGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.shared; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.neo4j.springframework.data.core.schema.IdGenerator; +import org.springframework.util.StringUtils; + +/** + * This is a naive sequence generator which must not be used in production. + * + * @author Michael J. Simons + */ +public class TestSequenceGenerator implements IdGenerator { + + private final AtomicInteger sequence = new AtomicInteger(0); + + @Override + public String generateId(String primaryLabel, Object entity) { + return StringUtils.uncapitalize(primaryLabel) + "-" + sequence.incrementAndGet(); + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithAssignedId.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithAssignedId.java index 4d3453de5..4417a77f6 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithAssignedId.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithAssignedId.java @@ -18,24 +18,20 @@ */ package org.neo4j.springframework.data.integration.shared; -import static org.neo4j.springframework.data.core.schema.Id.Strategy.*; - import org.neo4j.springframework.data.core.schema.Id; import org.neo4j.springframework.data.core.schema.Node; /** - * Has an assigned id and a changeable name. + * Has an assigned id. * * @author Michael J. Simons */ @Node("Thing") -public class ThingWithAssignedId { +public class ThingWithAssignedId extends AbstractNamedThing { - @Id(strategy = ASSIGNED) + @Id private final String theId; - private String name; - public ThingWithAssignedId(String theId) { this.theId = theId; } @@ -43,12 +39,4 @@ public class ThingWithAssignedId { public String getTheId() { return theId; } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithGeneratedId.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithGeneratedId.java new file mode 100644 index 000000000..94a9e51d0 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithGeneratedId.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.shared; + +import org.neo4j.springframework.data.core.schema.GeneratedValue; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; + +/** + * @author Michael J. Simons + */ +@Node +public class ThingWithGeneratedId extends AbstractNamedThing { + + @Id @GeneratedValue(TestSequenceGenerator.class) + private String theId; + + public ThingWithGeneratedId(String name) { + super.setName(name); + } + + public String getTheId() { + return theId; + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithIdGeneratedByBean.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithIdGeneratedByBean.java new file mode 100644 index 000000000..24418c642 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/ThingWithIdGeneratedByBean.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.shared; + +import org.neo4j.springframework.data.core.schema.GeneratedValue; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; + +/** + * @author Michael J. Simons + */ +@Node +public class ThingWithIdGeneratedByBean extends AbstractNamedThing { + + @Id @GeneratedValue(generatorRef = "aFancyIdGenerator") + private String theId; + + public ThingWithIdGeneratedByBean(String name) { + this.setName(name); + } + + public String getTheId() { + return theId; + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/IdPopulatorTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/IdPopulatorTest.java new file mode 100644 index 000000000..e3ae73dee --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/IdPopulatorTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.repository.event; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; +import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; +import org.neo4j.springframework.data.core.schema.GeneratedValue; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.IdDescription; +import org.neo4j.springframework.data.core.schema.IdGenerator; +import org.neo4j.springframework.data.core.schema.Node; + +@ExtendWith(MockitoExtension.class) +class IdPopulatorTest { + + @Mock + private Neo4jMappingContext neo4jMappingContext; + + @Mock + private Neo4jPersistentEntity nodeDescription; + + @Test + void shouldRejectNullMappingContext() { + Assertions.assertThatIllegalArgumentException().isThrownBy(() -> new IdPopulator(null)) + .withMessage("A mapping context is required."); + } + + @Test + void shouldRejectNullEntity() { + IdPopulator idPopulator = new IdPopulator(neo4jMappingContext); + Assertions.assertThatIllegalArgumentException().isThrownBy(() -> idPopulator.populateIfNecessary(null)) + .withMessage("Entity may not be null!"); + } + + @Test + void shouldIgnoreInternalIdGenerator() { + + IdDescription toBeReturned = IdDescription.forInternallyGeneratedIds(); + doReturn(toBeReturned).when(nodeDescription).getIdDescription(); + doReturn(nodeDescription).when(neo4jMappingContext).getRequiredPersistentEntity(Sample.class); + + IdPopulator idPopulator = new IdPopulator(neo4jMappingContext); + Sample sample = new Sample(); + + assertThat(idPopulator.populateIfNecessary(sample)).isSameAs(sample); + + verify(nodeDescription).getIdDescription(); + verify(neo4jMappingContext).getRequiredPersistentEntity(Sample.class); + + verifyNoMoreInteractions(nodeDescription, neo4jMappingContext); + } + + @Test + void shouldNotActOnAssignedProperty() { + + IdPopulator idPopulator = new IdPopulator(new Neo4jMappingContext()); + Sample sample = new Sample(); + sample.theId = "something"; + + Sample populatedSample = (Sample) idPopulator.populateIfNecessary(sample); + assertThat(populatedSample).isSameAs(sample); + assertThat(populatedSample.theId).isEqualTo("something"); + } + + @Test + void shouldInvokeGenerator() { + + IdPopulator idPopulator = new IdPopulator(new Neo4jMappingContext()); + Sample sample = new Sample(); + + Sample populatedSample = (Sample) idPopulator.populateIfNecessary(sample); + assertThat(populatedSample).isSameAs(sample); + assertThat(populatedSample.theId).isEqualTo("Not necessary unique."); + } + + @Node + static class Sample { + + @Id @GeneratedValue(DummyIdGenerator.class) + private String theId; + } + + static class DummyIdGenerator implements IdGenerator { + + @Override + public String generateId(String primaryLabel, Object entity) { + return "Not necessary unique."; + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ImmutableSample.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ImmutableSample.java index e7f588bcd..bc8ba0cf3 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ImmutableSample.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ImmutableSample.java @@ -18,8 +18,6 @@ */ package org.neo4j.springframework.data.repository.event; -import static org.neo4j.springframework.data.core.schema.Id.Strategy.*; - import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.Value; @@ -27,8 +25,8 @@ import lombok.experimental.Wither; import java.util.Date; -import org.neo4j.springframework.data.core.schema.Id; import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; /** @@ -40,7 +38,7 @@ import org.springframework.data.annotation.LastModifiedDate; @NoArgsConstructor(force = true) public class ImmutableSample { - @Id(strategy = ASSIGNED) String id; + @Id String id; @CreatedDate Date created; @LastModifiedDate Date modified; } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallbackTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallbackTest.java index bfb106b38..902b5d4fc 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallbackTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/ReactiveAuditingBeforeBindCallbackTest.java @@ -71,7 +71,8 @@ class ReactiveAuditingBeforeBindCallbackTest { .expectNextMatches(s -> { Sample auditedObject = (Sample) s; return auditedObject.created != null && auditedObject.modified != null; - }); + }) + .verifyComplete(); verify(spyOnHandler, times(1)).markCreated(sample); verify(spyOnHandler, times(0)).markModified(any()); @@ -89,7 +90,8 @@ class ReactiveAuditingBeforeBindCallbackTest { .expectNextMatches(s -> { Sample auditedObject = (Sample) s; return auditedObject.created == null && auditedObject.modified != null; - }); + }) + .verifyComplete(); verify(spyOnHandler, times(0)).markCreated(any()); verify(spyOnHandler, times(1)).markModified(sample); diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/Sample.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/Sample.java index 06e76ccac..2f0314a4f 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/Sample.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/event/Sample.java @@ -18,12 +18,10 @@ */ package org.neo4j.springframework.data.repository.event; -import static org.neo4j.springframework.data.core.schema.Id.Strategy.*; - import java.util.Date; -import org.neo4j.springframework.data.core.schema.Id; import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.Version; @@ -32,7 +30,7 @@ import org.springframework.data.annotation.Version; */ class Sample { - @Id(strategy = ASSIGNED) String id; + @Id String id; @Version Long version; @CreatedDate Date created; @LastModifiedDate Date modified; diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/query/RepositoryQueryTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/query/RepositoryQueryTest.java index e2b965d5a..8024527ec 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/query/RepositoryQueryTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/repository/query/RepositoryQueryTest.java @@ -33,6 +33,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.neo4j.driver.Values; import org.neo4j.driver.types.Point; +import org.neo4j.springframework.data.core.schema.GeneratedValue; import org.springframework.data.annotation.Id; import org.springframework.data.mapping.MappingException; import org.neo4j.springframework.data.core.Neo4jClient; @@ -243,7 +244,7 @@ final class RepositoryQueryTest { } static class TestEntity { - @Id + @Id @GeneratedValue private Long id; } diff --git a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/shared/KotlinPerson.kt b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/shared/KotlinPerson.kt index f325ab0b0..263095299 100644 --- a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/shared/KotlinPerson.kt +++ b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/shared/KotlinPerson.kt @@ -19,6 +19,7 @@ package org.neo4j.springframework.data.integration.shared +import org.neo4j.springframework.data.core.schema.GeneratedValue import org.neo4j.springframework.data.core.schema.Id import org.neo4j.springframework.data.core.schema.Node @@ -26,4 +27,4 @@ import org.neo4j.springframework.data.core.schema.Node * @author Gerrit Meier */ @Node -data class KotlinPerson(@Id val id: Long, val name: String) +data class KotlinPerson(@Id @GeneratedValue val id: Long, val name: String) From 8e29fc80f275218ec40952f62cdb21f12b9bad14 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 11 Jul 2019 18:20:24 +0200 Subject: [PATCH 148/342] Prevent mixing different databases in one client transaction. --- .../data/core/DefaultNeo4jClient.java | 13 +- .../data/core/DefaultReactiveNeo4jClient.java | 9 +- .../data/core/Neo4jClient.java | 22 +- .../transaction/Neo4jTransactionHolder.java | 5 +- .../transaction/Neo4jTransactionManager.java | 12 +- .../transaction/Neo4jTransactionUtils.java | 12 +- .../ReactiveNeo4jTransactionHolder.java | 4 + .../ReactiveNeo4jTransactionManager.java | 4 +- .../data/core/Neo4jClientTest.java | 41 ++- .../data/core/ReactiveNeo4jClientTest.java | 41 ++- .../MixedDatabasesTransactionIT.java | 176 +++++++++++++ .../reactive/ReactiveIdGeneratorsIT.java | 8 +- .../ReactiveMixedDatabasesTransactionIT.java | 235 ++++++++++++++++++ 13 files changed, 557 insertions(+), 25 deletions(-) create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/MixedDatabasesTransactionIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveMixedDatabasesTransactionIT.java diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java index 9518de531..49b7bbf81 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java @@ -19,6 +19,7 @@ package org.neo4j.springframework.data.core; import static java.util.stream.Collectors.*; +import static org.neo4j.springframework.data.core.Neo4jClient.*; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager.*; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; @@ -68,7 +69,7 @@ class DefaultNeo4jClient implements Neo4jClient { this.typeSystem = driver.defaultTypeSystem(); } - AutoCloseableStatementRunner getStatementRunner(final String targetDatabase) { + AutoCloseableStatementRunner getStatementRunner(@Nullable final String targetDatabase) { StatementRunner statementRunner = retrieveTransaction(driver, targetDatabase) .map(StatementRunner.class::cast) @@ -198,7 +199,8 @@ class DefaultNeo4jClient implements Neo4jClient { @Override public RunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) { - this.targetDatabase = targetDatabase; + + this.targetDatabase = verifyDatabaseName(targetDatabase); return this; } @@ -329,7 +331,7 @@ class DefaultNeo4jClient implements Neo4jClient { private final Function> callback; - private String targetDatabase; + @Nullable private String targetDatabase; DefaultRunnableDelegation(Function> callback) { this(callback, null); @@ -341,8 +343,9 @@ class DefaultNeo4jClient implements Neo4jClient { } @Override - public RunnableDelegation in(@SuppressWarnings("HiddenField") String targetDatabase) { - this.targetDatabase = targetDatabase; + public RunnableDelegation in(@Nullable @SuppressWarnings("HiddenField") String targetDatabase) { + + this.targetDatabase = verifyDatabaseName(targetDatabase); return this; } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java index bf85b11c7..8c83b48fb 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java @@ -18,6 +18,7 @@ */ package org.neo4j.springframework.data.core; +import static org.neo4j.springframework.data.core.Neo4jClient.*; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; import static org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager.*; @@ -138,7 +139,8 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { @Override public ReactiveRunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) { - this.targetDatabase = targetDatabase; + + this.targetDatabase = verifyDatabaseName(targetDatabase); return this; } @@ -301,8 +303,9 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } @Override - public ReactiveRunnableDelegation in(@SuppressWarnings("HiddenField") String targetDatabase) { - this.targetDatabase = targetDatabase; + public ReactiveRunnableDelegation in(@Nullable @SuppressWarnings("HiddenField") String targetDatabase) { + + this.targetDatabase = verifyDatabaseName(targetDatabase); return this; } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java index fe1bd26f8..890f45dd2 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java @@ -103,12 +103,13 @@ public interface Neo4jClient { interface RunnableSpec extends RunnableSpecTightToDatabase { /** - * Pins the previously defined query to a specific database. + * Pins the previously defined query to a specific database. A value of {@literal null} chooses the default database. + * The empty string {@literal ""} is not permitted. * * @param targetDatabase selected database to use * @return A runnable query specification that is now tight to a given database. */ - RunnableSpecTightToDatabase in(String targetDatabase); + RunnableSpecTightToDatabase in(@Nullable String targetDatabase); } /** @@ -291,4 +292,21 @@ public interface Neo4jClient { */ T getRequiredSingleResult(); } + + /** + * This is a utility method to verify and sanitize a database name. + * + * @param databaseName The database name to verify and sanitize + * @return A possibly trimmed name of the database. + * @throws IllegalArgumentException when the database name is not allowed with the underlying driver. + */ + static String verifyDatabaseName(String databaseName) { + + String newTargetDatabase = databaseName == null ? null : databaseName.trim(); + if (newTargetDatabase != null && newTargetDatabase.isEmpty()) { + throw new IllegalArgumentException( + "Either use null to indicate the default database or a valid database name. The empty string is not permitted."); + } + return newTargetDatabase; + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java index c8976af1d..d2976688f 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java @@ -56,7 +56,6 @@ class Neo4jTransactionHolder extends ResourceHolderSupport { * @return An optional, ongoing transaction. */ Optional getTransaction(String inDatabase) { - return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? Optional.of(transaction) : Optional.empty(); } @@ -109,4 +108,8 @@ class Neo4jTransactionHolder extends ResourceHolderSupport { return transaction.isOpen(); } + + String getDatabaseName() { + return databaseName; + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java index 9d4567fe7..8cae77382 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java @@ -79,7 +79,8 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager * @return An optional containing a managed transaction or an empty optional if the method hasn't been called inside * an ongoing Spring transaction */ - public static Optional retrieveTransaction(final Driver driver, final String targetDatabase) { + public static Optional retrieveTransaction(final Driver driver, + @Nullable final String targetDatabase) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { return Optional.empty(); @@ -90,8 +91,14 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager .getResource(driver); if (connectionHolder != null) { + Optional optionalOngoingTransaction = connectionHolder.getTransaction(targetDatabase); - return connectionHolder.getTransaction(targetDatabase); + if (optionalOngoingTransaction.isPresent()) { + return optionalOngoingTransaction; + } + + throw new IllegalStateException( + formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase)); } // Otherwise we open a session and synchronize it. @@ -138,7 +145,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager @Override protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { - Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction); TransactionConfig transactionConfig = createTransactionConfigFrom(definition); diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java index 8fa0bfe71..62ccf0a73 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionUtils.java @@ -21,6 +21,7 @@ package org.neo4j.springframework.data.core.transaction; import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Objects; import org.neo4j.driver.AccessMode; import org.neo4j.driver.TransactionConfig; @@ -88,11 +89,18 @@ public final class Neo4jTransactionUtils { } static boolean namesMapToTheSameDatabase(@Nullable String name1, @Nullable String name2) { - - return name1 == null && name2 == null || (name1 != null && name1.equals(name2)); + return Objects.equals(name1, name2); } + static String formatOngoingTxInAnotherDbErrorMessage(String currentDb, String requestedDb) { + String defaultDatabase = "the default database"; + String _currentDb = currentDb == null ? defaultDatabase : String.format("'%s'", currentDb); + String _requestedDb = requestedDb == null ? defaultDatabase : String.format("'%s'", requestedDb); + return String.format("There is already an ongoing Spring transaction for %s, but you request %s", _currentDb, + _requestedDb); + + } private Neo4jTransactionUtils() { } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java index ade47bbb7..90bf127d2 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java @@ -81,4 +81,8 @@ class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport { throw new UnsupportedOperationException(); } + + String getDatabaseName() { + return databaseName; + } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 83b4651c4..54a5e33cc 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -99,7 +99,9 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction }); }); }) - .map(connectionHolder -> connectionHolder.getTransaction(targetDatabase).get()) + .map(connectionHolder -> connectionHolder.getTransaction(targetDatabase) + .orElseThrow(() -> new IllegalStateException( + formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase)))) // If not, than just don't open a transaction .onErrorResume(NoTransactionException.class, nte -> Mono.empty()); } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java index 16ed606d6..e075e4b59 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/Neo4jClientTest.java @@ -37,7 +37,6 @@ import java.util.stream.Stream; import org.assertj.core.matcher.AssertionMatcher; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -83,7 +82,6 @@ class Neo4jClientTest { @Mock private Record record2; - @BeforeEach void prepareMocks() { when(driver.session(any(SessionConfig.class))).thenReturn(session); @@ -99,6 +97,8 @@ class Neo4jClientTest { @DisplayName("Creation of queries and binding parameters should feel natural") void queryCreationShouldFeelGood() { + prepareMocks(); + when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.stream()).thenReturn(Stream.of(record1, record2)); @@ -141,6 +141,8 @@ class Neo4jClientTest { @Test void databaseSelectionShouldBePossibleOnlyOnce() { + prepareMocks(); + when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.stream()).thenReturn(Stream.of(record1, record2)); @@ -168,6 +170,27 @@ class Neo4jClientTest { verify(session).close(); } + @Test + void databaseSelectionShouldPreventIllegalValues() { + + Neo4jClient client = Neo4jClient.create(driver); + + assertThat(client.query("RETURN 1").in(null)).isNotNull(); + assertThat(client.query("RETURN 1").in("foobar")).isNotNull(); + + String[] invalidDatabaseNames = { "", " ", "\t" }; + for (String invalidDatabaseName : invalidDatabaseNames) { + assertThatIllegalArgumentException() + .isThrownBy(() -> client.delegateTo(r -> Optional.empty()).in(invalidDatabaseName)); + } + + for (String invalidDatabaseName : invalidDatabaseNames) { + assertThatIllegalArgumentException().isThrownBy(() -> client.query("RETURN 1").in(invalidDatabaseName)); + } + + verify(driver).defaultTypeSystem(); + } + @Nested @DisplayName("Callback handling should feel good") class CallbackHandlingShouldFeelGood { @@ -175,6 +198,8 @@ class Neo4jClientTest { @Test void withDefaultDatabase() { + prepareMocks(); + Neo4jClient client = Neo4jClient.create(driver); Optional result = client .delegateTo(runner -> Optional.of(42)) @@ -190,6 +215,8 @@ class Neo4jClientTest { @Test void withDatabase() { + prepareMocks(); + Neo4jClient client = Neo4jClient.create(driver); Optional result = client .delegateTo(runner -> Optional.of(42)) @@ -211,6 +238,8 @@ class Neo4jClientTest { @Test void reading() { + prepareMocks(); + when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.stream()).thenReturn(Stream.of(record1)); when(record1.get("name")).thenReturn(Values.value("michael")); @@ -244,6 +273,8 @@ class Neo4jClientTest { @Test void shouldApplyNullChecksDuringReading() { + prepareMocks(); + when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.stream()).thenReturn(Stream.of(record1, record2)); when(record1.get("name")).thenReturn(Values.value("michael")); @@ -272,6 +303,8 @@ class Neo4jClientTest { @Test void writing() { + prepareMocks(); + when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.consume()).thenReturn(resultSummary); @@ -301,6 +334,8 @@ class Neo4jClientTest { @DisplayName("Some automatic conversion is ok") void automaticConversion() { + prepareMocks(); + when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.hasNext()).thenReturn(true); when(statementResult.single()).thenReturn(record1); @@ -330,6 +365,8 @@ class Neo4jClientTest { @DisplayName("Queries that return nothing should fit in") void queriesWithoutResultShouldFitInAsWell() { + prepareMocks(); + when(session.run(anyString(), anyMap())).thenReturn(statementResult); when(statementResult.consume()).thenReturn(resultSummary); diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java index edbc53fa7..f3d8d4481 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/ReactiveNeo4jClientTest.java @@ -34,7 +34,6 @@ import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -89,7 +88,6 @@ class ReactiveNeo4jClientTest { @Mock private Record record2; - @BeforeEach void prepareMocks() { when(driver.defaultTypeSystem()).thenReturn(typeSystem); @@ -109,6 +107,8 @@ class ReactiveNeo4jClientTest { @DisplayName("Creation of queries and binding parameters should feel natural") void queryCreationShouldFeelGood() { + prepareMocks(); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); @@ -156,6 +156,8 @@ class ReactiveNeo4jClientTest { @Test void databaseSelectionShouldBePossibleOnlyOnce() { + prepareMocks(); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); @@ -187,6 +189,27 @@ class ReactiveNeo4jClientTest { verify(session).close(); } + @Test + void databaseSelectionShouldPreventIllegalValues() { + + ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); + + assertThat(client.query("RETURN 1").in(null)).isNotNull(); + assertThat(client.query("RETURN 1").in("foobar")).isNotNull(); + + String[] invalidDatabaseNames = { "", " ", "\t" }; + for (String invalidDatabaseName : invalidDatabaseNames) { + assertThatIllegalArgumentException() + .isThrownBy(() -> client.delegateTo(r -> Mono.empty()).in(invalidDatabaseName)); + } + + for (String invalidDatabaseName : invalidDatabaseNames) { + assertThatIllegalArgumentException().isThrownBy(() -> client.query("RETURN 1").in(invalidDatabaseName)); + } + + verify(driver).defaultTypeSystem(); + } + @Nested @DisplayName("Callback handling should feel good") class CallbackHandlingShouldFeelGood { @@ -194,6 +217,8 @@ class ReactiveNeo4jClientTest { @Test void withDefaultDatabase() { + prepareMocks(); + when(transaction.commit()).thenReturn(Mono.empty()); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -215,6 +240,8 @@ class ReactiveNeo4jClientTest { @Test void withDatabase() { + prepareMocks(); + when(transaction.commit()).thenReturn(Mono.empty()); ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver); @@ -242,6 +269,8 @@ class ReactiveNeo4jClientTest { @Test void reading() { + prepareMocks(); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1)); @@ -279,6 +308,8 @@ class ReactiveNeo4jClientTest { @Test void shouldApplyNullChecksDuringReading() { + prepareMocks(); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.rollback()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1, record2)); @@ -313,6 +344,8 @@ class ReactiveNeo4jClientTest { @Test void writing() { + prepareMocks(); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.summary()).thenReturn(Mono.just(resultSummary)); @@ -350,6 +383,8 @@ class ReactiveNeo4jClientTest { @DisplayName("Some automatic conversion is ok") void automaticConversion() { + prepareMocks(); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.records()).thenReturn(Flux.just(record1)); @@ -381,6 +416,8 @@ class ReactiveNeo4jClientTest { @DisplayName("Queries that return nothing should fit in") void queriesWithoutResultShouldFitInAsWell() { + prepareMocks(); + when(transaction.run(anyString(), anyMap())).thenReturn(statementResult); when(transaction.commit()).thenReturn(Mono.empty()); when(statementResult.summary()).thenReturn(Mono.just(resultSummary)); diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/MixedDatabasesTransactionIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/MixedDatabasesTransactionIT.java new file mode 100644 index 000000000..13b2bb193 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/MixedDatabasesTransactionIT.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.imperative; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.exceptions.DatabaseException; +import org.neo4j.driver.internal.SessionConfig; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager; +import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +/** + * The goal of this integration tests is to ensure a sensible coexistence of declarative {@link Transactional @Transactional} + * transaction when the user uses the {@link Neo4jClient} in the same or another database. + */ +@ExtendWith(SpringExtension.class) +@ExtendWith(Neo4jExtension.class) +@ContextConfiguration(classes = MixedDatabasesTransactionIT.Config.class) +public class MixedDatabasesTransactionIT { + + protected static final String DATABASE_NAME = "boom"; + public static final String TEST_QUERY = "MATCH (n:DbTest) RETURN COUNT(n)"; + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private final Driver driver; + + private final Neo4jClient neo4jClient; + + private final TransactionTemplate transactionTemplate; + + private final PersonRepository repository; + + @Autowired + public MixedDatabasesTransactionIT(Driver driver, Neo4jClient neo4jClient, + Neo4jTransactionManager neo4jTransactionManager, + PersonRepository repository) { + this.driver = driver; + this.neo4jClient = neo4jClient; + this.transactionTemplate = new TransactionTemplate(neo4jTransactionManager); + this.repository = repository; + } + + @BeforeEach + protected void setupDatabase() { + + try (Session session = driver.session(SessionConfig.forDatabase("system"))) { + session.run("DROP DATABASE " + DATABASE_NAME); + } catch (DatabaseException e) { + // Database does probably not exist + } + + try (Session session = driver.session(SessionConfig.forDatabase("system"))) { + session.run("CREATE DATABASE " + DATABASE_NAME); + } + + try (Session session = driver.session(SessionConfig.forDatabase(DATABASE_NAME))) { + session.run("CREATE (n:DbTest) RETURN n"); + } + } + + @Test + void withoutActiveTransactions() { + + Optional numberOfNodes = + neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one(); + + assertThat(numberOfNodes).isPresent().hasValue(1L); + } + + @Transactional + @Test + void usingTheSameDatabaseDeclarative() { + + Optional numberOfNodes = + neo4jClient.query(TEST_QUERY).in(null).fetchAs(Long.class).one(); + + assertThat(numberOfNodes).isPresent().hasValue(0L); + } + + @Test + void usingSameDatabaseExplizitTx() { + Neo4jTransactionManager otherTransactionManger = new Neo4jTransactionManager(driver, DATABASE_NAME); + TransactionTemplate otherTransactionTemplate = new TransactionTemplate(otherTransactionManger); + + Optional numberOfNodes = otherTransactionTemplate.execute( + tx -> neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one()); + assertThat(numberOfNodes).isPresent().hasValue(1L); + } + + @Test + @Transactional + void usingAnotherDatabaseDeclarative() { + + assertThatIllegalStateException().isThrownBy( + () -> neo4jClient.query("MATCH (n) RETURN COUNT(n)").in(DATABASE_NAME).fetchAs(Long.class).one()) + .withMessage("There is already an ongoing Spring transaction for the default database, but you request 'boom'"); + + } + + @Test + void usingAnotherDatabaseExplizitTx() { + + assertThatIllegalStateException().isThrownBy( + () -> transactionTemplate.execute( + tx -> neo4jClient.query("MATCH (n) RETURN COUNT(n)").in(DATABASE_NAME).fetchAs(Long.class).one())) + .withMessage("There is already an ongoing Spring transaction for the default database, but you request 'boom'"); + } + + @Test + void usingAnotherDatabaseDeclarativeFromRepo() { + + Neo4jTransactionManager otherTransactionManger = new Neo4jTransactionManager(driver, DATABASE_NAME); + TransactionTemplate otherTransactionTemplate = new TransactionTemplate(otherTransactionManger); + + assertThatIllegalStateException().isThrownBy( + () -> otherTransactionTemplate.execute( + tx -> repository.save(new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, + LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null)))) + .withMessage("There is already an ongoing Spring transaction for 'boom', but you request the default database"); + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java index bccf02cba..72de53449 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java @@ -75,12 +75,12 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { List savedThings = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); transactionalOperator - .execute(t -> thingsWithGeneratedIds.save(new ThingWithGeneratedId("Foobar"))) + .execute(t -> thingsWithGeneratedIds.save(new ThingWithGeneratedId("WrapperService"))) .as(StepVerifier::create) .recordWith(() -> savedThings) .consumeNextWith(savedThing -> { - assertThat(savedThing.getName()).isEqualTo("Foobar"); + assertThat(savedThing.getName()).isEqualTo("WrapperService"); assertThat(savedThing.getTheId()) .isNotBlank() .matches("thingWithGeneratedId-\\d+"); @@ -96,12 +96,12 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { List savedThings = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(transactionManager); transactionalOperator - .execute(t -> thingsWithBeanGeneratedIds.save(new ThingWithIdGeneratedByBean("Foobar"))) + .execute(t -> thingsWithBeanGeneratedIds.save(new ThingWithIdGeneratedByBean("WrapperService"))) .as(StepVerifier::create) .recordWith(() -> savedThings) .consumeNextWith(savedThing -> { - assertThat(savedThing.getName()).isEqualTo("Foobar"); + assertThat(savedThing.getName()).isEqualTo("WrapperService"); assertThat(savedThing.getTheId()).isEqualTo("ReactiveID."); }) .verifyComplete(); diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveMixedDatabasesTransactionIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveMixedDatabasesTransactionIT.java new file mode 100644 index 000000000..fcfd196ad --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveMixedDatabasesTransactionIT.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed 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 + * + * https://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 org.neo4j.springframework.data.integration.reactive; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.exceptions.DatabaseException; +import org.neo4j.driver.internal.SessionConfig; +import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; +import org.neo4j.springframework.data.core.Neo4jClient; +import org.neo4j.springframework.data.core.ReactiveNeo4jClient; +import org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager; +import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; +import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; + +/** + * The goal of this integration tests is to ensure a sensible coexistence of declarative {@link Transactional @Transactional} + * transaction when the user uses the {@link Neo4jClient} in the same or another database. + */ +@ExtendWith(SpringExtension.class) +@ExtendWith(Neo4jExtension.class) +@ContextConfiguration(classes = ReactiveMixedDatabasesTransactionIT.Config.class) +public class ReactiveMixedDatabasesTransactionIT { + + protected static final String DATABASE_NAME = "boom"; + public static final String TEST_QUERY = "MATCH (n:DbTest) RETURN COUNT(n)"; + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private final Driver driver; + + private final ReactiveNeo4jClient neo4jClient; + + private final ReactiveNeo4jTransactionManager neo4jTransactionManager; + + private final ReactivePersonRepository repository; + + private final WrapperService wrappingComponent; + + @Autowired + public ReactiveMixedDatabasesTransactionIT(Driver driver, ReactiveNeo4jClient neo4jClient, + ReactiveNeo4jTransactionManager neo4jTransactionManager, + ReactivePersonRepository repository, + WrapperService wrappingComponent) { + this.driver = driver; + this.neo4jClient = neo4jClient; + this.neo4jTransactionManager = neo4jTransactionManager; + this.repository = repository; + this.wrappingComponent = wrappingComponent; + } + + @BeforeEach + protected void setupDatabase() { + + try (Session session = driver.session(SessionConfig.forDatabase("system"))) { + session.run("DROP DATABASE " + DATABASE_NAME); + } catch (DatabaseException e) { + // Database does probably not exist + } + + try (Session session = driver.session(SessionConfig.forDatabase("system"))) { + session.run("CREATE DATABASE " + DATABASE_NAME); + } + + try (Session session = driver.session(SessionConfig.forDatabase(DATABASE_NAME))) { + session.run("CREATE (n:DbTest) RETURN n"); + } + } + + @Test + void withoutActiveTransactions() { + + Mono numberOfNodes = + neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one(); + + StepVerifier + .create(numberOfNodes) + .expectNext(1L) + .verifyComplete(); + } + + @Test + void usingTheSameDatabaseDeclarative() { + + StepVerifier + .create(wrappingComponent.usingTheSameDatabaseDeclarative()) + .expectNext(0L) + .verifyComplete(); + } + + @Test + void usingSameDatabaseExplizitTx() { + ReactiveNeo4jTransactionManager otherTransactionManger = new ReactiveNeo4jTransactionManager(driver, + DATABASE_NAME); + TransactionalOperator otherTransactionTemplate = TransactionalOperator.create(otherTransactionManger); + + Mono numberOfNodes = neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one() + .as(otherTransactionTemplate::transactional); + + StepVerifier + .create(numberOfNodes) + .expectNext(1L) + .verifyComplete(); + } + + @Test + void usingAnotherDatabaseDeclarative() { + + StepVerifier + .create(wrappingComponent.usingAnotherDatabaseDeclarative()) + .expectErrorMatches(e -> + e instanceof IllegalStateException && e.getMessage().equals( + "There is already an ongoing Spring transaction for the default database, but you request 'boom'")) + .verify(); + } + + @Test + void usingAnotherDatabaseExplizitTx() { + + TransactionalOperator transactionTemplate = TransactionalOperator.create(neo4jTransactionManager); + + Mono numberOfNodes = neo4jClient.query("MATCH (n) RETURN COUNT(n)").in(DATABASE_NAME).fetchAs(Long.class) + .one() + .as(transactionTemplate::transactional); + + StepVerifier + .create(numberOfNodes) + .expectErrorMatches(e -> + e instanceof IllegalStateException && e.getMessage().equals( + "There is already an ongoing Spring transaction for the default database, but you request 'boom'")) + .verify(); + } + + @Test + void usingAnotherDatabaseDeclarativeFromRepo() { + + ReactiveNeo4jTransactionManager otherTransactionManger = new ReactiveNeo4jTransactionManager(driver, + DATABASE_NAME); + TransactionalOperator otherTransactionTemplate = TransactionalOperator.create(otherTransactionManger); + + Mono p = + repository.save(new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, + LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null)) + .as(otherTransactionTemplate::transactional); + + StepVerifier + .create(p) + .expectErrorMatches(e -> + e instanceof IllegalStateException && e.getMessage().equals( + "There is already an ongoing Spring transaction for 'boom', but you request the default database")) + .verify(); + } + + /** + * We need this wrapper service, as reactive {@link Transactional @Transactional} annoted methods are not + * recognized as such (See other also https://github.com/spring-projects/spring-framework/issues/23277). + * + * The class must be public to make the declarative transactions work. Please don't change its visibility. + */ + public static class WrapperService { + + private final ReactiveNeo4jClient neo4jClient; + + WrapperService(ReactiveNeo4jClient neo4jClient) { + this.neo4jClient = neo4jClient; + } + + @Transactional + public Mono usingTheSameDatabaseDeclarative() { + + return neo4jClient.query(TEST_QUERY).in(null).fetchAs(Long.class).one(); + } + + @Transactional + public Mono usingAnotherDatabaseDeclarative() { + return neo4jClient.query(TEST_QUERY).in(DATABASE_NAME).fetchAs(Long.class).one(); + } + } + + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.openConnection(); + } + + @Override + protected Collection getMappingBasePackages() { + return Collections.singletonList(PersonWithAllConstructor.class.getPackage().getName()); + } + + @Bean + public WrapperService wrapperService(ReactiveNeo4jClient reactiveNeo4jClient) { + return new WrapperService(reactiveNeo4jClient); + } + } +} From 92291eb22a46b0429c006db32dd5b634efca1fbc Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 12 Jul 2019 11:57:15 +0200 Subject: [PATCH 149/342] Use mocked driver for database selection tests. Multidatabase is an enterprise feature, so that test requires an enterprise image. We decided against an availibily guard as the transaction handling should be solid, regardless of edition. It also makes the tests faster. --- ...TransactionManagerMixedDatabasesTest.java} | 87 ++++++++++++------ ...TransactionManagerMixedDatabasesTest.java} | 91 +++++++++++++------ 2 files changed, 122 insertions(+), 56 deletions(-) rename spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/{MixedDatabasesTransactionIT.java => TransactionManagerMixedDatabasesTest.java} (65%) rename spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/{ReactiveMixedDatabasesTransactionIT.java => ReactiveTransactionManagerMixedDatabasesTest.java} (68%) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/MixedDatabasesTransactionIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/TransactionManagerMixedDatabasesTest.java similarity index 65% rename from spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/MixedDatabasesTransactionIT.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/TransactionManagerMixedDatabasesTest.java index 13b2bb193..54fb8dbb5 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/MixedDatabasesTransactionIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/TransactionManagerMixedDatabasesTest.java @@ -19,25 +19,29 @@ package org.neo4j.springframework.data.integration.imperative; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import java.time.LocalDate; import java.util.Collection; import java.util.Collections; +import java.util.Map; import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; import org.neo4j.driver.Session; -import org.neo4j.driver.exceptions.DatabaseException; +import org.neo4j.driver.StatementResult; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.Values; import org.neo4j.driver.internal.SessionConfig; import org.neo4j.springframework.data.config.AbstractNeo4jConfig; import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager; import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; -import org.neo4j.springframework.data.test.Neo4jExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -48,19 +52,19 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; /** - * The goal of this integration tests is to ensure a sensible coexistence of declarative {@link Transactional @Transactional} + * The goal of this tests is to ensure a sensible coexistence of declarative {@link Transactional @Transactional} * transaction when the user uses the {@link Neo4jClient} in the same or another database. + *

+ * While it does not integrate against a real database (multidatabase is an enterprise feature), it is still an integration + * test due to the high integration with Spring framework code. */ @ExtendWith(SpringExtension.class) -@ExtendWith(Neo4jExtension.class) -@ContextConfiguration(classes = MixedDatabasesTransactionIT.Config.class) -public class MixedDatabasesTransactionIT { +@ContextConfiguration(classes = TransactionManagerMixedDatabasesTest.Config.class) +public class TransactionManagerMixedDatabasesTest { protected static final String DATABASE_NAME = "boom"; public static final String TEST_QUERY = "MATCH (n:DbTest) RETURN COUNT(n)"; - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - private final Driver driver; private final Neo4jClient neo4jClient; @@ -70,7 +74,7 @@ public class MixedDatabasesTransactionIT { private final PersonRepository repository; @Autowired - public MixedDatabasesTransactionIT(Driver driver, Neo4jClient neo4jClient, + public TransactionManagerMixedDatabasesTest(Driver driver, Neo4jClient neo4jClient, Neo4jTransactionManager neo4jTransactionManager, PersonRepository repository) { this.driver = driver; @@ -79,24 +83,6 @@ public class MixedDatabasesTransactionIT { this.repository = repository; } - @BeforeEach - protected void setupDatabase() { - - try (Session session = driver.session(SessionConfig.forDatabase("system"))) { - session.run("DROP DATABASE " + DATABASE_NAME); - } catch (DatabaseException e) { - // Database does probably not exist - } - - try (Session session = driver.session(SessionConfig.forDatabase("system"))) { - session.run("CREATE DATABASE " + DATABASE_NAME); - } - - try (Session session = driver.session(SessionConfig.forDatabase(DATABASE_NAME))) { - session.run("CREATE (n:DbTest) RETURN n"); - } - } - @Test void withoutActiveTransactions() { @@ -165,7 +151,50 @@ public class MixedDatabasesTransactionIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + + Record boomRecord = mock(Record.class); + when(boomRecord.size()).thenReturn(1); + when(boomRecord.get(0)).thenReturn(Values.value(1L)); + + Record defaultRecord = mock(Record.class); + when(defaultRecord.size()).thenReturn(1); + when(defaultRecord.get(0)).thenReturn(Values.value(0L)); + + StatementResult boomStatementResult = mock(StatementResult.class); + when(boomStatementResult.hasNext()).thenReturn(true); + when(boomStatementResult.single()).thenReturn(boomRecord); + + StatementResult defaultStatementResult = mock(StatementResult.class); + when(defaultStatementResult.hasNext()).thenReturn(true); + when(defaultStatementResult.single()).thenReturn(defaultRecord); + + Transaction boomTransaction = mock(Transaction.class); + when(boomTransaction.run(eq(TEST_QUERY), any(Map.class))).thenReturn(boomStatementResult); + when(boomTransaction.isOpen()).thenReturn(true); + + Transaction defaultTransaction = mock(Transaction.class); + when(defaultTransaction.run(eq(TEST_QUERY), any(Map.class))).thenReturn(defaultStatementResult); + when(defaultTransaction.isOpen()).thenReturn(true); + + Session boomSession = mock(Session.class); + when(boomSession.run(eq(TEST_QUERY), any(Map.class))).thenReturn(boomStatementResult); + when(boomSession.beginTransaction(any(TransactionConfig.class))).thenReturn(boomTransaction); + when(boomSession.isOpen()).thenReturn(true); + + Session defaultSession = mock(Session.class); + when(defaultSession.run(eq(TEST_QUERY), any(Map.class))).thenReturn(defaultStatementResult); + when(defaultSession.beginTransaction(any(TransactionConfig.class))).thenReturn(defaultTransaction); + when(defaultSession.isOpen()).thenReturn(true); + + Driver driver = mock(Driver.class); + when(driver.session()).thenReturn(defaultSession); + when(driver.session(any(SessionConfig.class))).then(invocation -> { + SessionConfig sessionConfig = invocation.getArgument(0); + return sessionConfig.database().map(n -> n.equals(DATABASE_NAME) ? boomSession : defaultSession) + .orElse(defaultSession); + }); + + return driver; } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveMixedDatabasesTransactionIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveTransactionManagerMixedDatabasesTest.java similarity index 68% rename from spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveMixedDatabasesTransactionIT.java rename to spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveTransactionManagerMixedDatabasesTest.java index fcfd196ad..48ee249e3 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveMixedDatabasesTransactionIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveTransactionManagerMixedDatabasesTest.java @@ -18,20 +18,27 @@ */ package org.neo4j.springframework.data.integration.reactive; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.LocalDate; import java.util.Collection; import java.util.Collections; +import java.util.Map; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.exceptions.DatabaseException; +import org.neo4j.driver.Record; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.Values; import org.neo4j.driver.internal.SessionConfig; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxStatementResult; +import org.neo4j.driver.reactive.RxTransaction; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; import org.neo4j.springframework.data.core.Neo4jClient; import org.neo4j.springframework.data.core.ReactiveNeo4jClient; @@ -49,13 +56,15 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.reactive.TransactionalOperator; /** - * The goal of this integration tests is to ensure a sensible coexistence of declarative {@link Transactional @Transactional} + * The goal of this tests is to ensure a sensible coexistence of declarative {@link Transactional @Transactional} * transaction when the user uses the {@link Neo4jClient} in the same or another database. + *

+ * While it does not integrate against a real database (multidatabase is an enterprise feature), it is still an integration + * test due to the high integration with Spring framework code. */ @ExtendWith(SpringExtension.class) -@ExtendWith(Neo4jExtension.class) -@ContextConfiguration(classes = ReactiveMixedDatabasesTransactionIT.Config.class) -public class ReactiveMixedDatabasesTransactionIT { +@ContextConfiguration(classes = ReactiveTransactionManagerMixedDatabasesTest.Config.class) +public class ReactiveTransactionManagerMixedDatabasesTest { protected static final String DATABASE_NAME = "boom"; public static final String TEST_QUERY = "MATCH (n:DbTest) RETURN COUNT(n)"; @@ -73,7 +82,7 @@ public class ReactiveMixedDatabasesTransactionIT { private final WrapperService wrappingComponent; @Autowired - public ReactiveMixedDatabasesTransactionIT(Driver driver, ReactiveNeo4jClient neo4jClient, + public ReactiveTransactionManagerMixedDatabasesTest(Driver driver, ReactiveNeo4jClient neo4jClient, ReactiveNeo4jTransactionManager neo4jTransactionManager, ReactivePersonRepository repository, WrapperService wrappingComponent) { @@ -84,24 +93,6 @@ public class ReactiveMixedDatabasesTransactionIT { this.wrappingComponent = wrappingComponent; } - @BeforeEach - protected void setupDatabase() { - - try (Session session = driver.session(SessionConfig.forDatabase("system"))) { - session.run("DROP DATABASE " + DATABASE_NAME); - } catch (DatabaseException e) { - // Database does probably not exist - } - - try (Session session = driver.session(SessionConfig.forDatabase("system"))) { - session.run("CREATE DATABASE " + DATABASE_NAME); - } - - try (Session session = driver.session(SessionConfig.forDatabase(DATABASE_NAME))) { - session.run("CREATE (n:DbTest) RETURN n"); - } - } - @Test void withoutActiveTransactions() { @@ -219,7 +210,53 @@ public class ReactiveMixedDatabasesTransactionIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + + Record boomRecord = mock(Record.class); + when(boomRecord.size()).thenReturn(1); + when(boomRecord.get(0)).thenReturn(Values.value(1L)); + + Record defaultRecord = mock(Record.class); + when(defaultRecord.size()).thenReturn(1); + when(defaultRecord.get(0)).thenReturn(Values.value(0L)); + + RxStatementResult boomStatementResult = mock(RxStatementResult.class); + when(boomStatementResult.records()).thenReturn(Mono.just(boomRecord)); + + RxStatementResult defaultStatementResult = mock(RxStatementResult.class); + when(defaultStatementResult.records()).thenReturn(Mono.just(defaultRecord)); + + RxTransaction boomTransaction = mock(RxTransaction.class); + when(boomTransaction.run(eq(TEST_QUERY), any(Map.class))).thenReturn(boomStatementResult); + when(boomTransaction.commit()).thenReturn(Mono.empty()); + when(boomTransaction.rollback()).thenReturn(Mono.empty()); + + RxTransaction defaultTransaction = mock(RxTransaction.class); + when(defaultTransaction.run(eq(TEST_QUERY), any(Map.class))).thenReturn(defaultStatementResult); + when(defaultTransaction.commit()).thenReturn(Mono.empty()); + when(defaultTransaction.rollback()).thenReturn(Mono.empty()); + + RxSession boomSession = mock(RxSession.class); + when(boomSession.run(eq(TEST_QUERY), any(Map.class))).thenReturn(boomStatementResult); + when(boomSession.beginTransaction()).thenReturn(Mono.just(boomTransaction)); + when(boomSession.beginTransaction(any(TransactionConfig.class))).thenReturn(Mono.just(boomTransaction)); + when(boomSession.close()).thenReturn(Mono.empty()); + + RxSession defaultSession = mock(RxSession.class); + when(defaultSession.run(eq(TEST_QUERY), any(Map.class))).thenReturn(defaultStatementResult); + when(defaultSession.beginTransaction()).thenReturn(Mono.just(defaultTransaction)); + when(defaultSession.beginTransaction(any(TransactionConfig.class))) + .thenReturn(Mono.just(defaultTransaction)); + when(defaultSession.close()).thenReturn(Mono.empty()); + + Driver driver = mock(Driver.class); + when(driver.rxSession()).thenReturn(defaultSession); + when(driver.rxSession(any(SessionConfig.class))).then(invocation -> { + SessionConfig sessionConfig = invocation.getArgument(0); + return sessionConfig.database().map(n -> n.equals(DATABASE_NAME) ? boomSession : defaultSession) + .orElse(defaultSession); + }); + + return driver; } @Override From 493f4bdc4294c46ca336affe38c0647035ba2365 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 12 Jul 2019 14:17:31 +0200 Subject: [PATCH 150/342] Introduce JUnit 5 tags for various types of tests. This introduces some constants to mark tests as tests that need reactive support and to distinguish between tests that run only on the commercial or community edition. The Neo4jExtension assumes on the requirements of the tags, so those tags have a dual use: They can be used to filter for tests, i.e. setup a pipeline that does only reactive tests on community edition. They make sure the project is easy to build for external contributors and people who might not have access to latest edition by assuming on the requirements and prevent the tests from failing in conditions where the tests are bound to fail. --- .../integration/imperative/AuditingIT.java | 2 +- .../integration/imperative/CallbacksIT.java | 2 +- .../imperative/IdGeneratorsIT.java | 2 +- .../integration/imperative/RepositoryIT.java | 2 +- .../data/integration/kotlin/KotlinIT.java | 2 +- .../reactive/ReactiveAuditingIT.java | 5 +- .../reactive/ReactiveCallbacksIT.java | 6 +- .../reactive/ReactiveIdGeneratorsIT.java | 5 +- .../reactive/ReactiveRepositoryIT.java | 7 +- .../data/test/Neo4jExtension.java | 164 ++++++++++++++++-- 10 files changed, 170 insertions(+), 27 deletions(-) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java index 08a266ecc..17e68894c 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java @@ -98,7 +98,7 @@ class AuditingIT extends AuditingITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java index 5f2c31186..721e36025 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java @@ -93,7 +93,7 @@ class CallbacksIT extends CallbacksITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java index f9feb4162..16a7e0a51 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java @@ -131,7 +131,7 @@ class IdGeneratorsIT extends IdGeneratorsITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java index 65562fc24..8f5d33c30 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java @@ -1025,7 +1025,7 @@ class RepositoryIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java index 8e87609a1..5f6ef6da5 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java @@ -84,7 +84,7 @@ class KotlinIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } public Collection getMappingBasePackages() { diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java index f8e0d822d..3b1a33ee1 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java @@ -19,6 +19,7 @@ package org.neo4j.springframework.data.integration.reactive; import static org.assertj.core.api.Assertions.*; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -29,6 +30,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; @@ -50,6 +52,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; /** * @author Michael J. Simons */ +@Tag(NEEDS_REACTIVE_SUPPORT) @ContextConfiguration(classes = ReactiveAuditingIT.Config.class) class ReactiveAuditingIT extends AuditingITBase { @@ -125,7 +128,7 @@ class ReactiveAuditingIT extends AuditingITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java index 0322202af..1cebd24bc 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java @@ -18,6 +18,8 @@ */ package org.neo4j.springframework.data.integration.reactive; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -28,6 +30,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; @@ -47,6 +50,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; /** * @author Michael J. Simons */ +@Tag(NEEDS_REACTIVE_SUPPORT) @ContextConfiguration(classes = ReactiveCallbacksIT.Config.class) class ReactiveCallbacksIT extends CallbacksITBase { @@ -148,7 +152,7 @@ class ReactiveCallbacksIT extends CallbacksITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java index 72de53449..da46380c0 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java @@ -20,6 +20,7 @@ package org.neo4j.springframework.data.integration.reactive; import static java.util.stream.Collectors.*; import static org.assertj.core.api.Assertions.*; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -32,6 +33,7 @@ import java.util.List; import java.util.Set; import java.util.stream.IntStream; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; @@ -52,6 +54,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; /** * @author Michael J. Simons */ +@Tag(NEEDS_REACTIVE_SUPPORT) @ContextConfiguration(classes = ReactiveIdGeneratorsIT.Config.class) class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { @@ -171,7 +174,7 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index 011d64680..63daf023d 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -19,6 +19,7 @@ package org.neo4j.springframework.data.integration.reactive; import static org.neo4j.driver.Values.*; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; @@ -34,6 +35,7 @@ import java.util.List; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; @@ -47,7 +49,7 @@ import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructo import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; import org.neo4j.springframework.data.test.Neo4jExtension; -import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; +import org.neo4j.springframework.data.test.Neo4jExtension.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -64,6 +66,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; * @author Gerrit Meier * @author Michael J. Simons */ +@Tag(NEEDS_REACTIVE_SUPPORT) @ExtendWith(Neo4jExtension.class) @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = ReactiveRepositoryIT.Config.class) @@ -725,7 +728,7 @@ class ReactiveRepositoryIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java index feb83b636..28c472669 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java @@ -18,21 +18,33 @@ */ package org.neo4j.springframework.data.test; +import static org.assertj.core.api.Assumptions.*; + import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.util.ReflectionUtils; +import org.neo4j.driver.AccessMode; import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Logging; +import org.neo4j.driver.Session; +import org.neo4j.driver.internal.SessionConfig; +import org.neo4j.driver.internal.util.ServerVersion; import org.testcontainers.containers.Neo4jContainer; /** @@ -45,12 +57,21 @@ import org.testcontainers.containers.Neo4jContainer; * @since 1.0 */ @Slf4j -public class Neo4jExtension implements BeforeAllCallback { +public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { + + public final static String NEEDS_REACTIVE_SUPPORT = "reactiveTest"; + public final static String COMMUNITY_EDITION_ONLY = "communityEdition"; + public final static String COMMERCIAL_EDITION_ONLY = "commercialEdition"; private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace .create(Neo4jExtension.class); - private static final String KEY = "neo4j.standalone"; + private static final String KEY_NEO4J_INSTANCE = "neo4j.standalone"; + private static final String KEY_DRIVER_INSTANCE = "neo4j.driver"; + + private static Set COMMUNITY_EDITION_INDICATOR = Collections.singleton("community"); + + private static Set COMMERCIAL_EDITION_INDICATOR = new HashSet<>(Arrays.asList("commercial", "enterprise")); @Override public void beforeAll(ExtensionContext context) throws Exception { @@ -66,30 +87,65 @@ public class Neo4jExtension implements BeforeAllCallback { String neo4jUrl = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_URL")).orElse(""); String neo4jPassword = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_PASSWORD")).orElse(""); - Neo4jConnectionSupport neo4jConnectionSupport; - if (!(neo4jUrl.isEmpty() || neo4jPassword.isEmpty())) { - log.warn("Using Neo4j instance at {}.", neo4jUrl); - neo4jConnectionSupport = new Neo4jConnectionSupport(neo4jUrl, - AuthTokens.basic("neo4j", neo4jPassword)); - } else { - log.warn("Using Neo4j test container."); - ExtensionContext.Store store = context.getStore(NAMESPACE); - ContainerAdapter adapter = store - .getOrComputeIfAbsent(KEY, key -> new Neo4jExtension.ContainerAdapter(), ContainerAdapter.class); - adapter.start(); - neo4jConnectionSupport = new Neo4jConnectionSupport(adapter.getBoltUrl(), AuthTokens.none()); + ExtensionContext.Store contextStore = context.getStore(NAMESPACE); + Neo4jConnectionSupport neo4jConnectionSupport = contextStore + .get(KEY_DRIVER_INSTANCE, Neo4jConnectionSupport.class); + + if (neo4jConnectionSupport == null) { + if (!(neo4jUrl.isEmpty() || neo4jPassword.isEmpty())) { + log.warn("Using Neo4j instance at {}.", neo4jUrl); + neo4jConnectionSupport = new Neo4jConnectionSupport(neo4jUrl, AuthTokens.basic("neo4j", neo4jPassword)); + } else { + log.warn("Using Neo4j test container."); + ContainerAdapter adapter = contextStore + .getOrComputeIfAbsent(KEY_NEO4J_INSTANCE, key -> new Neo4jExtension.ContainerAdapter(), + ContainerAdapter.class); + adapter.start(); + neo4jConnectionSupport = new Neo4jConnectionSupport(adapter.getBoltUrl(), AuthTokens.none()); + } + contextStore.put(KEY_DRIVER_INSTANCE, neo4jConnectionSupport); } + checkRequiredFeatures(neo4jConnectionSupport, context.getTags()); + Field field = injectableFields.get(0); field.setAccessible(true); field.set(null, neo4jConnectionSupport); } + @Override + public void beforeEach(ExtensionContext context) throws Exception { + ExtensionContext.Store contextStore = context.getStore(NAMESPACE); + Neo4jConnectionSupport neo4jConnectionSupport = contextStore + .get(KEY_DRIVER_INSTANCE, Neo4jConnectionSupport.class); + checkRequiredFeatures(neo4jConnectionSupport, context.getTags()); + } + + private void checkRequiredFeatures(Neo4jConnectionSupport neo4jConnectionSupport, Set tags) { + if (tags.contains(NEEDS_REACTIVE_SUPPORT)) { + assumeThat(neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.v4_0_0)) + .describedAs("This test requires at least Neo4j 4.0 for reactive database connectivity.") + .isTrue(); + } + + if (tags.contains(COMMUNITY_EDITION_ONLY)) { + assumeThat(neo4jConnectionSupport.isCommunityEdition()) + .describedAs("This test should be run on the community edition only") + .isTrue(); + } + + if (tags.contains(COMMERCIAL_EDITION_ONLY)) { + assumeThat(neo4jConnectionSupport.isCommercialEdition()) + .describedAs("This test should be run on the commercial edition only") + .isTrue(); + } + } + /** * Support class that holds the connection information and opens a new connection on demand. * @since 1.0 */ - public static class Neo4jConnectionSupport { + public static class Neo4jConnectionSupport implements ExtensionContext.Store.CloseableResource { public final String url; @@ -97,14 +153,88 @@ public class Neo4jExtension implements BeforeAllCallback { public final Config config; + public volatile ServerVersion cachedServerVersion; + + /** + * Shared instance of the standard (non-routing) driver. + */ + public volatile Driver driverInstance; + public Neo4jConnectionSupport(String url, AuthToken authToken) { this.url = url; this.authToken = authToken; this.config = Config.builder().withLogging(Logging.slf4j()).build(); } - public Driver openConnection() { - return GraphDatabase.driver(url, authToken, config); + /** + * @return A possible shared driver instance, connected to either a database running inside test containers or + * running locally. + */ + public Driver getDriver() { + + Driver driver = this.driverInstance; + if (driver == null) { + synchronized (this) { + driver = this.driverInstance; + if (driver == null) { + this.driverInstance = GraphDatabase.driver(url, authToken, config); + driver = this.driverInstance; + } + } + } + return driver; + } + + ServerVersion getServerVersion() { + + ServerVersion serverVersion = this.cachedServerVersion; + if (serverVersion == null) { + synchronized (this) { + serverVersion = this.cachedServerVersion; + if (serverVersion == null) { + this.cachedServerVersion = ServerVersion.version(getDriver()); + serverVersion = this.cachedServerVersion; + } + } + } + + return serverVersion; + } + + String getEdition() { + String edition = "n/a"; + SessionConfig sessionConfig = SessionConfig.builder().withDefaultAccessMode(AccessMode.READ).build(); + try (Session session = getDriver().session( + sessionConfig)) { + edition = session.run("call dbms.components() yield edition").single().get("edition").asString(); + } + return edition.toLowerCase(Locale.ENGLISH); + } + + boolean isCommunityEdition() { + + return COMMUNITY_EDITION_INDICATOR.contains(getEdition()); + } + + boolean isCommercialEdition() { + + return COMMERCIAL_EDITION_INDICATOR.contains(getEdition()); + } + + @Override + public void close() { + + // Don't open up a driver for just closing it + if (driverInstance == null) { + return; + } + + // Catch all the things... The driver has been closed maybe by a Spring Context already + try { + log.debug("Closing Neo4j connection support."); + driverInstance.close(); + } catch (Exception e) { + } } } From cb3a3affe94ecd83d937fab1ab15fd8424b080d7 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 12 Jul 2019 15:19:10 +0200 Subject: [PATCH 151/342] Allow to accept the enterprise license during tests through an environment variable. --- .../data/test/Neo4jExtension.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java index 28c472669..2b785bf74 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java @@ -69,6 +69,11 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { private static final String KEY_NEO4J_INSTANCE = "neo4j.standalone"; private static final String KEY_DRIVER_INSTANCE = "neo4j.driver"; + private static final String SYS_PROPERTY_NEOJ4_URL = "SDN_RX_NEO4J_URL"; + private static final String SYS_PROPERTY_NEO4J_PASSWORD = "SDN_RX_NEO4J_PASSWORD"; + private static final String SYS_PROPERTY_NEO4J_ACCEPT_COMMERCIAL_EDITION = "SDN_RX_NEO4J_ACCEPT_COMMERCIAL_EDITION"; + private static final String SYS_PROPERTY_NEO4J_VERSION = "SDN_RX_NEO4J_VERSION"; + private static Set COMMUNITY_EDITION_INDICATOR = Collections.singleton("community"); private static Set COMMERCIAL_EDITION_INDICATOR = new HashSet<>(Arrays.asList("commercial", "enterprise")); @@ -84,8 +89,8 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { return; } - String neo4jUrl = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_URL")).orElse(""); - String neo4jPassword = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_PASSWORD")).orElse(""); + String neo4jUrl = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEOJ4_URL)).orElse(""); + String neo4jPassword = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_PASSWORD)).orElse(""); ExtensionContext.Store contextStore = context.getStore(NAMESPACE); Neo4jConnectionSupport neo4jConnectionSupport = contextStore @@ -240,10 +245,11 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { static class ContainerAdapter implements ExtensionContext.Store.CloseableResource { - private final String imageName = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_VERSION")).orElse("3.5.0"); + private final String imageVersion = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_VERSION)).orElse("3.5.6"); - private final Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:" + imageName) - .withoutAuthentication(); + private final Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:" + imageVersion) + .withoutAuthentication() + .withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_ACCEPT_COMMERCIAL_EDITION)).orElse("no")); public String getBoltUrl() { return neo4jContainer.getBoltUrl(); From e8f66fe83d0e97f74708fd88c28071825cfd0e6a Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 17 Jul 2019 09:57:22 +0200 Subject: [PATCH 152/342] Add CONTRIBUTING based on drivers repo. --- CONTRIBUTING.adoc | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 CONTRIBUTING.adoc diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 000000000..9acc1d080 --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,80 @@ += Contributing to the Neo4j Ecosystem +:sectanchors: + +At http://neo4j.com/[Neo4j], we develop our software in the open at +GitHub. This provides transparency for you, our users, and allows you to +fork the software to make your own additions and enhancements. We also +provide areas specifically for community contributions, in particular +the https://github.com/neo4j-contrib[neo4j-contrib] space. + +There's an active https://community.neo4j.com/[Neo4j Online Community] +where we work directly with the community. If you're not already a +member, sign up! + +We love our community and wouldn't be where we are without you. + +== Need to raise an issue? + +Where you raise an issue depends largely on the nature of the problem. + +Firstly, if you are an Enterprise customer, you might want to head over +to our http://support.neo4j.com/[Customer Support Portal]. + +There are plenty of public channels available too, though. If you simply +want to get started or have a question on how to use a particular +feature, ask a question in https://community.neo4j.com/[Neo4j Online +Community]. If you think you might have hit a bug in our software (it +happens occasionally!) or you have specific feature request then use the +issue feature on the relevant GitHub repository. Check first though as +someone else may have already raised something similar. + +http://stackoverflow.com/questions/tagged/neo4j[StackOverflow] also +hosts a ton of questions and might already have a discussion around your +problem. Make sure you have a look there too. + +Include as much information as you can in any request you make: + +* Which versions of our products are you using? +* Which language (and which version of that language) are you developing +with? +* What operating system are you on? +* Are you working with a cluster or on a single machine? +* What code are you running? +* What errors are you seeing? +* What solutions have you tried already? + +=== Want to contribute? + +If you want to contribute a pull request, we have a little bit of +process you'll need to follow: + +* Do all your work in a personal fork of the original repository +* https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request[Rebase], +don't merge (we prefer to keep our history clean) +* Create a branch (with a useful name) for your contribution +* Make sure you're familiar with the appropriate coding style (this +varies by language so ask if you're in doubt) +* Include unit tests if appropriate (obviously not necessary for +documentation changes) +* Take a moment to read and sign our http://neo4j.com/developer/cla[CLA] + +We can't guarantee that we'll accept pull requests and may ask you to +make some changes before they go in. Occasionally, we might also have +logistical, commercial, or legal reasons why we can't accept your work +but we'll try to find an alternative way for you to contribute in that +case. Remember that many community members have become regular +contributors and some are now even Neo employees! + +== Got an idea for a new project? + +If you have an idea for a new tool or library, start by talking to other +people in the community. Chances are that someone has a similar idea or +may have already started working on it. The best software comes from +getting like minds together to solve a problem. And we'll do our best to +help you promote and co-ordinate your Neo ecosystem projects. + +== Further reading + +If you want to find out more about how you can contribute, head over to +our website for http://neo4j.com/developer/contributing-code/[more +information]. \ No newline at end of file From 158e80f4f0140b0045d67aac275b17fc7d5b3052 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 17 Jul 2019 12:15:24 +0200 Subject: [PATCH 153/342] Use CI friendly version numbers. See https://maven.apache.org/maven-ci-friendly.html for reference. In addition, all examples are now independ from the main project. They use the same technique for the dependencies as the neo4j-java-driver-spring-boot-starter. When run without the version numbers set, they define their own and recreate the dependencies version based on that. When the first alpha has been released, the changelist attribute should reflect that, too. --- .gitignore | 2 +- pom.xml | 38 ++++++++++++++++++++++++++++++++++++-- spring-data-neo4j/pom.xml | 2 +- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 0b7821537..950ff63d9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,4 @@ target/ ### Misc ### .DS_Store - +.flattened-pom.xml diff --git a/pom.xml b/pom.xml index 4da593feb..7605b2d13 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ org.neo4j.springframework.data spring-data-neo4j-rx-parent - 1.0.0.BUILD-SNAPSHOT + ${revision}${sha1}${changelist} pom Spring Data Neo4j RX @@ -74,13 +74,16 @@ spring-data-neo4j-rx spring-data-neo4j-rx-spring-boot-starter-parent - examples + examples/reactive-web + examples/mapping 1.0.0 3.11.1 + -SNAPSHOT 8.21 + 1.1.0 0.8.2 1.8 1.6.0 @@ -103,8 +106,10 @@ UTF-8 1.2.1 Californium-SR4 + 1.0.0 1.3.8 2.2.5 + 1.7.26 2.2.0.RC1 1.10.7 @@ -318,6 +323,11 @@ + + org.codehaus.mojo + flatten-maven-plugin + ${flatten-maven-plugin.version} + @@ -393,6 +403,30 @@ + + org.codehaus.mojo + flatten-maven-plugin + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index c66fa68d3..bd5f2dcd9 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -23,7 +23,7 @@ spring-data-neo4j-rx-parent org.neo4j.springframework.data - 1.0.0.BUILD-SNAPSHOT + ${revision}${sha1}${changelist} spring-data-neo4j-rx From aaecb5712d770906f8e5f7dcadcd0413ed7805dc Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 17 Jul 2019 18:10:23 +0200 Subject: [PATCH 154/342] Prevent usage of shaded libraries. --- etc/checkstyle/config.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/etc/checkstyle/config.xml b/etc/checkstyle/config.xml index ea3eefad8..4cd90d2a2 100644 --- a/etc/checkstyle/config.xml +++ b/etc/checkstyle/config.xml @@ -56,7 +56,13 @@ - + + + + + From 960438fe5d95d01bacaca16266033ced1034b1fb Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 19 Jul 2019 23:03:19 +0200 Subject: [PATCH 155/342] Remove usage of java.util.Optional from some important methods. --- .../core/mapping/Neo4jMappingContext.java | 57 +++++++++++-------- .../data/core/schema/Schema.java | 47 +++++++-------- .../core/schema/UnknownEntityException.java | 2 +- .../repository/query/PartTreeNeo4jQuery.java | 2 +- .../query/ReactivePartTreeNeo4jQuery.java | 2 +- .../query/ReactiveStringBasedNeo4jQuery.java | 2 +- .../query/StringBasedNeo4jQuery.java | 2 +- .../support/SimpleQueryByExampleExecutor.java | 2 +- .../SimpleReactiveQueryByExampleExecutor.java | 2 +- .../core/mapping/Neo4jMappingContextTest.java | 17 +++--- 10 files changed, 66 insertions(+), 69 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java index fe315c65d..58758bcc8 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java @@ -31,7 +31,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Predicate; import java.util.function.Supplier; import org.apiguardian.api.API; @@ -44,6 +43,7 @@ import org.neo4j.springframework.data.core.schema.NodeDescription; import org.neo4j.springframework.data.core.schema.Relationship; import org.neo4j.springframework.data.core.schema.RelationshipDescription; import org.neo4j.springframework.data.core.schema.Schema; +import org.neo4j.springframework.data.core.schema.UnknownEntityException; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ListableBeanFactory; @@ -130,12 +130,13 @@ public final class Neo4jMappingContext newEntity, label.orElse("n/a"))); } - this.getNodeDescription(newEntity.getUnderlyingClass()).ifPresent(existingDescription -> { + NodeDescription existingDescription = this.getNodeDescription(newEntity.getUnderlyingClass()); + if (existingDescription != null) { throw new MappingException(String.format(Locale.ENGLISH, "The schema already contains description with the underlying class %s under the primary label %s", newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel())); - }); + } this.nodeDescriptionsByPrimaryLabel.put(primaryLabel, newEntity); @@ -154,22 +155,30 @@ public final class Neo4jMappingContext } @Override - public Optional> getNodeDescription(String primaryLabel) { - return Optional.ofNullable(this.nodeDescriptionsByPrimaryLabel.get(primaryLabel)); + @Nullable + public NodeDescription getNodeDescription(String primaryLabel) { + return this.nodeDescriptionsByPrimaryLabel.get(primaryLabel); } NodeDescription getRequiredNodeDescription(String primaryLabel) { - return this.getNodeDescription(primaryLabel) - .orElseThrow( - () -> new MappingException( - String.format("Required node description not found with primary label '%s'", primaryLabel))); + + NodeDescription nodeDescription = this.getNodeDescription(primaryLabel); + if (nodeDescription == null) { + throw new MappingException( + String.format("Required node description not found with primary label '%s'", primaryLabel)); + } + return nodeDescription; } @Override - public Optional> getNodeDescription(Class underlyingClass) { + public NodeDescription getNodeDescription(Class underlyingClass) { - Predicate underlyingClassMatches = n -> n.getUnderlyingClass().equals(underlyingClass); - return this.nodeDescriptionsByPrimaryLabel.values().stream().filter(underlyingClassMatches).findFirst(); + for (NodeDescription nodeDescription : this.nodeDescriptionsByPrimaryLabel.values()) { + if (nodeDescription.getUnderlyingClass().equals(underlyingClass)) { + return nodeDescription; + } + } + return null; } @Override @@ -179,26 +188,24 @@ public final class Neo4jMappingContext } @Override - public Optional> getMappingFunctionFor(Class targetClass) { - - if (!this.hasPersistentEntityFor(targetClass)) { - return Optional.empty(); + @Nullable + public BiFunction getMappingFunctionFor(Class targetClass) { + if (this.hasPersistentEntityFor(targetClass)) { + Neo4jPersistentEntity neo4jPersistentEntity = this.getPersistentEntity(targetClass); + return new DefaultNeo4jMappingFunction(instantiators, neo4jPersistentEntity); } - - return this.getNodeDescription(targetClass) - .map(Neo4jPersistentEntity.class::cast) - .map(neo4jPersistentEntity -> new DefaultNeo4jMappingFunction<>(instantiators, neo4jPersistentEntity)); + return null; } @Override - public Optional>> getBinderFunctionFor(Class sourceClass) { + public Function> getRequiredBinderFunctionFor(Class sourceClass) { + if (!this.hasPersistentEntityFor(sourceClass)) { - return Optional.empty(); + throw new UnknownEntityException(sourceClass); } - return this.getNodeDescription(sourceClass) - .map(Neo4jPersistentEntity.class::cast) - .map(neo4jPersistentEntity -> new DefaultNeo4jBinderFunction<>(neo4jPersistentEntity)); + Neo4jPersistentEntity neo4jPersistentEntity = this.getPersistentEntity(sourceClass); + return new DefaultNeo4jBinderFunction(neo4jPersistentEntity); } private Collection computeRelationshipsOf(String primaryLabel) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java index 4ac3546e7..484a720fc 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java @@ -28,6 +28,7 @@ import java.util.function.Function; import org.apiguardian.api.API; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; +import org.springframework.lang.Nullable; /** * Contains the descriptions of all nodes, their properties and relationships known to SDN-RX. @@ -54,26 +55,24 @@ public interface Schema { * Retrieves a nodes description by its primary label. * * @param primaryLabel The primary label under which the node is described - * @return The description if any + * @return The description if any, null otherwise */ - Optional> getNodeDescription(String primaryLabel); + @Nullable NodeDescription getNodeDescription(String primaryLabel); /** * Retrieves a nodes description by its underlying class. * * @param underlyingClass The underlying class of the node description to be retrieved - * @return The description if any + * @return The description if any, null otherwise */ - Optional> getNodeDescription(Class underlyingClass); + @Nullable NodeDescription getNodeDescription(Class underlyingClass); - /** - * @param underlyingClass the type to find in the schema registry - * @return The node description for the given. class - * @throws UnknownEntityException When {@code targetClass} is not a known entity class. - */ default NodeDescription getRequiredNodeDescription(Class underlyingClass) { - - return getNodeDescription(underlyingClass).orElseThrow(() -> new UnknownEntityException(underlyingClass)); + NodeDescription nodeDescription = getNodeDescription(underlyingClass); + if (nodeDescription == null) { + throw new UnknownEntityException(underlyingClass); + } + return nodeDescription; } /** @@ -96,28 +95,20 @@ public interface Schema { * * @param targetClass The target class to which to map to. * @param Type of the target class - * @return An empty optional if the target class is unknown, otherwise an optional containing a stateless, reusable mapping function - */ - Optional> getMappingFunctionFor(Class targetClass); - - /** - * @param targetClass The target class to which to map to. - * @param Type of the target class - * @return The default mapping function for the given target class - * @throws IllegalStateException When {@code targetClass} is not a managed class - * @see #getMappingFunctionFor(Class) + * @return The default, stateless and reusable mapping function for the given target class + * @throws UnknownEntityException When {@code targetClass} is not a managed class */ default BiFunction getRequiredMappingFunctionFor(Class targetClass) { - - return getMappingFunctionFor(targetClass).orElseThrow(() -> new UnknownEntityException(targetClass)); + BiFunction mappingFunction = getMappingFunctionFor(targetClass); + if (mappingFunction == null) { + throw new UnknownEntityException(targetClass); + } + return mappingFunction; } - Optional>> getBinderFunctionFor(Class sourceClass); + @Nullable BiFunction getMappingFunctionFor(Class targetClass); - default Function> getRequiredBinderFunctionFor(Class sourceClass) { - - return getBinderFunctionFor(sourceClass).orElseThrow(() -> new UnknownEntityException(sourceClass)); - } + Function> getRequiredBinderFunctionFor(Class sourceClass); /** * Creates or retrieves an instance of the given id generator class. During the lifetime of the schema, diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/UnknownEntityException.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/UnknownEntityException.java index 318204e0d..7cbd7ce02 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/UnknownEntityException.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/schema/UnknownEntityException.java @@ -32,7 +32,7 @@ public final class UnknownEntityException extends IllegalStateException { private final Class targetClass; - UnknownEntityException(Class targetClass) { + public UnknownEntityException(Class targetClass) { super(String.format("%s is not a known entity", targetClass.getName())); this.targetClass = targetClass; } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java index b8e2fb18e..a14a68193 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/PartTreeNeo4jQuery.java @@ -102,7 +102,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { return PreparedQuery.queryFor(super.domainType).withCypherQuery(cypherQuery) .withParameters(boundedParameters) - .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) + .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType)) .build(); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java index 33c7ccc89..482afb89b 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactivePartTreeNeo4jQuery.java @@ -102,7 +102,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { return PreparedQuery.queryFor(super.domainType).withCypherQuery(cypherQuery) .withParameters(boundedParameters) - .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) + .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType)) .build(); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java index b1e1dc1cd..48ccdbd43 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -162,7 +162,7 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { return PreparedQuery.queryFor(super.domainType) .withCypherQuery(cypherQuery) .withParameters(bindParameters(parameters)) - .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) // Null is fine + .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType)) // Null is fine .build(); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java index 23e192718..a0af0e939 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/StringBasedNeo4jQuery.java @@ -162,7 +162,7 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { return PreparedQuery.queryFor(super.domainType) .withCypherQuery(cypherQuery) .withParameters(bindParameters(parameters)) - .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType).orElse(null)) // Null is fine + .usingMappingFunction(mappingContext.getMappingFunctionFor(super.domainType)) // Null is fine .build(); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java index f1ced3d11..288e374b4 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleQueryByExampleExecutor.java @@ -148,7 +148,7 @@ class SimpleQueryByExampleExecutor implements QueryByExampleExecutor { Map parameters) { BiFunction mappingFunctionToUse - = this.mappingContext.getMappingFunctionFor(resultType).orElse(null); + = this.mappingContext.getMappingFunctionFor(resultType); PreparedQuery preparedQuery = PreparedQuery.queryFor(resultType) .withCypherQuery(renderer.render(statement)) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java index 4967b78c8..ad3d938e5 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveQueryByExampleExecutor.java @@ -123,7 +123,7 @@ class SimpleReactiveQueryByExampleExecutor implements ReactiveQueryByExampleE Map parameters) { BiFunction mappingFunctionToUse - = this.mappingContext.getMappingFunctionFor(resultType).orElse(null); + = this.mappingContext.getMappingFunctionFor(resultType); PreparedQuery preparedQuery = PreparedQuery.queryFor(resultType) .withCypherQuery(renderer.render(statement)) diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java index e0398fa7d..59339ea68 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java @@ -25,7 +25,6 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import org.junit.jupiter.api.Test; import org.neo4j.springframework.data.core.schema.GeneratedValue; @@ -51,10 +50,10 @@ public class Neo4jMappingContextTest { schema.setInitialEntitySet(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class, TripNode.class))); schema.initialize(); - Optional> optionalUserNodeDescription = schema.getNodeDescription("User"); + NodeDescription optionalUserNodeDescription = schema.getNodeDescription("User"); assertThat(optionalUserNodeDescription) - .isPresent() - .hasValueSatisfying(description -> { + .isNotNull() + .satisfies(description -> { assertThat(description.getUnderlyingClass()).isEqualTo(UserNode.class); assertThat(description.getIdDescription().isInternallyGeneratedId()).isTrue(); @@ -68,10 +67,10 @@ public class Neo4jMappingContextTest { .containsExactlyInAnyOrder("id", "name", "firstName"); }); - Optional> optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); + NodeDescription optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); assertThat(optionalBikeNodeDescription) - .isPresent() - .hasValueSatisfying(description -> { + .isNotNull() + .satisfies(description -> { assertThat(description.getUnderlyingClass()).isEqualTo(BikeNode.class); assertThat(description.getIdDescription().isAssignedId()).isTrue(); @@ -121,7 +120,7 @@ public class Neo4jMappingContextTest { void shouldNotProvideMappingForUnknownClasses() { Neo4jMappingContext schema = new Neo4jMappingContext(); - assertThat(schema.getMappingFunctionFor(UserNode.class)).isEmpty(); + assertThat(schema.getMappingFunctionFor(UserNode.class)).isNull(); } @Test @@ -130,7 +129,7 @@ public class Neo4jMappingContextTest { Neo4jMappingContext schema = new Neo4jMappingContext(); Neo4jPersistentEntity bikeNodeEntity = schema.getPersistentEntity(BikeNode.class); bikeNodeEntity.doWithAssociations((Association association) -> - assertThat(schema.getMappingFunctionFor(association.getInverse().getAssociationTargetType())).isPresent()); + assertThat(schema.getMappingFunctionFor(association.getInverse().getAssociationTargetType())).isNotNull()); } @Test From 4b2f7b36016ad01c19cce34567e02e76b3f3c52e Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 19 Jul 2019 18:35:26 +0200 Subject: [PATCH 156/342] Add benchmark projects. There are now 3 benchmarks: SDN as the old baseline, pure bolt as the upper range what's possible without mapping overhead and SDN-RX. Provided are two benchmarks: Simple find by a derived query method and a simple insert. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 950ff63d9..06c2335fd 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ target/ ### Misc ### .DS_Store .flattened-pom.xml +dependency-reduced-pom.xml From d111c143f5523a8273a5f2db0476f30b8684a66b Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 22 Jul 2019 10:24:49 +0200 Subject: [PATCH 157/342] Remove Optional return types from transaction support. --- .../data/core/DefaultNeo4jClient.java | 10 ++++---- .../transaction/Neo4jTransactionHolder.java | 7 +++--- .../transaction/Neo4jTransactionManager.java | 11 ++++---- .../ReactiveNeo4jTransactionHolder.java | 7 +++--- .../ReactiveNeo4jTransactionManager.java | 12 ++++++--- .../repository/query/CypherAdapterUtils.java | 23 +++++++++++------ .../repository/query/CypherQueryCreator.java | 2 +- .../data/repository/support/Predicate.java | 4 +-- .../support/SimpleNeo4jRepository.java | 11 ++++---- .../SimpleReactiveNeo4jRepository.java | 17 ++++++------- .../Neo4jTransactionManagerTest.java | 25 +++++++++---------- 11 files changed, 68 insertions(+), 61 deletions(-) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java index 49b7bbf81..6170f1084 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java @@ -71,9 +71,10 @@ class DefaultNeo4jClient implements Neo4jClient { AutoCloseableStatementRunner getStatementRunner(@Nullable final String targetDatabase) { - StatementRunner statementRunner = retrieveTransaction(driver, targetDatabase) - .map(StatementRunner.class::cast) - .orElseGet(() -> driver.session(defaultSessionConfig(targetDatabase))); + StatementRunner statementRunner = retrieveTransaction(driver, targetDatabase); + if (statementRunner == null) { + statementRunner = driver.session(defaultSessionConfig(targetDatabase)); + } return (AutoCloseableStatementRunner) Proxy.newProxyInstance(StatementRunner.class.getClassLoader(), new Class[] { AutoCloseableStatementRunner.class }, @@ -106,8 +107,7 @@ class DefaultNeo4jClient implements Neo4jClient { } return null; } else { - return cachedHandles.computeIfAbsent(method, this::findHandleFor) - .invokeWithArguments(args); + return cachedHandles.computeIfAbsent(method, this::findHandleFor).invokeWithArguments(args); } } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java index d2976688f..f365dc046 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionHolder.java @@ -20,10 +20,9 @@ package org.neo4j.springframework.data.core.transaction; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUtils.*; -import java.util.Optional; - import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; @@ -55,8 +54,8 @@ class Neo4jTransactionHolder extends ResourceHolderSupport { * @param inDatabase selected database to use * @return An optional, ongoing transaction. */ - Optional getTransaction(String inDatabase) { - return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? Optional.of(transaction) : Optional.empty(); + @Nullable Transaction getTransaction(String inDatabase) { + return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? transaction : null; } void commit() { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java index 8cae77382..dd732f000 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManager.java @@ -22,7 +22,6 @@ import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUt import java.util.Collections; import java.util.List; -import java.util.Optional; import org.apiguardian.api.API; import org.neo4j.driver.Driver; @@ -76,14 +75,14 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager * * @param driver The driver that has been used as a synchronization object. * @param targetDatabase The target database - * @return An optional containing a managed transaction or an empty optional if the method hasn't been called inside + * @return An optional managed transaction or {@literal null} if the method hasn't been called inside * an ongoing Spring transaction */ - public static Optional retrieveTransaction(final Driver driver, + public static @Nullable Transaction retrieveTransaction(final Driver driver, @Nullable final String targetDatabase) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { - return Optional.empty(); + return null; } // Check whether we have a transaction managed by a Neo4j transaction manager @@ -91,9 +90,9 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager .getResource(driver); if (connectionHolder != null) { - Optional optionalOngoingTransaction = connectionHolder.getTransaction(targetDatabase); + Transaction optionalOngoingTransaction = connectionHolder.getTransaction(targetDatabase); - if (optionalOngoingTransaction.isPresent()) { + if (optionalOngoingTransaction != null) { return optionalOngoingTransaction; } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java index 90bf127d2..499eedfab 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionHolder.java @@ -22,10 +22,9 @@ import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionUt import reactor.core.publisher.Mono; -import java.util.Optional; - import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxTransaction; +import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; /** @@ -50,9 +49,9 @@ class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport { return session; } - Optional getTransaction(String inDatabase) { + @Nullable RxTransaction getTransaction(String inDatabase) { - return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? Optional.of(transaction) : Optional.empty(); + return namesMapToTheSameDatabase(this.databaseName, inDatabase) ? transaction : null; } Mono commit() { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java index 54a5e33cc..23a9dfe09 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/transaction/ReactiveNeo4jTransactionManager.java @@ -99,9 +99,15 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction }); }); }) - .map(connectionHolder -> connectionHolder.getTransaction(targetDatabase) - .orElseThrow(() -> new IllegalStateException( - formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase)))) + .map(connectionHolder -> { + RxTransaction transaction = connectionHolder.getTransaction(targetDatabase); + if (transaction == null) { + throw new IllegalStateException( + formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase)); + } + return transaction; + } + ) // If not, than just don't open a transaction .onErrorResume(NoTransactionException.class, nte -> Mono.empty()); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java index a131f1b18..30bdddbf3 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherAdapterUtils.java @@ -23,7 +23,6 @@ import static org.neo4j.springframework.data.core.schema.NodeDescription.*; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.function.Function; import org.apiguardian.api.API; @@ -38,6 +37,7 @@ import org.neo4j.springframework.data.repository.support.Neo4jEntityInformation; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.MappingException; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -114,10 +114,10 @@ public final class CypherAdapterUtils { /** * @param nodeDescription The node description for which a match clause should be generated * @return An ongoing match - * @see #prepareMatchOf(NodeDescription, Optional) + * @see #prepareMatchOf(NodeDescription, Condition) */ public OngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription) { - return prepareMatchOf(nodeDescription, Optional.empty()); + return prepareMatchOf(nodeDescription, null); } /** @@ -133,7 +133,7 @@ public final class CypherAdapterUtils { * @param condition Optional conditions to add * @return An ongoing match */ - public OngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription, Optional condition) { + public OngoingReadingAndWith prepareMatchOf(NodeDescription nodeDescription, @Nullable Condition condition) { Node rootNode = node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); IdDescription idDescription = nodeDescription.getIdDescription(); @@ -142,15 +142,18 @@ public final class CypherAdapterUtils { if (idDescription.isInternallyGeneratedId()) { expressions.add(Functions.id(rootNode).as(NAME_OF_INTERNAL_ID)); } - return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())) + return Cypher.match(rootNode).where(conditionOrNoCondition(condition)) .with(expressions.toArray(new Expression[expressions.size()])); } - public Statement prepareDeleteOf(NodeDescription nodeDescription, - Optional condition) { + public Statement prepareDeleteOf(NodeDescription nodeDescription) { + return prepareDeleteOf(nodeDescription, null); + } + + public Statement prepareDeleteOf(NodeDescription nodeDescription, @Nullable Condition condition) { Node rootNode = node(nodeDescription.getPrimaryLabel()).named(NAME_OF_ROOT_NODE); - return Cypher.match(rootNode).where(condition.orElse(Conditions.noCondition())).delete(rootNode).build(); + return Cypher.match(rootNode).where(conditionOrNoCondition(condition)).delete(rootNode).build(); } public Statement prepareSaveOf(NodeDescription nodeDescription) { @@ -210,6 +213,10 @@ public final class CypherAdapterUtils { } } + private static Condition conditionOrNoCondition(@Nullable Condition condition) { + return condition == null ? Conditions.noCondition() : condition; + } + public static BuildableStatement addPagingParameter( NodeDescription nodeDescription, Pageable pageable, diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java index accb1e30a..fe4c8d577 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/query/CypherQueryCreator.java @@ -123,7 +123,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { protected String complete(Condition condition, Sort sort) { Statement statement = createSchemaBasedStatementBuilder(mappingContext) - .prepareMatchOf(nodeDescription, Optional.ofNullable(condition)) + .prepareMatchOf(nodeDescription, condition) .returning(Cypher.asterisk()) .orderBy( Stream.concat( diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java index da4666c18..3bb265c07 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java @@ -154,8 +154,8 @@ final class Predicate { } StatementBuilder.OngoingReadingAndWith useWithReadingFragment( - BiFunction, Optional, StatementBuilder.OngoingReadingAndWith> readingFragmentSupplier) { - return readingFragmentSupplier.apply(this.nodeDescription, Optional.of(this.condition)); + BiFunction, Condition, StatementBuilder.OngoingReadingAndWith> readingFragmentSupplier) { + return readingFragmentSupplier.apply(this.nodeDescription, this.condition); } private void add(ExampleMatcher.MatchMode matchMode, Condition additionalCondition) { diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java index 9710c4a9b..8fce0f6fd 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleNeo4jRepository.java @@ -171,8 +171,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository public Optional findById(ID id) { Statement statement = statementBuilder - .prepareMatchOf(entityMetaData, - Optional.of(entityInformation.getIdExpression().isEqualTo(literalOf(id)))) + .prepareMatchOf(entityMetaData, entityInformation.getIdExpression().isEqualTo(literalOf(id))) .returning(asterisk()) .build(); return createExecutableQuery(statement).getSingleResult(); @@ -195,7 +194,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository public Iterable findAllById(Iterable ids) { Statement statement = statementBuilder - .prepareMatchOf(entityMetaData, Optional.of(entityInformation.getIdExpression().in((parameter("ids"))))) + .prepareMatchOf(entityMetaData, entityInformation.getIdExpression().in((parameter("ids")))) .returning(asterisk()) .build(); @@ -224,7 +223,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository log.debug("Deleting entity with id {} ", id); - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, condition); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) .bind(id).to(nameOfParameter) .run(); @@ -253,7 +252,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository log.debug("Deleting all entities with the following ids: {} ", ids); - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, condition); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) .bind(ids).to(nameOfParameter) .run(); @@ -268,7 +267,7 @@ class SimpleNeo4jRepository implements PagingAndSortingRepository log.debug("Deleting all nodes with primary label {}", entityMetaData.getPrimaryLabel()); - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.empty()); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).run(); log.debug("Deleted {} nodes and {} relationships.", summary.counters().nodesDeleted(), diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java index 01c458371..043c3574a 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/repository/support/SimpleReactiveNeo4jRepository.java @@ -30,7 +30,6 @@ import reactor.core.publisher.Mono; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.Function; import java.util.stream.StreamSupport; @@ -91,7 +90,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Mono findById(ID id) { Statement statement = statementBuilder - .prepareMatchOf(entityMetaData, Optional.of(entityInformation.getIdExpression().isEqualTo(literalOf(id)))) + .prepareMatchOf(entityMetaData, entityInformation.getIdExpression().isEqualTo(literalOf(id))) .returning(asterisk()) .build(); return createExecutableQuery(statement).getSingleResult(); @@ -104,7 +103,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Flux findAll(Sort sort) { - Statement statement = statementBuilder.prepareMatchOf(entityMetaData, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData) .returning(asterisk()) .orderBy(toSortItems(entityMetaData, sort)) .build(); @@ -124,7 +123,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Flux findAll() { - Statement statement = statementBuilder.prepareMatchOf(entityMetaData, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData) .returning(asterisk()).build(); return createExecutableQuery(statement).getResults(); } @@ -132,7 +131,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Flux findAllById(Iterable ids) { Statement statement = statementBuilder - .prepareMatchOf(entityMetaData, Optional.of(entityInformation.getIdExpression().in((parameter("ids"))))) + .prepareMatchOf(entityMetaData, entityInformation.getIdExpression().in((parameter("ids")))) .returning(asterisk()) .build(); @@ -146,7 +145,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Override public Mono count() { - Statement statement = statementBuilder.prepareMatchOf(entityMetaData, Optional.empty()) + Statement statement = statementBuilder.prepareMatchOf(entityMetaData) .returning(Functions.count(asterisk())).build(); PreparedQuery preparedQuery = PreparedQuery.queryFor(Long.class) @@ -239,7 +238,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< String nameOfParameter = "id"; Condition condition = this.entityInformation.getIdExpression().isEqualTo(parameter(nameOfParameter)); - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, condition); return this.neo4jClient.query(() -> renderer.render(statement)) .bind(id).to(nameOfParameter).run().then(); } @@ -280,7 +279,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< String nameOfParameter = "ids"; Condition condition = entityInformation.getIdExpression().in(parameter(nameOfParameter)); - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.of(condition)); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, condition); List ids = StreamSupport.stream(entities.spliterator(), false) .map(this.entityInformation::getId) @@ -309,7 +308,7 @@ class SimpleReactiveNeo4jRepository implements ReactiveSortingRepository< @Transactional public Mono deleteAll() { - Statement statement = statementBuilder.prepareDeleteOf(entityMetaData, Optional.empty()); + Statement statement = statementBuilder.prepareDeleteOf(entityMetaData); return this.neo4jClient.query(() -> renderer.render(statement)).run().then(); } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java index ed0ed467b..0c3af3939 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/transaction/Neo4jTransactionManagerTest.java @@ -22,7 +22,6 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import static org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager.*; -import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import javax.transaction.Status; @@ -74,9 +73,9 @@ class Neo4jTransactionManagerTest { @Test void shouldWorkWithoutSynchronizations() { - Optional optionalTransaction = retrieveTransaction(driver, databaseName); + Transaction optionalTransaction = retrieveTransaction(driver, databaseName); - assertThat(optionalTransaction).isEmpty(); + assertThat(optionalTransaction).isNull(); verifyZeroInteractions(driver, session, transaction); } @@ -165,8 +164,8 @@ class Neo4jTransactionManagerTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); - Optional optionalTransaction = retrieveTransaction(driver, databaseName); - assertThat(optionalTransaction).isPresent(); + Transaction optionalTransaction = retrieveTransaction(driver, databaseName); + assertThat(optionalTransaction).isNotNull(); transactionStatus.setRollbackOnly(); } @@ -194,8 +193,8 @@ class Neo4jTransactionManagerTest { @Override protected void doInTransactionWithoutResult(TransactionStatus outerStatus) { - Optional outerNativeTransaction = retrieveTransaction(driver, databaseName); - assertThat(outerNativeTransaction).isPresent(); + Transaction outerNativeTransaction = retrieveTransaction(driver, databaseName); + assertThat(outerNativeTransaction).isNotNull(); assertThat(outerStatus.isNewTransaction()).isTrue(); txTemplate.execute(new TransactionCallbackWithoutResult() { @@ -205,8 +204,8 @@ class Neo4jTransactionManagerTest { assertThat(innerStatus.isNewTransaction()).isFalse(); - Optional innerNativeTransaction = retrieveTransaction(driver, databaseName); - assertThat(innerNativeTransaction).isPresent(); + Transaction innerNativeTransaction = retrieveTransaction(driver, databaseName); + assertThat(innerNativeTransaction).isNotNull(); } }); @@ -248,9 +247,9 @@ class Neo4jTransactionManagerTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - Optional nativeTransaction = retrieveTransaction(driver, databaseName); + Transaction nativeTransaction = retrieveTransaction(driver, databaseName); - assertThat(nativeTransaction).isPresent(); + assertThat(nativeTransaction).isNotNull(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); } }); @@ -286,9 +285,9 @@ class Neo4jTransactionManagerTest { assertThat(transactionStatus.isNewTransaction()).isTrue(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse(); - Optional nativeTransaction = retrieveTransaction(driver, databaseName); + Transaction nativeTransaction = retrieveTransaction(driver, databaseName); - assertThat(nativeTransaction).isPresent(); + assertThat(nativeTransaction).isNotNull(); assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue(); transactionStatus.setRollbackOnly(); From b2f9c89501d0b3b0e8c868d161a7ac5615b31a3a Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 23 Jul 2019 17:30:09 +0200 Subject: [PATCH 158/342] Add source and javaDoc plugin for generating needed artifacts. --- pom.xml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pom.xml b/pom.xml index 7605b2d13..398496e33 100644 --- a/pom.xml +++ b/pom.xml @@ -427,6 +427,36 @@ + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + syntax + html + + From a34e189b4cef80e0919d6a5a3f0607b60be42a5b Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 23 Jul 2019 21:49:43 +0200 Subject: [PATCH 159/342] Remove declaration of java-doc and source plugin again. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is done by Spring Datas parent build infrastructure already when run with profile CI or distribute. Spring Datas configuration of JavaDoc doesn’t handle self-closing html tags, so I replaced them. --- pom.xml | 30 ------------------- ...elegatingMappingFunctionWithNullCheck.java | 2 +- .../data/core/PreparedQuery.java | 2 +- .../data/core/cypher/Aliased.java | 2 +- .../data/core/cypher/MapExpression.java | 2 +- .../data/core/cypher/Node.java | 2 +- .../data/core/cypher/Pattern.java | 2 +- .../data/core/cypher/Statement.java | 2 +- .../data/core/cypher/SymbolicName.java | 2 +- .../data/core/cypher/package-info.java | 4 +-- .../cypher/renderer/RenderingVisitor.java | 4 +-- .../cypher/support/ReflectiveVisitor.java | 8 ++--- .../mapping/DefaultNeo4jIsNewStrategy.java | 2 +- .../data/core/schema/Neo4jSimpleTypes.java | 2 +- .../data/core/schema/Schema.java | 2 +- .../repository/query/CypherAdapterUtils.java | 4 +-- .../data/repository/support/Predicate.java | 2 +- 17 files changed, 22 insertions(+), 52 deletions(-) diff --git a/pom.xml b/pom.xml index 398496e33..7605b2d13 100644 --- a/pom.xml +++ b/pom.xml @@ -427,36 +427,6 @@ - - org.apache.maven.plugins - maven-source-plugin - ${maven-source-plugin.version} - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - - attach-javadocs - - jar - - - - - syntax - html - - diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java index 25eab7a4b..6d518499c 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DelegatingMappingFunctionWithNullCheck.java @@ -26,7 +26,7 @@ import org.neo4j.driver.types.TypeSystem; /** * A delegating mapping function that first calls the delegate to get a record map and than checks the returned * value for {@literal null} and in the case of a null value, an {@link IllegalStateException} will be thrown. - *

+ *

* This class has been introduced instead of {@code Function#andThen} notion to be able throw a decent exception * containing some information about the delegate used and which record was problematic. * diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java index e849c3cc6..17958df84 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/PreparedQuery.java @@ -33,7 +33,7 @@ import org.springframework.lang.Nullable; * Typed preparation of a query that is used to create either an executable query. * Executable queries come in two fashions: imperative and reactive. Depending on which client is used to retrieve one, * you get one or the other. - *

+ *

* When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure * that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected * by the mapping function to work correctly. diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Aliased.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Aliased.java index d587bd057..184e6172f 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Aliased.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Aliased.java @@ -23,7 +23,7 @@ import org.apiguardian.api.API; /** * An element with an alias. An alias has a subtle difference to a symbolic name in cypher. Nodes and relationships can * have symbolic names which in turn can be aliased as well. - *

+ *

* Therefor, the Cypher generator needs both {@code Named} and {@code Aliased}. * * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java index 06e99892d..95049f9b4 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java @@ -28,7 +28,7 @@ import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; import org.springframework.util.Assert; /** - * A dedicated map expression.

+ * A dedicated map expression.

* Most of the comparision methods on this expression will not result in a sensible query fragment. * A {@link MapExpression} is be useful as a concrete parameter to functions or as properties on {@link Node nodes} * or {@link Relationship relationships}. diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java index dc77c4beb..80af23b8c 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Node.java @@ -136,7 +136,7 @@ public final class Node implements PatternElement, Named, Expression, ExposesRel /** * Creates a new {@link Property} associated with this property container.. - *

+ *

* Note: The property container does not track property creation and there is no possibility to enumerate all * properties that have been created for this node. * diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java index 2c4e6e5b6..0547ec682 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Pattern.java @@ -26,7 +26,7 @@ import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; /** * A pattern is something that can be matched. It consists of one or more pattern elements. Those can be nodes or chains * of nodes and relationships. - *

+ *

* See Pattern. * * @author Michael J. Simons diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java index f8a9a7a6a..392ddda3e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/Statement.java @@ -23,7 +23,7 @@ import org.neo4j.springframework.data.core.cypher.support.Visitable; /** * Shall be the common interfaces for queries that we support. - *

+ *

* For reference see: Cypher. * We have skipped the intermediate "Query" structure so a statement in the context of this generator is either a * {@link RegularQuery} or a {@code StandaloneCall}. diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java index 5c233a14e..ddaec1672 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/SymbolicName.java @@ -22,7 +22,7 @@ import org.apiguardian.api.API; /** * A symbolic name to identify nodes and relationships. - *

+ *

* See SchemaName * SymbolicName * diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/package-info.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/package-info.java index abba5c1bb..85c2e2f6d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/package-info.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/package-info.java @@ -1,11 +1,11 @@ /** * Contains an internal DSL for creating Cypher. It is part of our internal API and may change without further notice. - *

+ *

* While we have extensive tests in place to check that queries SDN needs are correctly generated, the AST itself is not * validated for type errors. That is, one could misuse the DSL here to create an AST that renders to a syntactically * correct Cypher statement, that will explode during runtime. For example using wrongly typed expression (an expression * referencing a list while a map is needed or something like that). - *

+ *

* With this in mind, please use this DSL consciously if you find it useful. It won't go away anytime soon, but might * change in ways that break your code without further notice. * diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitor.java index ff63278d8..1710a1e9b 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/renderer/RenderingVisitor.java @@ -35,10 +35,10 @@ import org.springframework.lang.Nullable; /** * This is a simple (some would call it naive) implementation of a visitor to the Cypher AST created by the Cypher builder * based on the {@link ReflectiveVisitor reflective visitor}. - *

+ *

* It takes care of separating elements of sub trees containing the element type with a separator and provides pairs of * {@code enter} / {@code leave} for the structuring elements of the Cypher AST as needed. - *

+ *

* This rendering visitor is not meant to be used outside framework code and we don't give any guarantees on the format * being output apart from that it works within the constraints of SDN-RX. * diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java index 5d7433ac6..5112f09f5 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/cypher/support/ReflectiveVisitor.java @@ -33,10 +33,10 @@ import java.util.concurrent.ConcurrentHashMap; /** * This is a convenience class implementing a {@link Visitor} and it takes care of choosing the right methods * to dispatch the {@link Visitor#enter(Visitable)} and {@link Visitor#leave(Visitable)} calls to. - *

+ *

* Classes extending this visitor need to provide corresponding {@code enter} and {@code leave} methods taking exactly * one argument of the type of {@link Visitable} they are interested it. - *

+ *

* The type must be an exact match, this support class doesn't try to find a close match up in the class hierarchy if it * doesn't find an exact match. * @@ -71,7 +71,7 @@ public abstract class ReflectiveVisitor implements Visitor { /** * This is a hook that is called with the uncasted, raw visitable just before entering a visitable. - *

+ *

* The hook is called regardless wither a matching {@code enter} is found or not. * * @param visitable The visitable that is passed on to a matching enter after this call. @@ -81,7 +81,7 @@ public abstract class ReflectiveVisitor implements Visitor { /** * This is a hook that is called with the uncasted, raw visitable just after leaving the visitable. - *

+ *

* The hook is called regardless wither a matching {@code leave} is found or not. * * @param visitable The visitable that is passed on to a matching leave after this call. diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java index 70936f73a..1f76d2a34 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jIsNewStrategy.java @@ -35,7 +35,7 @@ import org.springframework.util.Assert; *

  • when using externally generated values and the id is {@literal null},
  • *
  • when using assigned values without a version property or with a version property that is {@literal null}.
  • * - *

    + *

    * An entity will not be treated as new *