From f1c11281dc67e5c61a5d0c66cfdb6d6aca9c1eba Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Fri, 21 Jul 2017 09:26:37 -0600 Subject: [PATCH 01/28] Initial commit --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4fa332f5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +.gradle +.idea +*.iml +ci/variables.yml From f13575e4911c01c8aa1429aa63ad64d10995024d Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 09:14:09 -0700 Subject: [PATCH 02/28] Initial Spring Boot application commit. --- .gitignore | 12 ++ README.md | 1 + build.gradle | 20 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54712 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 ++++++++++++++++++ gradlew.bat | 84 +++++++++ settings.gradle | 1 + .../io/pivotal/pal/tracker/EnvController.java | 8 + .../pal/tracker/PalTrackerApplication.java | 11 ++ .../pal/tracker/WelcomeController.java | 16 ++ 11 files changed, 330 insertions(+) create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/io/pivotal/pal/tracker/EnvController.java create mode 100644 src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java create mode 100644 src/main/java/io/pivotal/pal/tracker/WelcomeController.java diff --git a/.gitignore b/.gitignore index 4fa332f5c..a70b4b956 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,15 @@ build .idea *.iml ci/variables.yml + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties diff --git a/README.md b/README.md new file mode 100644 index 000000000..5c72a5164 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# pal-tracker \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..b16c5860d --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +plugins { + id "java" + id "org.springframework.boot" version "1.5.4.RELEASE" +} + +repositories { + mavenCentral() +} + +dependencies { + compile("org.springframework.boot:spring-boot-starter-web") +} + +bootRun.environment([ + "WELCOME_MESSAGE": "hello", +]) + +test.environment([ + "WELCOME_MESSAGE": "Hello from test", +]) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..ed88a042a287c140a32e1639edfc91b2a233da8c GIT binary patch literal 54712 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

49F z#-T-=dw(N6=)iK%<^NT)|NLJL3jh8D`j3C*KWa?-fBYLO6Rl+Czc)6%nlaB$Kru-} zrXl@!Aro@*Lg?f?z(xfT9YQY zvpsKYvmI~QuV;66ef*Fe3Ij!+$EZs=B@t7hE60m;g(gN(Oi-evKRENMALT0Fb7Agx z8AOGy$7?xUGv0KZAkl2Fv~b)u3Bzs=ZT?muv-dzVba>par{rV;IbbE-EEFYY*s zGiupeZq+#Ki*+-U{HY-wj^}-Bq#Hi`8*uo!pzX-DN!8J{+$i20Cju)RofwaJ@0{#h zKfb$q6%zoJZ+(Q8UdwfG+iw0)yMF^LV4q3Zm>FGOlhM#lD;^4{3ss<`rH^(YXUlf*Zu)hr?fRpX_*n(i*?lnyiv~w*PzjW_0(&+{+ec4qCEeN~15Nk@L2!qM)_6W{S{PB#q2L?Cb>ngOy<5iCik<@f zkRvk7o$8QOP^-b?ul@_$rfj|2mrXtvR#z4DqBiM=ntO7hS2~ZA#q+ORy}inp>Qkq| zLd*%O^H1qtE{W~yPk6Y#m2{%##&C z{2y=x!<3h1hR(1}eeqW-+vV0`7gaFFmZ2%%O9%qz`O zs`HD6%d3`U-nl%vUwu;z{z;`z8YXXrU->+F^Y+dLV8k`OwnaKuj?x_EeXqmbVO`)`}_|_sor&nfM0{RTu*0 zPNWNwipq$9Yht~_YDRy%ynb|Z2-2f8QBPDHly@#yFVkF9P^(u~h}_JuHf>fauTn$j zr#TCudXGqdrSqCJS(RBbSgug)CZ0DAn%q@)xnUZ$(jCO7J!Uerx|7a$ZqmkemE8Xh>NkUmEH7=WK?AM9{*^HyT9Vzu$MUDxv5aWYPfL(iZi>XagCnFv?d z=`H=Y!r6j9jgnQvyMn+a5v=Ia@?v2DE`9|8V|ykXi0NaCpL{RS=9J_1UdT0&?FI2}knVhnDWKqhU4HMmJYV&4j z50ah1WBIsHo8aN}L$(OD&^~61aeGL8u*Chr{Z|z7Aepg{{X1_ieUD3o|1W2VfS$dP zn60uSUsY96!yAMuODmD7betq~ zB7ETz2MiJQrA*(vd?B}tSFN0qhr0K?cLtNwUUWU4M9`0^F(W_*2jH$IGP&%Hr!Fp@ zado-?O?L)-qT+lb*yUaFqKesJlv*nC%kqozr(&$dRD!I61Y7N`PI%P76$%&{{1c zdkydM{UY6UT)rP(S~(UFQ3VoF56HA}E-CBU_xkl`4r7eipF6~QVMwply|=pM(8k$v zGIsJ%bg&5Pnm!?GUDhq>NBA&FfhDO30jTk|F3+V-2;Yikl%gf}G#cFQa+w9=Vof9L zFjS#8AA_O0-Nl_v*+bLk!VF_V)T4!HrBS3+9=+Sv0&WP4d}rUDd@DhsVP6jwqDYM- zR=@tpb^(2DC>1v2Sn}69y|+O&r2dL}VaTrTM*|x3{vzxBMeX@3|Dwr?b}g|vSsf%} z|0la!T97m@veB$bQb5s>AWOV8irU(bg0k#fPka%9sHp}BbF&TN?F^tAU%-W`8L<5W z_}q9y$i_D&gv-rn(aWiX>qM* zeow-?>XIXA&3iV42Ozt}HI{<2hi+lFr{p2~`)?rOY%}EmM2$*$Qn{RZ3LyMb-X(;~ zEvv^Xz&pTygnKBn#3WK!Gnbshg%{;@LgrtilLyuGYxujO3;w42as6MSQ^NY&sr?^S z-9K`kf`gue(Ld_DO;rmQq(fw{Zo_yrVxXYFAHK@PX)%WOumIsR4S0D4LA;of5e;j4 z&XS-k4C|?@z!!t!8kd{eGtA2FwP0&*zTyb{9Shnud5=qZGG9-wZ=9ZQ+u4;|CdN+R zVz4!#JnzTp-@9)cUH0!&SA$lDL+*Pr!`g_|^w&I(Cf8=95AF!Gnm}^yScIG6*Z;!PJ zZ+cmO5xWGh5l-^3q}peCSvxeu$gpLS^5!+^?j6kATFtj}HeU0_DX0aXE`p+a zt2j`P7FsxA%cPQQ6V~F12#SU`Bfqgk>Bj7+DN*o}l;|1UXj{p2h!MKP-EVtpIp{;D zZ*DzC+{*+R<^1*@U>wx|`h2BtdsVW#(4h5s)CD5hIZq4SEV0AyX?tfRoZE67&f(@d zWr>Ca_NZ!mw}dP?myN+uu>P|_0K8A|ts*4}ZNbw28GwFZ3qjR6-b^Z`ONniELwm!g zui`*smA?Hlb|J;O4Y2*}zJIZ1LlN8p-8I(37O;HfIqmZ4TtrizYDT%+61!a3!8!V9 zLPJ}9+os$ZeNU3u;YmC-xeky>II?H0qT|BRQNx~Ussdshd-2kf51m|$rs32|yY5hk zdjt+V8j^Qs`wR9|Eu(EyVmFS!s-xk4u6GN2M3KB{r7?cxfWIZoR27f5YVEWEsLKSEN_jQpE<_iF742n(TzX*^dtjoRQjE zfj!;{H}sV6r5Sj55}aM-L%DKkk=@aJV-9<aS;aPxtQLYftA|hGX0EmaXW1HM2mR zp+%CV=I~pkst04Ic0m({9@UfRjBT&Yrdu`VfH#cG;B?r|^io9a{xUK}QnPwT5J;8S z!3p&RdQ@Mw!`?-#^Bh{cJrvrruVZ&1MXDZr#!Rd+M|QWi)!>$X{Tk^hbM4ciAOE^g z#L44#`BSE*$1xYt4$)?+3QxkGve>BL1GdX~ketS%HP(lKggG#_+!@RWthtz4JmQS* z86%<(AmtJ+3LP3W50(!~ovWbJdT~W-NGpi-S0GnrJ`tp~5jgoXU^XK|`+~qSL#6~5 z`RMc>K8+hKOeQST3#POM78p~Xr>yBu#c&TbR2?rYP+dM0LAO}b@MNVBAAc?=PS8$R z!VDZ&=V|rkE)CR9gX9%k{^-vd<@#y2G9K2L9sSl2xvQ<9y@Qm4`1wq`2!y&?^^J3C z^3^Dt*;iia**HufXcxQnyzkn$STF;odj%J@z!CIM)!X4^#qeZ#X81{VrcDf`Cp0Z+W=^^a-&bEK! z>~=jr#G--EM9|l>4?Ud+d9<%sJmQdm+Sj4rcfIi@ATl#@Qcy7)KyWO<(U z9UV4NMveNf9N*Mms~{P4;85FDO%S8rbxdjh;PwkFIDW`4OOL z;D705t}Jg!6`w3*hm7-#cAAu1u@iC0C7|e32)NGc=)9k_SobhJ&b&SO;459xR23bB z`ss_mF*?^R$q}9irLK3r$tH?awjOxi1gC$X@mscl?O0`tM1Q)cHjS^=+$JVQ1xVtk`*houY2pfBy z&DUyjn`~y1(k@s^F`tb*zNnHje2lYKm=ls|R7jnp#N#T!Rb7R~UmUvk<|ZP%@|F>_A&^a+RUPM!Vo z)&f6!!VwGq%6lw8HrLwIkPBD^HKLtn8QCqx=nPR=L$rOy*Qnh54m+hl-hThl>sHT< zKGQEV+J2rVh)b&PPNIt?o5m6=JcK`u>^Z zZ8}1yBMNvhj4H8qeo2c&r59>l=o|wfAWy&d;2!*!+) zAOZ{48Tj@|)i`U{G0FnMK22RC6Fy-dViO#-fxXB8=EQtl&Khj&RZosn&DMwr4PF#Jds)08b#V{D%yuW1a_b>14dvRtr5E4Dil z)*OdH7O~lxX{G9R72!D+8Orpb+erONxQpQ3ceZlD9w;~%jH!xXY^>4s=0MUgalw+? zr>kJyq69SN;j0yaz&F=U3~%uCIXrXp1MTaDi`Y-K6cTies(9(c_G|RY^I;MQmq##7 z@4R~mRZLZ7{YbzFISIKiiH`V82|tj1KLpBhUnlRp&kgLyF~B1mbH>m)$*Mx&kTlL| z<&=#Am5Wvtn==gq8_xqO+JbQuX=QbR-g@U{u|WYB;mgc%U~3``JSrR_he?q1>|=uq z5>Ut$dtzBHhevmW&1N$IL{1u)`+5MK0nghS9IBTz(Jri3n4f(c!&+c79A~N?B@>N@ zS3o{u?5R#J?)VT!@31&%%3T;g0a!+t$a{%!sA9DOq~fvGK$~4@eyL(edo#}gEJj;Y zZH!r*6$DengoD%s@fMj{7xX*2a;NAd^Mwf2)r-6jwe1!5iGUtZ1n2?3HAnup={xJ% z8UAgKT&i>=iwsz})oC>zIaQ)&d9Fd|AvU5wv-TH2BQcV|B~P<-c-0-Lqt`WkJD&X{ zeg^fhi6A2qPQvF62m;fHSD#E4-N+an9Zs^(cm8(#^l#j_@0QUGt$Pz5{xh76r+N5j&b^x~{H58<%?bvd&D4Mn^L?QV{M^qZt%Amzn(j zw*#fNO`QRxj|89loiLd`Y2U>kSTuhldP{x3RM6ad#F0N=-LRA7uK|C=w3zYn$-Hr3 zRaxe{zgMs>MiSN0nM$*ceStj1eWx8(aYF&DJRMfmdOAsXx1*EhPB4LM$=CbG-A-=4 zm3(CxCWyDrdF0H~t`EHLAS4dK%dRSO3MIj7 zgE=(zCuyA=LVc`2=N4Hv>jh>Bwn;qRk5VIY4dIkgey5HRPJazm1-8*5ine!S{nuq7 ziD=2Y27t+z0!*I$cb0{JnEXpGMVyj3z~PVKDLXeI&xNs>Jq#19psW-7%J;2^jo251 zS237K{P7dR(PgBT;t!ZluUl`x!bk$go+vilX2Ho*P-04VT6j*jn-i|)WtVjzJzW(d+W z1(9{Vov|BURz7dP;KPDoldMvzvzosTG`8hK3h6K&GVVXbg@`|hUH(?Nj_Gs2NP*hN z*ivPA(<&LfU39=&9cYZRkgr@v8(tyP#kr-+hmmgm;WmYjg1vC%8^!p2CoQ~?G%sG3 za(}^MzBKIA8Ti~OX`}hnhVs|_C|f;~+I5y{^))qFVFX9eBuWR0Yxs!a51G!=(;@yc z;MHf9hLn5Fw4Di}fe%{au#c?8>llKebES9X{^~x4=3XH!nD0Yr?P%wg;#JsJ)tYZN zwbs$=&A~8X0a*-bp~^95kLPbH;?sy(Rx~;ucStR$=R2^ITT+g6<|E+L*xbKt+~jX+ z_&Dl0^`(aiQX~o_=~9)M;Uwr&JYHWsJ62ZM!{)T8i~Hg5qfIvkV)_f3;0D2%^ZW|}lLlA3{qUZ7#;!eM1QXF~dX z!m(A`VBs=th)m@TIt_<>MSO9%DdZF&pB?wwn{f%^y|9_Q!9!ds>7Hg9x9|q=2Dwwj zmh**{&_db?l4UTugl%gZ2VuG(UrPjAFeo`A(RdnY7Oo@d1&-+UGzgD{aaD-8i0x2; zOPFL;T2`nGBB_xRMH9KalX)&-o5a|tmYz=flkrjph8*!cA}_br1oS;8WO1Fej+ z*EVUUNMom|7d-S4^a7R{U+TqoSY~L#iPW}(>E4x~NQO)Y<9OJGxcDr+=nbhBnqvik zBWwA8SAqKV*4KaWkHo#x`k?~F`$g@GlZ;Gt@`iI5r5L3Z%6k$b69E)o=qR2WHp#%F zej{Zga?TZ-#nt6rLNWbK z40faeo;Mcr+sZ|u(lwcM8|tOLsJdV4+rahg1^2C0*VneF;Iuh;&<6_Cqd}dTXIn~f z!|oE;^4kg|VW$;cK!MBq21i|u%l^zIjEYY|GU4iH0?s{o)zXx$n>h1O_KAYC!U5|h zcS;N>+JG-4PY4~{ts_uxoGl!}vyL3z4_aBzmEMUyo7 zHY({_lQ!?WKms|gQ(zlvc%Py);GI)ujdmBU?2~lc&4X%pqQB@hIn@s`XdLp+rBGTj zl9*`=GZ@TSsFa-4Ir%@wpKu5{ecxaDy3tCzNs$EUeB>>-`WBAckivbtv9p|$2NLv1 z-8_A@I+@R!qqB&+R_R=w_L$8elzj=o|2;=I`KzRS$oKPti|ZM4uAz5fXwr}V`9kHJ z<}Up}fph4Su6!9q$)bl-zAQ?nXqeFG9gMJNAGAOPvl^=fPSww%_wS))tUug)YBg7H zkN3Gz4NC_{=wfi$VMKk4ilBkj(=Ie|DbdHIhDBb^%Q#t-6~5t0*HP+&d&5`}5^<0? zc^aE#N4XE%3pjbm?Us$lG@Q!M{9#Cx(<&zgcMo3ZIH-f0d&v;vz`h~x`eM+viFOHm z;>bCZ9Mv?x@Y~pCAkSkx?BgtkOl+^DwybQ@Z0=zAcnWr&NlG?HsoFV)?HBi8e@%l4HYIO%=!u7VMEJZB&K4Yjdwh>23a<+i(-qrj zOfof2lqsHM9H!Ff&TWjwSU%>}6vbjU4T!pX5%Z1lXu0JDFmWY-iT{60aDTlsk)}8X z&^JJNGHNv3Q_uXKqf-;Cnw8i8P5_dUFq(`^28*#Ha@Ud~hRL8w+NrMF3ru!}XFe2N zf`u{tF(=Hr7Bw!L70+qq)9s4eYP0K?_iY{zu$lgjFi^u96&HF$)_NVjKB6r=Y?Zln zk_%+AJjMx zvkoyiwwzixtTRg-8Zx?L-nXGRA7O*-1fh0(iBg2JDf(3^*#lwC%q<}KHIA#N1jp4~ zP^Dez_;E*NNzk^oT$Y({d+@2pmg5*lE3sqMd-{U~eUp>q2FynDy zpZAz8uj;Bf{(Zt&Y-^K)Tmva?tO$~@WQpELH*#Qs2dAJHrq0B~vtkXDN+642#cXCUiJ^Y@Io~dE+)C7oiIXY}m9(&EckfGD8mk^8&#XH_g#UtYdeJT%o&S-GU@$Clm zyE_soUw~5jN61pg5GN>8OC$xv0o*0RaO4Ih1Dk|h!>BIa$*^g7#4)Fj*5k5aTjN4! zFvFsEbPV%PLwiA7J?|FM_sG7I3^bMMv)9orztUt3-+X0c${!en$QL9YmD+_d01HrR z)rqY(jh29$v_yR?@{RkigEuXzi7y1evYP;#Z%n^4c>is^7N@ZKLuK?ymJ$WVzI{oN z1(_X(foOXg_KyE{Bb1Eq2I58>bIkIqfgh;pWIFzJ z>WTYK>f)-G=M%6EP@fpqA{*2EXtvoVrW4IHEem9lO8Q0ioWEj=tq=ou$2e(;6Yn0L zcG!K{9mO4=o7A!n!2@y@kEL9yk;AtD|E0>eS;Zfsg6ET-3G#}$S|NoK5Hywr!c(J= zgjXHGTX!6M&s6)f$|ARv3MLo*J5}BHnk));cNMn4qARpd(nF=!Z-gRJwR3qm&Ddq3 z)aaX`C81a+X^b}@seMv~zEnt4kln$p6xfFhQ#RG7VOo5PgxS(1DDQ7gn;V<7hu%`` z=jN;)C-Ht;OdrT)a$t#_k%3(Fj4V^())9bJf5O{x6P}b9Z$*IsqvosRh0J!PAp_&) zdYEI9B|5K>&wG5l>K$>nb4)pSYVAV2UGG;+WaH`5T0t=~zCc8$Im$={N zG#FAChsBVh+q)OAj(sp}a1r#@f+&RFM~KfICRdJ}SQ>FF3{&{fnDmcGZb--X=9VUH zeMiZ-V9j7j&qONV4d1M@Nif8u3dBH)K66Cp5D)Ey>p%6*zAoJ&Ads_h(e^c>)%$67q3%WJ&s4V9#85{fVONk z1YtL!xfmt{i&Gh5I=6Z{Vtq}AMQm9^%wg@mZl>e);0Qk;IuA8AkpaW*gDlQ28-^wf zeMr*P>#+?_UH_h)w*wuYq~Rn*YK5-yMx%T~Y=7+>mhc!0b|B990c=cdiOtSD-FyKY zw+ALjHE=y=m`|=UB7-0bY>KT#6r9&1wUSfNt;cv4vvWu`D&zo-vN&!s|CsMvN<5wR z7D|21sFuQ^pU%9SS+oR*+~H2``J`w4c2dM+LMm;n4N_wIs^RX6hqks|xRhia?>qLi zubCD43V{cu`->~lk#0=^B*`}`~wu1+h~ zRn@$dZ?qFBb@MUB)L%^v&8CKEjGrWBKNjk9B5#CKeV8C0ZVs^`QM@216cS7b(S;SO z%-kSD%c<{SxE`D8V3*i? zF}%0t)l4NdqX>Q{{GDoGBnn)X(!1*Z>uJ*Oh!WWzER~Pd)Dv`XTHotKL{?Yw`d1&~ zbuQfCZQ*i7MiQm?zF~esWV#0p@DO9a_vO1nE!cfijHCl(4CF;hXYeGYNqI{x|0X!w z*p{xIOIq7Na(%AGjfkkT^|smUl4Z@1<5LGv5=>-O?Wg_};ni~ zc7Au8qU5)3QDLD^p@|qL-bPV&-KbEO3DXOrCiwDp-wAVwROjWozm_*fBPl8GOVHCn zXkzCQ?1d(>#pCh=Epon_fxzci7%5aj?TQoF!4LyU)0epC3RUVo3G-*npbeG%-i7i} zAI(hlL4S0EMalIe50ajRku7h@!;4c@0eRKv#z;Uwy;T1zLgkOq_>W0+ni`CY;!KS; z^*KjO3kdXH&lZaw0C{5VZXWWwmAA|;9Gsoh zIIle1G$4zxgvx!JgunB;eGuitAJ{3!dZbNwlEpvR%2MDu(wQv$sJ4ld=3uJDg?Tvp zwM#o`mgUMcHKmVhXHT&`Q1+HbXfuin_3Sgx=#DQB-4^o}v-&1c8vH2+{-+sJo=;Qr zKHsx7S?ZKv%2n;ORx9qQ zdg-pkt|%>CH@h0laJ7lvddxZ&c)^XHf}{f-DjN)VX4Dx7ea7HgWAhwKGibL$%Ww^O zHlZUC1CKOl39uAL9DrYW<&1*~ z8ZuaX9i-C)P6sohK>4bKzhqfMGThURT>-+HzD_@kQmdLKU;I_E)fMu@T~J{>(QNkbTo=2!~_7GhI6QIj<9AaTht3Qd{~rUt8#t5$uYMhH@# zns~&>Bn=a8@T5DFdj-!QOMy?-nWEY`BHDPs;>2q%(0zNUW_A{lUP&Wh6wd8r8Y23 z;K$lN;~^xQDl{jVt=|4d$RnK#5<;~@DDa#eru`Mw!HV~Sv-EG^jIEGu=aO0&A?mm+ zwN+q{U9~wcW|`S}$(V~8xnrWXx8QDcb?G4LK-2?EjWW1? zcAk^;Y8Qo)}mJyXs5LuaV?bO|h^dUFt}<*#Wu#W>cd2u#sST0qg+r=-GZI{{ zOo`7lcKs?abkIOwI?b6RrfX@sV02fJYTwBI3u>rL3b#Gk)J1fbU?vKf!02%j2=&g; zpnc#MySEe329*M;Tcn=D?}}(J___pp0;~~tkA#G*!yH6Y0s?-J%@2RGoD75rxsBAbHEn&ZMt;cGFUbh*i#K+? z>_^8ovWz}BPv5rmC1c>}uEo-LMVnDXZ4idZSx@_EfoVwC{YODFr#-n=wZ(9GeUvT z?=gae9G}!$@+Bg$s8I|-qyL!HW_nsjf^D`xY{P*>&!(XsLTqX-QvydWo@|+{x)jzM z>I}Ad)C1U3Qp9qASuQ#o0&nXqH+x7n=)Qb|9W+qMfdub}&Mw|*%Zw42opxk|vG@@@ zB{XGmz0$~E750p>O7V-K*fu$M$&q{bd-fYRc>|$={5&@{M{Ol+;MEpvg_aQ7lp!fN z<pJg}^XUP54pVXp{aOKQQ4cj%Ddin~mk$G#}Hef;MqDh!DARF-B(>8?M~}z53hK zN1G0m8OF@GO8t^mFILI2?PaE4;{>iUzgR*n7wYOQuF+zK@ zCUrzW2HoxSpAEB7symmH_z=_RB-S411@ z#wY*8(LlX%O)@0uJDU6)X;{hhZIV=1&eBHn~DJlB4Y?*cX@ z^|$~c7^N5jxr8l7d_tDyAWySmD&+&^2}Z*>Yx3Z73(E$^8PKMX1eto^32;Sm4wOSW z5<{<1^lO5ard`6|d2xiP1V&@?w4Bd&LxZ2D`th(X6+=o@_Ufp@m8?RP=y-)b={-T- zRUMVDJ$GGTH24=D5L}FbU_0|~vtKF*Dt!I2wVxxDug#T{AU1ccAg`e2?PmH$*cCs# zXQuDS`FesQU)cTK2hV)#@f$3;fYm8sW4sOWhxAS;UY4`h+t*XMWNKUu>yWFV>l=7n z2d~2+@>W4MD0x|GdJnFz##vq;zUpugeY^B!_9W9P$_Lk zHcZ85MOrTDiAG+icNLdz&6JG;;)RC{s6LH2(?OKw8NjECJ)x?=*;wTkizi1pgE%6? z80*_I5{e5GO;9VW$=TCLvG-JWh0tL}XrudCUxj#@f<~j9%)-Y`4zf6dK98SBTcH$;ukruq<)C@82LQGthNso{~=_1De$queQSD1+=S9$KvIRB(aS#{ zAur+e-k{+*R)jmxm+YW#5G3iEROu1#fL6mKW92Ba~{OOE{| zdbt;W>WDM?3F)3!qQc6cOJ&deW<@e)&-Mdwi+H33Q@$W`S9$;)TN=2j!tTKZ$sUgI zD1QKI!?;;-@};1aRkn&$`P5YARHk3cY*5p)@R$>M6Gbe<7Kzs%j5Gxq+F9Tiple4M3u62w|GB6ePHzAvS%d1ERSm-%(no7 zv{-Azmco$&p`!)Ugh*4{g4MI7S6$j^*Tau~BuxQp%4!I#l<~!Gsm^R|&e{;ESyVcr z{EpIqFC>NLYBc8pGO$!M(BXvsZT#*Luls}^*rPhQFslZfpA9 z5QLEvY?h25386L#v}ssG1CGAL2YmZ)1Fy@)eU9BbJ9MvgE z@^_8L>gvMggi$c}SzzS^)>mK$ANHC9Lr_|;ChQWkGe(2<=sw%3P?%2xbs=4GP`3CB zHnXL`1dKpe$V&u=wNVUg=`pB%9Gmr)7O18pcz5LKoqyvYi!mjBspI5N zhVVuXgU4fEzOB4soukwj<#UDle5;ge$hGby^qfxn(b(mIhr>tdU1K7GtxS_x-cToP zDDb>xl0DMusSqAtxtTsw%NmGvLTNhO=3M3)gEZBwPqW3_Qa4FHokL6XZxv#IO{N>e zd8-kz)n7{-8za}g107$~bZKazlmmB-;qI8w28ihUug4cC5W0{8OAC;pLmi4kCkQot zl~|jBgh4jN7E3-BN|uUWMktjfxOf74b!G2AaSxfWO6bogH17U}T&xS@^b*&4!RVn^0mEFDvu19}4UumQj_-s86N*&QTphQvwd%}Ch4=~NF z5^ZU%!q&t*s|BV&P2`OC-2PYB(;+kCmlU1L(yRu;u@F^oV4hfV?@VoB$LGQ8pZQus zD_Pe*_mX^$G4)wN*bRcb)$~mA6yX;`y17-mgJQ9nuxN!?SP`>2JOsNdlwy`l<}|V7 z^@^W7-vLt6n5AbhI(H%N+2qa5qoW`Cc(;>ak_DkPEvM3+WYpp&d27B!tNEp>bX0>RS%HqHQ?=e?6_&j0F;gUT!1G55nsS1mC5&@O*FF~m~9 zSlV7@`L37WY~URNP?h*z9aN-ckfFgG`QQNvd3=4*vgq+ggo!eNC^x5Yal&G9`OusD z!kWSg+9;s(-p%P1X+cGf=&i-*^TjVapWaE(Oic){cR?jfu#I@K1Ksy}sxOh`8Phw? zLZIB_ZeHhxC{i`WX&(z9y;^~R#i5^J0bb>wFu7wUcoa^S^YXFe%O2XbrXYHfbz^X* z4YPw`<{jl=oEIJ2{46PSZxV|kKEs&e2m9J&Owg;`*9ra5J6h(f4Y*fNobm?dHk=Nn;eohx}QB;rlgskS66_qi#WzUQ$E~<+N z!9)Dbw)%N4o=QkOf=kfCa74B0%RlO&XD^3yYp^c>oNK~vs0+L%*S>wmWf$=Q<~z4H{=W?8FwiR z-6WI2S){a;v{8pfHYbvcG>)C<|EM7)oi!eimj{*{@4+1Elgk1{#vjjqb2f@?-F&L@ zx|N$5OM14Rk)9c#!FSEQ<8ItA$^UfU#}9JDup*a<90<$o^EwP|OFrI~(&uwiMRyKS zmuzOwav@oz|B$3+N0kc?@unJPhIA&X81UkmCQ=4K>2Hku47l}mUno;+;#ws=>3Bqf zfjg&<6^5<5X!HAsP1G|_C6i_{Sx?rF(+WfPqq$UTE+^Nj)$yXnnTpZjX3Er){bR!4i!U2W zvWEE4wfD!GqC$kmt5cZzos)W;+RhZ21VGu_%CkZ%G-jpQ(L{tHaw^qUhZxNtE9Xtz zlo%;&cl4$K`N2o#(I!i)cR)y)s#r+l)`iT7ZrJD4z#B`IL+-X<*Rpj)gaaZKKS}Nm z&r+sVuuAj3pA@?rl?Z;52qxn)c4|}j()A_S;{-78wZfM$YrYKu&#^I!>=x665M+9yKz9vWbXTeM&Vai1c4+D zERz+QW%9Nh0$vx##~-W1Kn?Y!jYWGmYSpN3t}NcaMT(VH0>~DHcjgwtWc$V7J1{Aq zMH;GnhCD8IYA}=DHRH0$ElPk+mMASziP%#PP@m+C`kHwPOzq$>)adh6OY=ESo%g7p z&HQ2_Wfp9vxM9kwAc_DFn?2r~ys63&j9WKeYN5HSj-#c;0Ii^!v~=s*SiGXjUvn); z1{fc*78B>L0Zs&*MWbFaP3t;Togz0@GR0%&EEV61AFdHTsPvrx9c~kDaDqL1%1GTI756 z>S}8N**ZY?DSS4k)Xmli6de8_NXKf=TvlrPRRpRX;-~VMDyYqV&W7$P9oMHGVEpX} zqz1>|*qLitl~Eob`Qy)!*}BD!4IdOc#!l^_AX;oWQuhkCn8VSdn&wgiqy|J@lYzj?4Jb;Xa2DC4zqo(%nWBiZpo$?~fB?%{qG=f$La z)>ySgp}WqY-2{*p2+6sy%@n)y=nfPV!-|{po364n>n>Rg+Ens3()b9cDrV$fBI1!~ zxCl^wngOSTWnXun?g-|B*h+rf5R%;Tg%J*Rd9!#k7#uf8H-S+)V`nvJK&}e->&5CF zf^Ru8%2o#GZGcBVn!hD^volvA)2Wo28yS77Vv*4huDo9jCk}2!)Dq}M0xRB>ovtk= z5utZ)LvzhXq*YngkLBcV>$E-ErhMG&oHpvu*tzqH!rh5^3-_UPRj@FIf5e$-%Z>5l zJwvEgMI|=tFIF4x?zNY1rwp-r;Ja1iKH7G5wESIZ;)MLo$xyQ z(#i=1Uqx(StVw)A6zzl>gkC)!&6V4bluE^Mj}~(GRrZ(lY9ziQcNh(@50h>UNc&=s zNJxguNY!2c`q{h896k(kl6PT~<2ZT$dI|EQ?KdN+=XGvCilN0_vbt+o@m|fRAYxzM zps&J0-xI@+KR|qRziUmIcVx?0Icjdqb}T-fA53f8vEFzx=Ucu{8{8Gf;Uw7=jYmx| zF2yw%?;`O?b#zKak+m~UW+mf612wLM_@oe0H7SO}MBJ&6F+7#48i~0xF!e?#c(?Bi z)D&h8k*0d|1~enDTksIk-Itq4(6Zqvr9Tz*km(LWNcGb64F_~*nmA4$*Ahr8vgPnK z#yUyrJT+zQy0B+vHtzDz3^hzIDR!W`jp4ZLZ_KqWI?bBKw7Bj6=;6?;N+ zNo0R6uzFV+L~@u{-g2RuIEq-E0uSYvZOH7oe06(#v93^Zeb?fBFxpqF0!LuaPHMLy11JpeGx#R+3CSSQsmgtp}mh-=tC%Oh#wW65TjC zaZgFu8@qbzW$AQx8B-@yIbcm0n8eSdu2!xc3Cb-<;zj`oNsPZLFu0zHd%Fa*8%Z80b#p=oz6 zxSzE!w_4zj(_FNOUeIG?tv8t;r6NT<0KvCohje);lI}@qocC!b?yH>4ZO`t{uXGTp zDGlDfm48F%D}->zkdeI|7?L7MlN;%s-b+hEW~$m-ox0-5I$);~*_{T}iDFsFQ+YL$?JSd5dndJ34QOmBJzzpEy)7}E}r z8;8)-dqtn5wrZa+nm_dYYi=O~Ob`4(1@iS>#7V>l->X6`+e8*$vqHt3jGMkfAv|Q< znS_OzK1#f^;sb~1=Z)ygSekW+0`BNdUAyr}oE|bpRCO0IL{pU*l5_Fh@HIzg>Ti)` zoW!kf@v*-TEVCH7V>%cM_r9Z`#=cmK_67M|iD2-cpU`^?AO5wrgA+gQ`Ng@fP(fA; zcfh$;7{Iw!u3z*5J8LsTeTUx>@lA`Cmg^Ki2^a$d6>;Z(+m(IT4vK~BxJfJ@Mx9N9 zaV^Xp&uAG03j*~lr9wvc@aln5=&Djf01eyK;#*~$ zIAzcgjuYfpWJG8WF$ooOXa6?}jj0t&NQ7;8;96x?YIE$P>e5`pZTeCo=kvq6=@_pg z)Ze+*79<|nFP;S~D}VRlUXaM3roG9e^z#m|sy0^$B-Xcce4~1KV{GC+H76A4A8uB9 z*)BGGrMCMOw^U>|X?OI~F6rExnC2pp=Q_a8rRxA0%i26Ism1@ZVS z`0IH|&4gb;q2rd7&WAXBH#*R!lD@8=!G&I}$%j)_S~z6((3=O961=$`KCLgWfRZb|f zJJb-P=BM=w^?h4#S`Xo=_q$TS$?2j)A9u}wlaoZLp+4U_lNVmT2v&&!;!gVUP9FfH z8|UBO>C3!pe_Fe|klRd)9+K)3KnWb1FSe|yoi&>gU1AkN7U>Q^k3>U%NB?%uGZ}9x ziUgT#N@zt&#TN#@JqU^1^mUOot34AhHfPR!naauAaV z`cTUm>FZisRqLthMnP$B^G1i=kgft$TA2p!Mp4yeAUou;E!Ic`OfeHk6gXEr6Q}!a zp9+f4<|`@7G850L(q4QPPQfEHm(rSv3b@iK`z{ke0Hg7AQnuA=j)y+h!bPo2Ix!!V z>F7553JA{2HTfankE7WeRai+>$Z_`f^a6loFO(G3H~mU@LsV*ezd>roR_GbfV-lPE z){AOywzjM!dIwst5t?l7LKDEhblK|AMSSLm>d&Bm{a7J9cd4KNUM1kj#J}=l|67F~ zaP<7273jBL>dr$#LINNJ0oTuh0l3w3G_^3GwKLQ=G_`T~vzEOyE!F7o*g5rxj1+AZ z>jd*8Gbmtz3;|h=I{gk6@E|}m0Py!KU=$!g{r&_Fu)Tj3fPmYJA|t9ONGmBTM*s4$ zGZvJMA2m}3Tt9y7&+8lM#D~@Z*X95Ce|;{i&n^?yc>N|Hk>_ zzx?M3fWpt8N;}~G#}ofn!IJN%IMw~^5`wE{#`{uK3pSvM131EU-O-VER({)r2?Qw0R``~q48An0IW zt!wZf$-tL@gVtR32!Li10MwV|FMvD%&&WRk0=^tBu7ZvZCID$=Q~jUfB`+Bw&k#ou ze&kgK@K-^9jkUUcfAp0m`ZE5(%+S=UF=GRSgP+}I7576vq z0n`+5`sz=Gk?0?wRRD$uNYwem_I{1KbEs-x1VCE^TmYay6@0)i_(hvY{>SQmjh9x8 zaWf5QV>UoqjKAVR(fu0F%HBcO!s5T#a2ia5012R{UI7LZfu`m)9^lPvy3r%?MP`oE3z7bzECLch#$_Y)da^Piyqn(^)> z>dRy?KT#140dnX6>ZAXdI_4$d4}c$5_M`kr5A(Cw0095gseS~Zy?pp(vY4L_XZ-!) z|K#iV@3X;|@Gld9{Dh}6`CHxo1OGcKiI+q#Mf`sfjamGS=uh?kFOvQ*;a{rJ|AaTR z`X~4oljWDNFGbjY!uDAI6YQV;3@hF1 zyDzK%Qa|)35$=cICHkXz{;ekZlHjFS<4*!akKZ8p2kFL_OfMzfeljWf{07r+3B0}J zd@0oRlk?o~H#q-Ew(BL;OT~|$RH$LULG=&%A1|3+Dl7bCLjUv|On;bz;4gI+UQ)e` zX#YtC9sS!>fbqrV&nkQ`NnQpD|0KbP`)!gx`s<%mQ(m&Xj7a^-f|~H#EPswqeM#^# z9`Gl@P164-!T)NpmjPNosU`udwin;HKMn8y9@PG4TfL0b`H4@N@f-O69Jlkb-ChPZ z{3J8Z{@-N3o@Bk;f&Ph!RP#5?e>KP7_oH9d^QHIuC+2?h|Hk~MZ`4bV;!ot$w!b6) zzJKv=zu<+J>nBfl$KQBfPCNf(M*rh8{?haElP00-|E6^R%@6aRI{(M(@@J>X53kG5 fvI_wFZ$6l}(qMpf(2wXvCZP9#QwYM{KmPiEB|XYR literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..74bb77845 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +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 +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +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 + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..ef961960e --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "pal-tracker" \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/EnvController.java b/src/main/java/io/pivotal/pal/tracker/EnvController.java new file mode 100644 index 000000000..297eeed2c --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/EnvController.java @@ -0,0 +1,8 @@ +package io.pivotal.pal.tracker; + +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class EnvController { + +} diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java new file mode 100644 index 000000000..554360cb5 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -0,0 +1,11 @@ +package io.pivotal.pal.tracker; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PalTrackerApplication { + public static void main(String[] args) { + SpringApplication.run(PalTrackerApplication.class, args); + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java new file mode 100644 index 000000000..5fea27b71 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java @@ -0,0 +1,16 @@ +package io.pivotal.pal.tracker; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WelcomeController { + @Value("${WELCOME_MESSAGE}") + String hello = "hello"; + + @GetMapping("/") + public String sayHello(){ + return hello; + } +} From fead739f7b3cfca916d51a593ad007c54d24f723 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 13:56:50 -0600 Subject: [PATCH 03/28] Add tests for deployment lab --- .../pal/tracker/EnvControllerTest.java | 28 +++++++++++++++++++ .../pal/tracker/WelcomeControllerTest.java | 16 +++++++++++ .../pal/trackerapi/WelcomeApiTest.java | 26 +++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java b/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java new file mode 100644 index 000000000..fda0f0f34 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java @@ -0,0 +1,28 @@ +package test.pivotal.pal.tracker; + +import org.junit.Test; + +import java.util.Map; +import io.pivotal.pal.tracker.EnvController; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EnvControllerTest { + @Test + public void getEnv() throws Exception { + EnvController controller = new EnvController( + "8675", + "12G", + "34", + "123.sesame.street" + ); + + Map env = controller.getEnv(); + + assertThat(env.get("PORT")).isEqualTo("8675"); + assertThat(env.get("MEMORY_LIMIT")).isEqualTo("12G"); + assertThat(env.get("CF_INSTANCE_INDEX")).isEqualTo("34"); + assertThat(env.get("CF_INSTANCE_ADDR")).isEqualTo("123.sesame.street"); + } + +} diff --git a/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java b/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java new file mode 100644 index 000000000..bfa8271a0 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java @@ -0,0 +1,16 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.WelcomeController; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WelcomeControllerTest { + + @Test + public void itSaysHello() throws Exception { + WelcomeController controller = new WelcomeController("A welcome message"); + + assertThat(controller.sayHello()).isEqualTo("A welcome message"); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java new file mode 100644 index 000000000..cc7091ed4 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java @@ -0,0 +1,26 @@ +package test.pivotal.pal.trackerapi; + +import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class WelcomeApiTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void exampleTest() { + String body = this.restTemplate.getForObject("/", String.class); + assertThat(body).isEqualTo("Hello from test"); + } +} From 77837864aed690d23fae9a2b7658d22b944de30f Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 10:46:31 -0700 Subject: [PATCH 04/28] pal-tracker assignment completion 2 --- build.gradle | 7 +++++- .../io/pivotal/pal/tracker/EnvController.java | 25 +++++++++++++++++++ .../pal/tracker/WelcomeController.java | 11 +++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index b16c5860d..8bebfa603 100644 --- a/build.gradle +++ b/build.gradle @@ -9,10 +9,15 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") + testCompile("org.springframework.boot:spring-boot-starter-test") } bootRun.environment([ - "WELCOME_MESSAGE": "hello", + "WELCOME_MESSAGE": "A welcome message", + "PORT": "8675", + "MEMORY_LIMIT": "12G", + "CF_INSTANCE_INDEX": "34", + "CF_INSTANCE_ADDR": "123.sesame.street", ]) test.environment([ diff --git a/src/main/java/io/pivotal/pal/tracker/EnvController.java b/src/main/java/io/pivotal/pal/tracker/EnvController.java index 297eeed2c..712675ac1 100644 --- a/src/main/java/io/pivotal/pal/tracker/EnvController.java +++ b/src/main/java/io/pivotal/pal/tracker/EnvController.java @@ -1,8 +1,33 @@ package io.pivotal.pal.tracker; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; +import java.util.Map; + @RestController public class EnvController { + private Map env; + + public EnvController( + @Value("${PORT:NOT SET}") String port, + @Value("${MEMORY_LIMIT:NOT SET}") String memoryLimit, + @Value("${CF_INSTANCE_INDEX:NOT SET}") String cfInstancceIndex, + @Value("${CF_INSTANCE_ADDR:NOT SET}") String cfInstanceAddr + ) + { + env = new HashMap<>(); + env.put("PORT", port); + env.put("MEMORY_LIMIT", memoryLimit); + env.put("CF_INSTANCE_INDEX", cfInstancceIndex); + env.put("CF_INSTANCE_ADDR", cfInstanceAddr); + } + + @GetMapping("/env") + public Map getEnv() { + return env; + } } diff --git a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java index 5fea27b71..bbd332039 100644 --- a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java +++ b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java @@ -6,11 +6,16 @@ @RestController public class WelcomeController { - @Value("${WELCOME_MESSAGE}") - String hello = "hello"; + + String _message; + + + public WelcomeController(@Value("${WELCOME_MESSAGE}") String message) { + _message = message; + } @GetMapping("/") public String sayHello(){ - return hello; + return _message; } } From 699fc811ead8f67691fbf6d2bc7e4b6bb50ce9e1 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 10:51:03 -0700 Subject: [PATCH 05/28] added manifest.yml to simplify push. --- manifest.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 manifest.yml diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 000000000..7f06f91ee --- /dev/null +++ b/manifest.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + random-route: true + env: + WELCOME_MESSAGE: Hello from Cloud Foundry \ No newline at end of file From e53e24b395bc05770e7c32904054d5b4b0a82b5e Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 12:31:56 -0700 Subject: [PATCH 06/28] Concourse pipeline code added. --- .gitignore | 1 + ci/build.yml | 22 ++++++++++++++++++++++ ci/pipeline.yml | 31 +++++++++++++++++++++++++++++++ ci/variables.example.yml | 9 +++++++++ manifest.yml | 2 -- 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 ci/build.yml create mode 100644 ci/pipeline.yml create mode 100644 ci/variables.example.yml diff --git a/.gitignore b/.gitignore index a70b4b956..ba99a46c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build +out .gradle .idea *.iml diff --git a/ci/build.yml b/ci/build.yml new file mode 100644 index 000000000..e735e47d6 --- /dev/null +++ b/ci/build.yml @@ -0,0 +1,22 @@ +platform: linux + +image_resource: + type: docker-image + source: + repository: openjdk + tag: '8-jdk' + +inputs: + - name: pal-tracker + +outputs: + - name: build-output + +run: + path: bash + args: + - -exc + - | + cd pal-tracker + ./gradlew build + cp build/libs/pal-tracker.jar ../build-output \ No newline at end of file diff --git a/ci/pipeline.yml b/ci/pipeline.yml new file mode 100644 index 000000000..01d389043 --- /dev/null +++ b/ci/pipeline.yml @@ -0,0 +1,31 @@ +--- +resources: +- name: pal-tracker + type: git + source: + uri: {{github-repository}} + branch: master + private_key: {{github-private-key}} + +- name: deploy + type: cf + source: + api: {{cf-api-url}} + username: {{cf-username}} + password: {{cf-password}} + organization: {{cf-org}} + space: sandbox + +jobs: +- name: build-and-deploy + plan: + - get: pal-tracker + trigger: true + - task: build and test + file: pal-tracker/ci/build.yml + - put: deploy + params: + manifest: pal-tracker/manifest.yml + path: build-output/pal-tracker.jar + environment_variables: + WELCOME_MESSAGE: "Hello from Concourse" \ No newline at end of file diff --git a/ci/variables.example.yml b/ci/variables.example.yml new file mode 100644 index 000000000..414b9693a --- /dev/null +++ b/ci/variables.example.yml @@ -0,0 +1,9 @@ +cf-api-url: CF_API_URL +cf-username: CF_USERNAME +cf-password: CF_PASSWORD +cf-org: CF_ORG +github-repository: git@github.com:swkarr/pal-tracker.git +github-private-key: | + -----BEGIN RSA PRIVATE KEY----- + REPLACE WITH YOUR PRIVATE KEY HERE + -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/manifest.yml b/manifest.yml index 7f06f91ee..2cefccc45 100644 --- a/manifest.yml +++ b/manifest.yml @@ -3,5 +3,3 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar random-route: true - env: - WELCOME_MESSAGE: Hello from Cloud Foundry \ No newline at end of file From f7424e3d6148df62ca8dbf461a14960bb2e3870d Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 13:37:02 -0700 Subject: [PATCH 07/28] Extend pipeline: publishing to multiple environments. --- ci/build.yml | 3 +- ci/pipeline.yml | 66 +++++++++++++++++++++++++++++++++++----- ci/variables.example.yml | 7 ++++- manifest-production.yml | 5 +++ manifest-review.yml | 5 +++ 5 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 manifest-production.yml create mode 100644 manifest-review.yml diff --git a/ci/build.yml b/ci/build.yml index e735e47d6..1e9e1c094 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -8,6 +8,7 @@ image_resource: inputs: - name: pal-tracker + - name: version outputs: - name: build-output @@ -18,5 +19,5 @@ run: - -exc - | cd pal-tracker - ./gradlew build + ./gradlew -P version=$(cat ../version/number) build cp build/libs/pal-tracker.jar ../build-output \ No newline at end of file diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 01d389043..c34c3b2a9 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -7,25 +7,77 @@ resources: branch: master private_key: {{github-private-key}} -- name: deploy +- name: pal-tracker-artifacts + type: s3 + source: + bucket: {{aws-bucket}} + regexp: releases/pal-tracker-(.*).jar + access_key_id: {{aws-access-key-id}} + secret_access_key: {{aws-secret-access-key}} + +- name: version + type: semver + source: + bucket: {{aws-bucket}} + key: pal-tracker/version + access_key_id: {{aws-access-key-id}} + secret_access_key: {{aws-secret-access-key}} + +- name: review-deployment + type: cf + source: + api: {{cf-api-url}} + username: {{cf-username}} + password: {{cf-password}} + organization: {{cf-org}} + space: review + +- name: production-deployment type: cf source: api: {{cf-api-url}} username: {{cf-username}} password: {{cf-password}} organization: {{cf-org}} - space: sandbox + space: production jobs: -- name: build-and-deploy +- name: build plan: - get: pal-tracker trigger: true + - get: version + params: {bump: patch} - task: build and test file: pal-tracker/ci/build.yml - - put: deploy + - put: pal-tracker-artifacts + params: + file: build-output/pal-tracker-*.jar + - put: version + params: + file: version/number + +- name: deploy-review + plan: + - get: pal-tracker + - get: pal-tracker-artifacts + trigger: true + passed: [build] + - put: review-deployment + params: + manifest: pal-tracker/manifest-review.yml + path: pal-tracker-artifacts/pal-tracker-*.jar + environment_variables: + WELCOME_MESSAGE: "Hello from the review environment" + +- name: deploy-production + plan: + - get: pal-tracker + - get: pal-tracker-artifacts + passed: [deploy-review] + - put: production-deployment params: - manifest: pal-tracker/manifest.yml - path: build-output/pal-tracker.jar + manifest: pal-tracker/manifest-production.yml + path: pal-tracker-artifacts/pal-tracker-*.jar environment_variables: - WELCOME_MESSAGE: "Hello from Concourse" \ No newline at end of file + WELCOME_MESSAGE: "Hello from the production environment" \ No newline at end of file diff --git a/ci/variables.example.yml b/ci/variables.example.yml index 414b9693a..b38bf2598 100644 --- a/ci/variables.example.yml +++ b/ci/variables.example.yml @@ -2,8 +2,13 @@ cf-api-url: CF_API_URL cf-username: CF_USERNAME cf-password: CF_PASSWORD cf-org: CF_ORG + +aws-access-key-id: +aws-secret-access-key: +aws-bucket: + github-repository: git@github.com:swkarr/pal-tracker.git github-private-key: | -----BEGIN RSA PRIVATE KEY----- REPLACE WITH YOUR PRIVATE KEY HERE - -----END RSA PRIVATE KEY----- \ No newline at end of file + -----END RSA PRIVATE KEY----- diff --git a/manifest-production.yml b/manifest-production.yml new file mode 100644 index 000000000..0cf82e4f5 --- /dev/null +++ b/manifest-production.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + host: swk-pal-tracker diff --git a/manifest-review.yml b/manifest-review.yml new file mode 100644 index 000000000..259a6c618 --- /dev/null +++ b/manifest-review.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + host: swk-pal-tracker-review From 7077d7e7bac75912923ab441399d3729a3337abe Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 13:53:46 -0700 Subject: [PATCH 08/28] Fixed to copy the 'versioned' jar. --- ci/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/build.yml b/ci/build.yml index 1e9e1c094..4c72d2f54 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -20,4 +20,4 @@ run: - | cd pal-tracker ./gradlew -P version=$(cat ../version/number) build - cp build/libs/pal-tracker.jar ../build-output \ No newline at end of file + cp build/libs/pal-tracker-*.jar ../build-output \ No newline at end of file From a9f97ac47f3012d709137d615ce16c6e115fc87a Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 15:04:20 -0600 Subject: [PATCH 09/28] Add tests for MVC lab --- .../InMemoryTimeEntryRepositoryTest.java | 71 ++++++++++ .../pal/tracker/TimeEntryControllerTest.java | 106 +++++++++++++++ .../pal/trackerapi/TimeEntryApiTest.java | 126 ++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java create mode 100644 src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java new file mode 100644 index 000000000..c88bb8266 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java @@ -0,0 +1,71 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.InMemoryTimeEntryRepository; +import io.pivotal.pal.tracker.TimeEntry; +import org.junit.Test; + +import java.time.LocalDate; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InMemoryTimeEntryRepositoryTest { + @Test + public void create() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry createdTimeEntry = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); + assertThat(createdTimeEntry).isEqualTo(expected); + + TimeEntry readEntry = repo.find(createdTimeEntry.getId()); + assertThat(readEntry).isEqualTo(expected); + } + + @Test + public void find() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); + TimeEntry readEntry = repo.find(1L); + assertThat(readEntry).isEqualTo(expected); + } + + @Test + public void list() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + repo.create(new TimeEntry(789, 654, LocalDate.parse("2017-01-07"), 4)); + + List expected = asList( + new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8), + new TimeEntry(2L, 789, 654, LocalDate.parse("2017-01-07"), 4) + ); + assertThat(repo.list()).isEqualTo(expected); + } + + @Test + public void update() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry created = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry updatedEntry = repo.update( + created.getId(), + new TimeEntry(321, 654, LocalDate.parse("2017-01-09"), 5)); + + TimeEntry expected = new TimeEntry(created.getId(), 321, 654, LocalDate.parse("2017-01-09"), 5); + assertThat(updatedEntry).isEqualTo(expected); + assertThat(repo.find(created.getId())).isEqualTo(expected); + } + + @Test + public void delete() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry created = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + repo.delete(created.getId()); + assertThat(repo.list()).isEmpty(); + } +} diff --git a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java new file mode 100644 index 000000000..f7c0090e3 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java @@ -0,0 +1,106 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.TimeEntry; +import io.pivotal.pal.tracker.TimeEntryController; +import io.pivotal.pal.tracker.TimeEntryRepository; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDate; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class TimeEntryControllerTest { + private TimeEntryRepository timeEntryRepository; + private TimeEntryController controller; + + @Before + public void setUp() throws Exception { + timeEntryRepository = mock(TimeEntryRepository.class); + controller = new TimeEntryController(timeEntryRepository); + } + + @Test + public void testCreate() throws Exception { + TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); + doReturn(expected) + .when(timeEntryRepository) + .create(any(TimeEntry.class)); + + ResponseEntity response = controller.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testRead() throws Exception { + TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); + doReturn(expected) + .when(timeEntryRepository) + .find(1L); + + ResponseEntity response = controller.read(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testRead_NotFound() throws Exception { + doReturn(null) + .when(timeEntryRepository) + .find(1L); + + ResponseEntity response = controller.read(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void testList() throws Exception { + List expected = asList( + new TimeEntry(1, 123, 456, LocalDate.parse("2017-01-08"), 8), + new TimeEntry(2, 789, 321, LocalDate.parse("2017-01-07"), 4) + ); + doReturn(expected).when(timeEntryRepository).list(); + + ResponseEntity> response = controller.list(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testUpdate() throws Exception { + TimeEntry expected = new TimeEntry(1, 987, 654, LocalDate.parse("2017-01-07"), 4); + doReturn(expected) + .when(timeEntryRepository) + .update(eq(1L), any(TimeEntry.class)); + + ResponseEntity response = controller.update(1L, expected); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testUpdate_NotFound() throws Exception { + doReturn(null) + .when(timeEntryRepository) + .update(eq(1L), any(TimeEntry.class)); + + ResponseEntity response = controller.update(1L, new TimeEntry()); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void testDelete() throws Exception { + ResponseEntity response = controller.delete(1L); + verify(timeEntryRepository).delete(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java new file mode 100644 index 000000000..b742e5447 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -0,0 +1,126 @@ +package test.pivotal.pal.trackerapi; + +import com.jayway.jsonpath.DocumentContext; +import io.pivotal.pal.tracker.PalTrackerApplication; +import io.pivotal.pal.tracker.TimeEntry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import java.time.LocalDate; +import java.util.Collection; + +import static com.jayway.jsonpath.JsonPath.parse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class TimeEntryApiTest { + + @Autowired + private TestRestTemplate restTemplate; + + private TimeEntry timeEntry = new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8); + + @Test + public void testCreate() throws Exception { + ResponseEntity createResponse = restTemplate.postForEntity("/time-entries", timeEntry, String.class); + + + assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + DocumentContext createJson = parse(createResponse.getBody()); + assertThat(createJson.read("$.id", Long.class)).isGreaterThan(0); + assertThat(createJson.read("$.projectId", Long.class)).isEqualTo(123L); + assertThat(createJson.read("$.userId", Long.class)).isEqualTo(456L); + assertThat(createJson.read("$.date", String.class)).isEqualTo("2017-01-08"); + assertThat(createJson.read("$.hours", Long.class)).isEqualTo(8); + } + + @Test + public void testList() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity listResponse = restTemplate.getForEntity("/time-entries", String.class); + + + assertThat(listResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext listJson = parse(listResponse.getBody()); + + Collection timeEntries = listJson.read("$[*]", Collection.class); + assertThat(timeEntries.size()).isEqualTo(1); + + Long readId = listJson.read("$[0].id", Long.class); + assertThat(readId).isEqualTo(id); + } + + @Test + public void testRead() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity readResponse = this.restTemplate.getForEntity("/time-entries/" + id, String.class); + + + assertThat(readResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + DocumentContext readJson = parse(readResponse.getBody()); + assertThat(readJson.read("$.id", Long.class)).isEqualTo(id); + assertThat(readJson.read("$.projectId", Long.class)).isEqualTo(123L); + assertThat(readJson.read("$.userId", Long.class)).isEqualTo(456L); + assertThat(readJson.read("$.date", String.class)).isEqualTo("2017-01-08"); + assertThat(readJson.read("$.hours", Long.class)).isEqualTo(8); + } + + @Test + public void testUpdate() throws Exception { + Long id = createTimeEntry(); + TimeEntry updatedTimeEntry = new TimeEntry(2, 3, LocalDate.parse("2017-01-09"), 9); + + + ResponseEntity updateResponse = restTemplate.exchange("/time-entries/" + id, HttpMethod.PUT, new HttpEntity<>(updatedTimeEntry, null), String.class); + + + assertThat(updateResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext updateJson = parse(updateResponse.getBody()); + assertThat(updateJson.read("$.id", Long.class)).isEqualTo(id); + assertThat(updateJson.read("$.projectId", Long.class)).isEqualTo(2L); + assertThat(updateJson.read("$.userId", Long.class)).isEqualTo(3L); + assertThat(updateJson.read("$.date", String.class)).isEqualTo("2017-01-09"); + assertThat(updateJson.read("$.hours", Long.class)).isEqualTo(9); + } + + @Test + public void testDelete() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity deleteResponse = restTemplate.exchange("/time-entries/" + id, HttpMethod.DELETE, null, String.class); + + + assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + + ResponseEntity deletedReadResponse = this.restTemplate.getForEntity("/time-entries/" + id, String.class); + assertThat(deletedReadResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + private Long createTimeEntry() { + HttpEntity entity = new HttpEntity<>(timeEntry); + + ResponseEntity response = restTemplate.exchange("/time-entries", HttpMethod.POST, entity, TimeEntry.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + return response.getBody().getId(); + } +} From b23544f09b6995efd2658850c50956b1ff477233 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 15:21:18 -0700 Subject: [PATCH 10/28] Add TimeEntry and in memory repository for Entries. --- build.gradle | 1 + .../tracker/InMemoryTimeEntryRepository.java | 41 +++++++++ .../io/pivotal/pal/tracker/TimeEntry.java | 87 +++++++++++++++++++ .../pal/tracker/TimeEntryRepository.java | 11 +++ 4 files changed, 140 insertions(+) create mode 100644 src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntry.java create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java diff --git a/build.gradle b/build.gradle index 8bebfa603..547140351 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") + compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") testCompile("org.springframework.boot:spring-boot-starter-test") } diff --git a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java new file mode 100644 index 000000000..304912d7b --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java @@ -0,0 +1,41 @@ +package io.pivotal.pal.tracker; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class InMemoryTimeEntryRepository implements TimeEntryRepository { + private Map db = new HashMap<>(); + + @Override + public TimeEntry create(TimeEntry timeEntry) { + assert (timeEntry.getId() == 0); + long id = db.size() + 1; + timeEntry.setId(id); + db.put(id, timeEntry); + return null; + } + + @Override + public TimeEntry find(Long id) { + return db.get(id); + } + + @Override + public List list() { + return new ArrayList<>(db.values()); + } + + @Override + public TimeEntry update(Long id, TimeEntry timeEntry) { + db.replace(id, timeEntry); + timeEntry.setId(id); + return timeEntry; + } + + @Override + public void delete(Long id) { + db.remove(id); + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java new file mode 100644 index 000000000..95741ccc8 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java @@ -0,0 +1,87 @@ +package io.pivotal.pal.tracker; + +import java.time.LocalDate; + +public class TimeEntry { + private long id; + private long projectId; + private long userId; + private LocalDate date; + private int hours; + + public TimeEntry() { + id = 0; + } + + public TimeEntry(long projectId, long userId, LocalDate date, int hours) { + this(); + this.projectId = projectId; + this.userId = userId; + this.date = date; + this.hours = hours; + } + + public TimeEntry(long id, long projectId, long userId, LocalDate date, int hours) { + this(projectId, userId, date, hours); + this.id = id; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getProjectId() { + return projectId; + } + + public long getUserId() { + return userId; + } + + public LocalDate getDate() { + return date; + } + + public int getHours() { + return hours; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TimeEntry timeEntry = (TimeEntry) o; + + if (id != timeEntry.id) return false; + if (projectId != timeEntry.projectId) return false; + if (userId != timeEntry.userId) return false; + if (hours != timeEntry.hours) return false; + return date != null ? date.equals(timeEntry.date) : timeEntry.date == null; + } + + @Override + public int hashCode() { + int result = (int) (id ^ (id >>> 32)); + result = 31 * result + (int) (projectId ^ (projectId >>> 32)); + result = 31 * result + (int) (userId ^ (userId >>> 32)); + result = 31 * result + (date != null ? date.hashCode() : 0); + result = 31 * result + hours; + return result; + } + + @Override + public String toString() { + return "TimeEntry{" + + "id=" + id + + ", projectId=" + projectId + + ", userId=" + userId + + ", date=" + date + + ", hours=" + hours + + '}'; + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java new file mode 100644 index 000000000..866097db4 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java @@ -0,0 +1,11 @@ +package io.pivotal.pal.tracker; + +import java.util.List; + +public interface TimeEntryRepository { + TimeEntry create(TimeEntry timeEntry); + TimeEntry find(Long id); + List list(); + TimeEntry update(Long id, TimeEntry timeEntry); + void delete(Long id); +} \ No newline at end of file From 9f90874a45cd8843e1c9685416e1377379088a67 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 15:22:33 -0700 Subject: [PATCH 11/28] Added object deserialization of dates to the Pal Tracker Application --- .../pal/tracker/PalTrackerApplication.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index 554360cb5..550a95201 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -1,11 +1,31 @@ package io.pivotal.pal.tracker; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @SpringBootApplication public class PalTrackerApplication { public static void main(String[] args) { SpringApplication.run(PalTrackerApplication.class, args); } + + @Bean + TimeEntryRepository timeEntryRepository() { + return new InMemoryTimeEntryRepository(); + } + + @Bean + public ObjectMapper jsonObjectMapper() { + return Jackson2ObjectMapperBuilder.json() + .serializationInclusion(JsonInclude.Include.NON_NULL) // Don’t include null values + .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) //ISODate + .modules(new JavaTimeModule()) + .build(); + } } From d3c6535b1b4a28cebd92a1221ced04fbcb7d276e Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Tue, 17 Oct 2017 16:01:15 -0700 Subject: [PATCH 12/28] Got Controller and other tests to pass --- .../tracker/InMemoryTimeEntryRepository.java | 3 +- .../io/pivotal/pal/tracker/TimeEntry.java | 4 +- .../pal/tracker/TimeEntryController.java | 57 +++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryController.java diff --git a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java index 304912d7b..c6a89195e 100644 --- a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java @@ -10,11 +10,10 @@ public class InMemoryTimeEntryRepository implements TimeEntryRepository { @Override public TimeEntry create(TimeEntry timeEntry) { - assert (timeEntry.getId() == 0); long id = db.size() + 1; timeEntry.setId(id); db.put(id, timeEntry); - return null; + return timeEntry; } @Override diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java index 95741ccc8..2c9976345 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java @@ -9,9 +9,7 @@ public class TimeEntry { private LocalDate date; private int hours; - public TimeEntry() { - id = 0; - } + public TimeEntry() {} public TimeEntry(long projectId, long userId, LocalDate date, int hours) { this(); diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java new file mode 100644 index 000000000..1f460951a --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -0,0 +1,57 @@ +package io.pivotal.pal.tracker; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/time-entries") +public class TimeEntryController { + + private TimeEntryRepository timeEntriesRepo; + + public TimeEntryController(TimeEntryRepository timeEntriesRepo) { + this.timeEntriesRepo = timeEntriesRepo; + } + + @PostMapping + public ResponseEntity create(@RequestBody TimeEntry timeEntry) { + TimeEntry createdTimeEntry = timeEntriesRepo.create(timeEntry); + + return new ResponseEntity<>(createdTimeEntry, HttpStatus.CREATED); + } + + @GetMapping("{id}") + public ResponseEntity read(@PathVariable Long id) { + TimeEntry timeEntry = timeEntriesRepo.find(id); + if (timeEntry != null) { + return new ResponseEntity<>(timeEntry, HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @GetMapping + public ResponseEntity> list() { + return new ResponseEntity<>(timeEntriesRepo.list(), HttpStatus.OK); + } + + @PutMapping("{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody TimeEntry timeEntry) { + TimeEntry updatedTimeEntry = timeEntriesRepo.update(id, timeEntry); + if (updatedTimeEntry != null) { + return new ResponseEntity<>(updatedTimeEntry, HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @DeleteMapping("{id}") + public ResponseEntity delete(@PathVariable Long id) { + timeEntriesRepo.delete(id); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} \ No newline at end of file From e019c52011e1132af17cf311fb78b0a7798eebca Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Wed, 18 Oct 2017 21:13:53 -0700 Subject: [PATCH 13/28] create database and initial migration script --- databases/tracker/create_databases.sql | 0 databases/tracker/migrations/V1__initial_schema.sql | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 databases/tracker/create_databases.sql create mode 100644 databases/tracker/migrations/V1__initial_schema.sql diff --git a/databases/tracker/create_databases.sql b/databases/tracker/create_databases.sql new file mode 100644 index 000000000..e69de29bb diff --git a/databases/tracker/migrations/V1__initial_schema.sql b/databases/tracker/migrations/V1__initial_schema.sql new file mode 100644 index 000000000..e69de29bb From 31a57d16609d38799705cc199124cdb5ba4c9c36 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Wed, 18 Oct 2017 21:14:13 -0700 Subject: [PATCH 14/28] create database and initial migration script --- databases/tracker/create_databases.sql | 10 ++++++++++ databases/tracker/migrations/V1__initial_schema.sql | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/databases/tracker/create_databases.sql b/databases/tracker/create_databases.sql index e69de29bb..6f1e86abf 100644 --- a/databases/tracker/create_databases.sql +++ b/databases/tracker/create_databases.sql @@ -0,0 +1,10 @@ +DROP DATABASE IF EXISTS tracker_dev; +DROP DATABASE IF EXISTS tracker_test; + +CREATE DATABASE tracker_dev; +CREATE DATABASE tracker_test; + +CREATE USER IF NOT EXISTS 'tracker'@'localhost' + IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON tracker_dev.* TO 'tracker' @'localhost'; +GRANT ALL PRIVILEGES ON tracker_test.* TO 'tracker' @'localhost'; \ No newline at end of file diff --git a/databases/tracker/migrations/V1__initial_schema.sql b/databases/tracker/migrations/V1__initial_schema.sql index e69de29bb..daca8c4e3 100644 --- a/databases/tracker/migrations/V1__initial_schema.sql +++ b/databases/tracker/migrations/V1__initial_schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE time_entries ( + id BIGINT(20) NOT NULL AUTO_INCREMENT, + project_id BIGINT(20), + user_id BIGINT(20), + date DATE, + hours INT, + + PRIMARY KEY (id) +) + ENGINE = innodb + DEFAULT CHARSET = utf8; \ No newline at end of file From 3fedf6209c4b3389fa59e12cce8637c5a06f865a Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 11:45:04 -0600 Subject: [PATCH 15/28] Add tests for JDBC lab --- .../tracker/JdbcTimeEntryRepositoryTest.java | 159 ++++++++++++++++++ .../pal/trackerapi/TimeEntryApiTest.java | 12 ++ 2 files changed, 171 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java new file mode 100644 index 000000000..e1eac20fc --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java @@ -0,0 +1,159 @@ +package test.pivotal.pal.tracker; + + +import com.mysql.cj.jdbc.MysqlDataSource; +import io.pivotal.pal.tracker.JdbcTimeEntryRepository; +import io.pivotal.pal.tracker.TimeEntry; +import io.pivotal.pal.tracker.TimeEntryRepository; +import org.junit.Before; +import org.junit.Test; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Date; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JdbcTimeEntryRepositoryTest { + private TimeEntryRepository subject; + private JdbcTemplate jdbcTemplate; + + @Before + public void setUp() throws Exception { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); + + subject = new JdbcTimeEntryRepository(dataSource); + + jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("DELETE FROM time_entries"); + + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + + @Test + public void createInsertsATimeEntryRecord() throws Exception { + TimeEntry newTimeEntry = new TimeEntry(123, 321, LocalDate.parse("2017-01-09"), 8); + TimeEntry entry = subject.create(newTimeEntry); + + Map foundEntry = jdbcTemplate.queryForMap("Select * from time_entries where id = ?", entry.getId()); + + assertThat(foundEntry.get("id")).isEqualTo(entry.getId()); + assertThat(foundEntry.get("project_id")).isEqualTo(123L); + assertThat(foundEntry.get("user_id")).isEqualTo(321L); + assertThat(((Date)foundEntry.get("date")).toLocalDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(foundEntry.get("hours")).isEqualTo(8); + } + + @Test + public void createReturnsTheCreatedTimeEntry() throws Exception { + TimeEntry newTimeEntry = new TimeEntry(123, 321, LocalDate.parse("2017-01-09"), 8); + TimeEntry entry = subject.create(newTimeEntry); + + assertThat(entry.getId()).isNotNull(); + assertThat(entry.getProjectId()).isEqualTo(123); + assertThat(entry.getUserId()).isEqualTo(321); + assertThat(entry.getDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(entry.getHours()).isEqualTo(8); + } + + @Test + public void findFindsATimeEntry() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, '2017-01-09', 8)" + ); + + TimeEntry timeEntry = subject.find(999L); + + assertThat(timeEntry.getId()).isEqualTo(999L); + assertThat(timeEntry.getProjectId()).isEqualTo(123L); + assertThat(timeEntry.getUserId()).isEqualTo(321L); + assertThat(timeEntry.getDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(timeEntry.getHours()).isEqualTo(8); + } + + @Test + public void findReturnsNullWhenNotFound() throws Exception { + TimeEntry timeEntry = subject.find(999L); + + assertThat(timeEntry).isNull(); + } + + @Test + public void listFindsAllTimeEntries() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, '2017-01-09', 8), (888, 456, 678, '2017-01-08', 9)" + ); + + List timeEntries = subject.list(); + assertThat(timeEntries.size()).isEqualTo(2); + + TimeEntry timeEntry = timeEntries.get(0); + assertThat(timeEntry.getId()).isEqualTo(888L); + assertThat(timeEntry.getProjectId()).isEqualTo(456L); + assertThat(timeEntry.getUserId()).isEqualTo(678L); + assertThat(timeEntry.getDate()).isEqualTo(LocalDate.parse("2017-01-08")); + assertThat(timeEntry.getHours()).isEqualTo(9); + + timeEntry = timeEntries.get(1); + assertThat(timeEntry.getId()).isEqualTo(999L); + assertThat(timeEntry.getProjectId()).isEqualTo(123L); + assertThat(timeEntry.getUserId()).isEqualTo(321L); + assertThat(timeEntry.getDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(timeEntry.getHours()).isEqualTo(8); + } + + @Test + public void updateReturnsTheUpdatedRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (1000, 123, 321, '2017-01-09', 8)"); + + TimeEntry timeEntryUpdates = new TimeEntry(456, 987, LocalDate.parse("2017-01-10"), 10); + + TimeEntry updatedTimeEntry = subject.update(1000L, timeEntryUpdates); + + assertThat(updatedTimeEntry.getId()).isEqualTo(1000L); + assertThat(updatedTimeEntry.getProjectId()).isEqualTo(456L); + assertThat(updatedTimeEntry.getUserId()).isEqualTo(987L); + assertThat(updatedTimeEntry.getDate()).isEqualTo(LocalDate.parse("2017-01-10")); + assertThat(updatedTimeEntry.getHours()).isEqualTo(10); + } + + @Test + public void updateUpdatesTheRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (1000, 123, 321, '2017-01-09', 8)"); + + TimeEntry updatedTimeEntry = new TimeEntry(456, 322, LocalDate.parse("2017-01-10"), 10); + + TimeEntry timeEntry = subject.update(1000L, updatedTimeEntry); + + Map foundEntry = jdbcTemplate.queryForMap("Select * from time_entries where id = ?", timeEntry.getId()); + + assertThat(foundEntry.get("id")).isEqualTo(timeEntry.getId()); + assertThat(foundEntry.get("project_id")).isEqualTo(456L); + assertThat(foundEntry.get("user_id")).isEqualTo(322L); + assertThat(((Date)foundEntry.get("date")).toLocalDate()).isEqualTo(LocalDate.parse("2017-01-10")); + assertThat(foundEntry.get("hours")).isEqualTo(10); + } + + @Test + public void deleteRemovesTheRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, '2017-01-09', 8)" + ); + + subject.delete(999L); + + Map foundEntry = jdbcTemplate.queryForMap("Select count(*) count from time_entries where id = ?", 999); + assertThat(foundEntry.get("count")).isEqualTo(0L); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index b742e5447..2b7464304 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -1,8 +1,10 @@ package test.pivotal.pal.trackerapi; import com.jayway.jsonpath.DocumentContext; +import com.mysql.cj.jdbc.MysqlDataSource; import io.pivotal.pal.tracker.PalTrackerApplication; import io.pivotal.pal.tracker.TimeEntry; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -12,6 +14,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.junit4.SpringRunner; import java.time.LocalDate; @@ -30,6 +33,15 @@ public class TimeEntryApiTest { private TimeEntry timeEntry = new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8); + @Before + public void setUp() throws Exception { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("TRUNCATE time_entries"); + } + @Test public void testCreate() throws Exception { ResponseEntity createResponse = restTemplate.postForEntity("/time-entries", timeEntry, String.class); From cc68935ed238e8e5f635af3fc78278998cd43df1 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Wed, 18 Oct 2017 21:52:51 -0700 Subject: [PATCH 16/28] convert to use a JDBC DB for TimeEntryRepository --- build.gradle | 27 ++++-- ci/build.yml | 22 ++++- manifest-production.yml | 2 + manifest-review.yml | 2 + .../pal/tracker/JdbcTimeEntryRepository.java | 86 +++++++++++++++++++ .../pal/tracker/PalTrackerApplication.java | 5 +- 6 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java diff --git a/build.gradle b/build.gradle index 547140351..ef8397592 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,9 @@ +import org.flywaydb.gradle.task.FlywayMigrateTask + plugins { id "java" id "org.springframework.boot" version "1.5.4.RELEASE" + id "org.flywaydb.flyway" version "4.2.0" } repositories { @@ -10,17 +13,31 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") + compile("org.springframework.boot:spring-boot-starter-jdbc") + compile("mysql:mysql-connector-java:6.0.6") + testCompile("org.springframework.boot:spring-boot-starter-test") } +def developmentDbUrl = "jdbc:mysql://localhost:3306/tracker_dev?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" bootRun.environment([ - "WELCOME_MESSAGE": "A welcome message", - "PORT": "8675", - "MEMORY_LIMIT": "12G", - "CF_INSTANCE_INDEX": "34", - "CF_INSTANCE_ADDR": "123.sesame.street", + "WELCOME_MESSAGE": "hello", + "SPRING_DATASOURCE_URL": developmentDbUrl, ]) +def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" test.environment([ "WELCOME_MESSAGE": "Hello from test", + "SPRING_DATASOURCE_URL": testDbUrl, ]) + +flyway { + url = developmentDbUrl + user = "tracker" + password = "" + locations = ["filesystem:databases/tracker/migrations"] +} + +task testMigrate(type: FlywayMigrateTask) { + url = testDbUrl +} diff --git a/ci/build.yml b/ci/build.yml index 4c72d2f54..0e1e62650 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -18,6 +18,24 @@ run: args: - -exc - | + function stop_mysql { + service mysql stop + } + trap stop_mysql EXIT + + export DEBIAN_FRONTEND="noninteractive" + + apt-get update + apt-get install -y software-properties-common + apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db + apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xF1656F24C74CD1D8 + add-apt-repository 'deb http://ftp.osuosl.org/pub/mariadb/repo/10.2/ubuntu trusty main' + + apt-get -y install mariadb-server + service mysql start + cd pal-tracker - ./gradlew -P version=$(cat ../version/number) build - cp build/libs/pal-tracker-*.jar ../build-output \ No newline at end of file + + mysql -uroot < databases/tracker/create_databases.sql + ./gradlew -P version=$(cat ../version/number) testMigrate build + cp build/libs/pal-tracker-*.jar ../build-output diff --git a/manifest-production.yml b/manifest-production.yml index 0cf82e4f5..dc4af1488 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -3,3 +3,5 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: swk-pal-tracker + services: + - tracker-database \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml index 259a6c618..582d7adc4 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -3,3 +3,5 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: swk-pal-tracker-review + services: + - tracker-database \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java new file mode 100644 index 000000000..1ed2e5ab2 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java @@ -0,0 +1,86 @@ +package io.pivotal.pal.tracker; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; + +import javax.sql.DataSource; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.util.List; + +import static java.sql.Statement.RETURN_GENERATED_KEYS; + +public class JdbcTimeEntryRepository implements TimeEntryRepository { + + private final JdbcTemplate jdbcTemplate; + + public JdbcTimeEntryRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public TimeEntry create(TimeEntry timeEntry) { + KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement sqlStatement = connection.prepareStatement( + "INSERT INTO time_entries (project_id, user_id, date, hours) " + + "VALUES (?, ?, ?, ?)", + RETURN_GENERATED_KEYS + ); + sqlStatement.setLong(1, timeEntry.getProjectId()); + sqlStatement.setLong(2, timeEntry.getUserId()); + sqlStatement.setDate(3, Date.valueOf(timeEntry.getDate())); + sqlStatement.setInt(4, timeEntry.getHours()); + return sqlStatement; + }, generatedKeyHolder); + + return find(generatedKeyHolder.getKey().longValue()); + } + + @Override + public TimeEntry find(Long id) { + return jdbcTemplate.query( + "SELECT id, project_id, user_id, date, hours FROM time_entries WHERE id = ?", + new Object[]{id}, + extractor); + } + + @Override + public List list() { + return jdbcTemplate.query("SELECT id, project_id, user_id, date, hours FROM time_entries", mapper); + } + + @Override + public TimeEntry update(Long id, TimeEntry timeEntry) { + jdbcTemplate.update("UPDATE time_entries " + + "SET project_id = ?, user_id = ?, date = ?, hours = ? " + + "WHERE id = ?", + timeEntry.getProjectId(), + timeEntry.getUserId(), + Date.valueOf(timeEntry.getDate()), + timeEntry.getHours(), + id); + + return find(id); + } + + @Override + public void delete(Long id) { + jdbcTemplate.update("DELETE FROM time_entries WHERE id = ?", id); + } + + private final RowMapper mapper = (rs, rowNum) -> new TimeEntry( + rs.getLong("id"), + rs.getLong("project_id"), + rs.getLong("user_id"), + rs.getDate("date").toLocalDate(), + rs.getInt("hours") + ); + + private final ResultSetExtractor extractor = + (rs) -> rs.next() ? mapper.mapRow(rs, 1) : null; +} \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index 550a95201..afa8c5d03 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.mysql.cj.jdbc.MysqlDataSource; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -17,7 +18,9 @@ public static void main(String[] args) { @Bean TimeEntryRepository timeEntryRepository() { - return new InMemoryTimeEntryRepository(); + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); + return new JdbcTimeEntryRepository(dataSource); } @Bean From b93db5c34560f7c3644f10ff1fd0b8c2adcdb2a4 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Wed, 18 Oct 2017 22:08:46 -0700 Subject: [PATCH 17/28] Replacing JDBC URL from my env --- build.gradle | 4 ++-- ci/build.yml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index ef8397592..21f2545b9 100644 --- a/build.gradle +++ b/build.gradle @@ -19,13 +19,13 @@ dependencies { testCompile("org.springframework.boot:spring-boot-starter-test") } -def developmentDbUrl = "jdbc:mysql://localhost:3306/tracker_dev?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" +def developmentDbUrl = "jdbc:mysql://10.0.4.41:3306/cf_2ffdb61f_651b_49ca_acff_de58d4c11088?user=rpYZJ7R5JbnKeyF5\\u0026password=aq0RP2xS3ay3WKVp" bootRun.environment([ "WELCOME_MESSAGE": "hello", "SPRING_DATASOURCE_URL": developmentDbUrl, ]) -def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" +def testDbUrl = "jdbc:mysql://10.0.4.41:3306/cf_b2530c34_9e68_4955_bb2a_8eea75a8666f?user=5nbNE4ecxqaqh2W9\u0026password=se4UCXKuoRLUvcLS" test.environment([ "WELCOME_MESSAGE": "Hello from test", "SPRING_DATASOURCE_URL": testDbUrl, diff --git a/ci/build.yml b/ci/build.yml index 0e1e62650..b01d4da2d 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -18,6 +18,7 @@ run: args: - -exc - | + function stop_mysql { service mysql stop } @@ -38,4 +39,4 @@ run: mysql -uroot < databases/tracker/create_databases.sql ./gradlew -P version=$(cat ../version/number) testMigrate build - cp build/libs/pal-tracker-*.jar ../build-output + cp build/libs/pal-tracker-*.jar ../build-output \ No newline at end of file From 5f22a0ed62d17650e43121f25dbe28734366ddf3 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Wed, 18 Oct 2017 22:19:44 -0700 Subject: [PATCH 18/28] Rechecking build.gradle --- build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 21f2545b9..4563f9acc 100644 --- a/build.gradle +++ b/build.gradle @@ -14,18 +14,19 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") compile("org.springframework.boot:spring-boot-starter-jdbc") + compile("mysql:mysql-connector-java:6.0.6") testCompile("org.springframework.boot:spring-boot-starter-test") } -def developmentDbUrl = "jdbc:mysql://10.0.4.41:3306/cf_2ffdb61f_651b_49ca_acff_de58d4c11088?user=rpYZJ7R5JbnKeyF5\\u0026password=aq0RP2xS3ay3WKVp" +def developmentDbUrl = "jdbc:mysql://localhost:3306/tracker_dev?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" bootRun.environment([ "WELCOME_MESSAGE": "hello", "SPRING_DATASOURCE_URL": developmentDbUrl, ]) -def testDbUrl = "jdbc:mysql://10.0.4.41:3306/cf_b2530c34_9e68_4955_bb2a_8eea75a8666f?user=5nbNE4ecxqaqh2W9\u0026password=se4UCXKuoRLUvcLS" +def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" test.environment([ "WELCOME_MESSAGE": "Hello from test", "SPRING_DATASOURCE_URL": testDbUrl, @@ -40,4 +41,4 @@ flyway { task testMigrate(type: FlywayMigrateTask) { url = testDbUrl -} +} \ No newline at end of file From 8ce83eaa7fd100a5c6301e4e6150ded614b0c2f2 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Wed, 18 Oct 2017 22:49:30 -0700 Subject: [PATCH 19/28] Still trying to track down the issue --- .../pal/tracker/JdbcTimeEntryRepository.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java index 1ed2e5ab2..67b4370d0 100644 --- a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java @@ -26,16 +26,18 @@ public TimeEntry create(TimeEntry timeEntry) { KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { - PreparedStatement sqlStatement = connection.prepareStatement( + PreparedStatement statement = connection.prepareStatement( "INSERT INTO time_entries (project_id, user_id, date, hours) " + "VALUES (?, ?, ?, ?)", RETURN_GENERATED_KEYS ); - sqlStatement.setLong(1, timeEntry.getProjectId()); - sqlStatement.setLong(2, timeEntry.getUserId()); - sqlStatement.setDate(3, Date.valueOf(timeEntry.getDate())); - sqlStatement.setInt(4, timeEntry.getHours()); - return sqlStatement; + + statement.setLong(1, timeEntry.getProjectId()); + statement.setLong(2, timeEntry.getUserId()); + statement.setDate(3, Date.valueOf(timeEntry.getDate())); + statement.setInt(4, timeEntry.getHours()); + + return statement; }, generatedKeyHolder); return find(generatedKeyHolder.getKey().longValue()); From 61d01c604183917d673afb194e14fff390f70f00 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Thu, 19 Oct 2017 04:20:25 -0700 Subject: [PATCH 20/28] removed extra spaces in GRANT --- databases/tracker/create_databases.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/databases/tracker/create_databases.sql b/databases/tracker/create_databases.sql index 6f1e86abf..f78322915 100644 --- a/databases/tracker/create_databases.sql +++ b/databases/tracker/create_databases.sql @@ -6,5 +6,5 @@ CREATE DATABASE tracker_test; CREATE USER IF NOT EXISTS 'tracker'@'localhost' IDENTIFIED BY ''; -GRANT ALL PRIVILEGES ON tracker_dev.* TO 'tracker' @'localhost'; -GRANT ALL PRIVILEGES ON tracker_test.* TO 'tracker' @'localhost'; \ No newline at end of file +GRANT ALL PRIVILEGES ON tracker_dev.* TO 'tracker'@'localhost'; +GRANT ALL PRIVILEGES ON tracker_test.* TO 'tracker'@'localhost'; \ No newline at end of file From aa404b74a753297953e12ac0c61d32b55496191c Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Thu, 19 Oct 2017 06:48:39 -0700 Subject: [PATCH 21/28] Let Spring wire up the datasource. --- .../java/io/pivotal/pal/tracker/PalTrackerApplication.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index afa8c5d03..1ef776f2f 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -10,6 +10,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import javax.sql.DataSource; + @SpringBootApplication public class PalTrackerApplication { public static void main(String[] args) { @@ -17,9 +19,7 @@ public static void main(String[] args) { } @Bean - TimeEntryRepository timeEntryRepository() { - MysqlDataSource dataSource = new MysqlDataSource(); - dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); + TimeEntryRepository timeEntryRepository(DataSource dataSource) { return new JdbcTimeEntryRepository(dataSource); } From 543fdd47c913e842df4af3be1ee355b1f91aefcd Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 12:28:32 -0600 Subject: [PATCH 22/28] Add tests for Actuator lab --- .../pivotal/pal/trackerapi/HealthApiTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java diff --git a/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java new file mode 100644 index 000000000..b3eef23cc --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java @@ -0,0 +1,38 @@ +package test.pivotal.pal.trackerapi; + +import com.jayway.jsonpath.DocumentContext; +import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import static com.jayway.jsonpath.JsonPath.parse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class HealthApiTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void healthTest() { + ResponseEntity response = this.restTemplate.getForEntity("/health", String.class); + + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext healthJson = parse(response.getBody()); + + assertThat(healthJson.read("$.status", String.class)).isEqualTo("UP"); + assertThat(healthJson.read("$.db.status", String.class)).isEqualTo("UP"); + assertThat(healthJson.read("$.diskSpace.status", String.class)).isEqualTo("UP"); + } +} From cb9cf753ac13d6a29dbb9331a0d9128caf774b18 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Thu, 19 Oct 2017 08:08:43 -0700 Subject: [PATCH 23/28] added an actuator to get metrics --- build.gradle | 4 ++- .../pal/tracker/PalTrackerApplication.java | 5 ++-- .../pal/tracker/TimeEntryController.java | 19 ++++++++++-- .../pal/tracker/TimeEntryHealthIndicator.java | 30 +++++++++++++++++++ .../pal/tracker/TimeEntryControllerTest.java | 9 +++++- 5 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java diff --git a/build.gradle b/build.gradle index 4563f9acc..a272fa57d 100644 --- a/build.gradle +++ b/build.gradle @@ -14,8 +14,8 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") compile("org.springframework.boot:spring-boot-starter-jdbc") - compile("mysql:mysql-connector-java:6.0.6") + compile("org.springframework.boot:spring-boot-starter-actuator") testCompile("org.springframework.boot:spring-boot-starter-test") } @@ -24,12 +24,14 @@ def developmentDbUrl = "jdbc:mysql://localhost:3306/tracker_dev?user=tracker&use bootRun.environment([ "WELCOME_MESSAGE": "hello", "SPRING_DATASOURCE_URL": developmentDbUrl, + "MANAGEMENT_SECURITY_ENABLED": false, ]) def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" test.environment([ "WELCOME_MESSAGE": "Hello from test", "SPRING_DATASOURCE_URL": testDbUrl, + "MANAGEMENT_SECURITY_ENABLED": false, ]) flyway { diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index 1ef776f2f..a892660a9 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -4,8 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.mysql.cj.jdbc.MysqlDataSource; import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @@ -19,7 +20,7 @@ public static void main(String[] args) { } @Bean - TimeEntryRepository timeEntryRepository(DataSource dataSource) { + TimeEntryRepository timeEntryRepository(DataSource dataSource, CounterService counter, GaugeService gauge) { return new JdbcTimeEntryRepository(dataSource); } diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java index 1f460951a..08a625d33 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -1,5 +1,7 @@ package io.pivotal.pal.tracker; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -10,16 +12,23 @@ @RequestMapping("/time-entries") public class TimeEntryController { + private final CounterService counter; + private final GaugeService gauge; private TimeEntryRepository timeEntriesRepo; - public TimeEntryController(TimeEntryRepository timeEntriesRepo) { + public TimeEntryController(TimeEntryRepository timeEntriesRepo, + CounterService counter, + GaugeService gauge) { this.timeEntriesRepo = timeEntriesRepo; + this.counter = counter; + this.gauge = gauge; } @PostMapping public ResponseEntity create(@RequestBody TimeEntry timeEntry) { TimeEntry createdTimeEntry = timeEntriesRepo.create(timeEntry); - + counter.increment("TimeEntry.created"); + gauge.submit("timeEntries.count", timeEntriesRepo.list().size()); return new ResponseEntity<>(createdTimeEntry, HttpStatus.CREATED); } @@ -27,6 +36,7 @@ public ResponseEntity create(@RequestBody TimeEntry timeEntry) { public ResponseEntity read(@PathVariable Long id) { TimeEntry timeEntry = timeEntriesRepo.find(id); if (timeEntry != null) { + counter.increment("TimeEntry.read"); return new ResponseEntity<>(timeEntry, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); @@ -35,6 +45,7 @@ public ResponseEntity read(@PathVariable Long id) { @GetMapping public ResponseEntity> list() { + counter.increment("TimeEntry.listed"); return new ResponseEntity<>(timeEntriesRepo.list(), HttpStatus.OK); } @@ -42,6 +53,7 @@ public ResponseEntity> list() { public ResponseEntity update(@PathVariable Long id, @RequestBody TimeEntry timeEntry) { TimeEntry updatedTimeEntry = timeEntriesRepo.update(id, timeEntry); if (updatedTimeEntry != null) { + counter.increment("TimeEntry.updated"); return new ResponseEntity<>(updatedTimeEntry, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); @@ -51,7 +63,8 @@ public ResponseEntity update(@PathVariable Long id, @RequestBody Time @DeleteMapping("{id}") public ResponseEntity delete(@PathVariable Long id) { timeEntriesRepo.delete(id); - + counter.increment("TimeEntry.deleted"); + gauge.submit("timeEntries.count", timeEntriesRepo.list().size()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java new file mode 100644 index 000000000..5028c6978 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java @@ -0,0 +1,30 @@ +package io.pivotal.pal.tracker; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class TimeEntryHealthIndicator implements HealthIndicator { + + private static final int MAX_TIME_ENTRIES = 5; + private final TimeEntryRepository timeEntryRepo; + + public TimeEntryHealthIndicator(TimeEntryRepository timeEntryRepo) { + this.timeEntryRepo = timeEntryRepo; + } + + @Override + public Health health() { + Health.Builder builder = new Health.Builder(); + + if(timeEntryRepo.list().size() < MAX_TIME_ENTRIES) { + builder.up(); + } else { + builder.down(); + } + + return builder.build(); + } + +} diff --git a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java index f7c0090e3..26c04a916 100644 --- a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java +++ b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java @@ -5,6 +5,8 @@ import io.pivotal.pal.tracker.TimeEntryRepository; import org.junit.Before; import org.junit.Test; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -20,11 +22,16 @@ public class TimeEntryControllerTest { private TimeEntryRepository timeEntryRepository; private TimeEntryController controller; + private CounterService counter; + private GaugeService gauge; + @Before public void setUp() throws Exception { timeEntryRepository = mock(TimeEntryRepository.class); - controller = new TimeEntryController(timeEntryRepository); + counter = mock(CounterService.class); + gauge = mock(GaugeService.class); + controller = new TimeEntryController(timeEntryRepository, counter, gauge); } @Test From 9377b4b613cb8a536926d7b0cb5b2fc12da9304c Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Thu, 19 Oct 2017 09:10:39 -0700 Subject: [PATCH 24/28] Refactor TimeEntry Component and TimeEntryRepository Implementations --- .../pivotal/pal/tracker/PalTrackerApplication.java | 4 +++- .../pal/tracker/{ => timeEntry}/TimeEntry.java | 2 +- .../{ => timeEntry}/TimeEntryController.java | 2 +- .../{ => timeEntry}/TimeEntryHealthIndicator.java | 2 +- .../{ => timeEntry}/TimeEntryRepository.java | 2 +- .../TimeEntryRepository_InMemoryImpl.java} | 4 ++-- .../TimeEntryRepository_JdbcImpl.java} | 8 +++++--- .../tracker/InMemoryTimeEntryRepositoryTest.java | 14 +++++++------- .../pal/tracker/JdbcTimeEntryRepositoryTest.java | 8 ++++---- .../pal/tracker/TimeEntryControllerTest.java | 6 +++--- .../pivotal/pal/trackerapi/TimeEntryApiTest.java | 2 +- 11 files changed, 29 insertions(+), 25 deletions(-) rename src/main/java/io/pivotal/pal/tracker/{ => timeEntry}/TimeEntry.java (98%) rename src/main/java/io/pivotal/pal/tracker/{ => timeEntry}/TimeEntryController.java (98%) rename src/main/java/io/pivotal/pal/tracker/{ => timeEntry}/TimeEntryHealthIndicator.java (94%) rename src/main/java/io/pivotal/pal/tracker/{ => timeEntry}/TimeEntryRepository.java (85%) rename src/main/java/io/pivotal/pal/tracker/{InMemoryTimeEntryRepository.java => timeEntry/TimeEntryRepository_InMemoryImpl.java} (86%) rename src/main/java/io/pivotal/pal/tracker/{JdbcTimeEntryRepository.java => timeEntry/TimeEntryRepository_JdbcImpl.java} (90%) diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index a892660a9..cf1bff7b8 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.pivotal.pal.tracker.timeEntry.TimeEntryRepository_JdbcImpl; +import io.pivotal.pal.tracker.timeEntry.TimeEntryRepository; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.metrics.CounterService; import org.springframework.boot.actuate.metrics.GaugeService; @@ -21,7 +23,7 @@ public static void main(String[] args) { @Bean TimeEntryRepository timeEntryRepository(DataSource dataSource, CounterService counter, GaugeService gauge) { - return new JdbcTimeEntryRepository(dataSource); + return new TimeEntryRepository_JdbcImpl(dataSource); } @Bean diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntry.java similarity index 98% rename from src/main/java/io/pivotal/pal/tracker/TimeEntry.java rename to src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntry.java index 2c9976345..266248da2 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java +++ b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntry.java @@ -1,4 +1,4 @@ -package io.pivotal.pal.tracker; +package io.pivotal.pal.tracker.timeEntry; import java.time.LocalDate; diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryController.java similarity index 98% rename from src/main/java/io/pivotal/pal/tracker/TimeEntryController.java rename to src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryController.java index 08a625d33..fae6520ab 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java +++ b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryController.java @@ -1,4 +1,4 @@ -package io.pivotal.pal.tracker; +package io.pivotal.pal.tracker.timeEntry; import org.springframework.boot.actuate.metrics.CounterService; import org.springframework.boot.actuate.metrics.GaugeService; diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryHealthIndicator.java similarity index 94% rename from src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java rename to src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryHealthIndicator.java index 5028c6978..6fd5e8d47 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java +++ b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryHealthIndicator.java @@ -1,4 +1,4 @@ -package io.pivotal.pal.tracker; +package io.pivotal.pal.tracker.timeEntry; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository.java similarity index 85% rename from src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java rename to src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository.java index 866097db4..3f01b8cc9 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository.java @@ -1,4 +1,4 @@ -package io.pivotal.pal.tracker; +package io.pivotal.pal.tracker.timeEntry; import java.util.List; diff --git a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository_InMemoryImpl.java similarity index 86% rename from src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java rename to src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository_InMemoryImpl.java index c6a89195e..ce9eb10b0 100644 --- a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository_InMemoryImpl.java @@ -1,11 +1,11 @@ -package io.pivotal.pal.tracker; +package io.pivotal.pal.tracker.timeEntry; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -public class InMemoryTimeEntryRepository implements TimeEntryRepository { +public class TimeEntryRepository_InMemoryImpl implements TimeEntryRepository { private Map db = new HashMap<>(); @Override diff --git a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository_JdbcImpl.java similarity index 90% rename from src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java rename to src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository_JdbcImpl.java index 67b4370d0..2c63b3531 100644 --- a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/timeEntry/TimeEntryRepository_JdbcImpl.java @@ -1,5 +1,7 @@ -package io.pivotal.pal.tracker; +package io.pivotal.pal.tracker.timeEntry; +import io.pivotal.pal.tracker.timeEntry.TimeEntry; +import io.pivotal.pal.tracker.timeEntry.TimeEntryRepository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; @@ -13,11 +15,11 @@ import static java.sql.Statement.RETURN_GENERATED_KEYS; -public class JdbcTimeEntryRepository implements TimeEntryRepository { +public class TimeEntryRepository_JdbcImpl implements TimeEntryRepository { private final JdbcTemplate jdbcTemplate; - public JdbcTimeEntryRepository(DataSource dataSource) { + public TimeEntryRepository_JdbcImpl(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } diff --git a/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java index c88bb8266..16fece5cf 100644 --- a/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java +++ b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java @@ -1,7 +1,7 @@ package test.pivotal.pal.tracker; -import io.pivotal.pal.tracker.InMemoryTimeEntryRepository; -import io.pivotal.pal.tracker.TimeEntry; +import io.pivotal.pal.tracker.timeEntry.TimeEntryRepository_InMemoryImpl; +import io.pivotal.pal.tracker.timeEntry.TimeEntry; import org.junit.Test; import java.time.LocalDate; @@ -13,7 +13,7 @@ public class InMemoryTimeEntryRepositoryTest { @Test public void create() throws Exception { - InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntryRepository_InMemoryImpl repo = new TimeEntryRepository_InMemoryImpl(); TimeEntry createdTimeEntry = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); @@ -25,7 +25,7 @@ public void create() throws Exception { @Test public void find() throws Exception { - InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntryRepository_InMemoryImpl repo = new TimeEntryRepository_InMemoryImpl(); repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); @@ -35,7 +35,7 @@ public void find() throws Exception { @Test public void list() throws Exception { - InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntryRepository_InMemoryImpl repo = new TimeEntryRepository_InMemoryImpl(); repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); repo.create(new TimeEntry(789, 654, LocalDate.parse("2017-01-07"), 4)); @@ -48,7 +48,7 @@ public void list() throws Exception { @Test public void update() throws Exception { - InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntryRepository_InMemoryImpl repo = new TimeEntryRepository_InMemoryImpl(); TimeEntry created = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); TimeEntry updatedEntry = repo.update( @@ -62,7 +62,7 @@ public void update() throws Exception { @Test public void delete() throws Exception { - InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntryRepository_InMemoryImpl repo = new TimeEntryRepository_InMemoryImpl(); TimeEntry created = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); repo.delete(created.getId()); diff --git a/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java index e1eac20fc..76b02ccbb 100644 --- a/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java +++ b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java @@ -2,9 +2,9 @@ import com.mysql.cj.jdbc.MysqlDataSource; -import io.pivotal.pal.tracker.JdbcTimeEntryRepository; -import io.pivotal.pal.tracker.TimeEntry; -import io.pivotal.pal.tracker.TimeEntryRepository; +import io.pivotal.pal.tracker.timeEntry.TimeEntryRepository_JdbcImpl; +import io.pivotal.pal.tracker.timeEntry.TimeEntry; +import io.pivotal.pal.tracker.timeEntry.TimeEntryRepository; import org.junit.Before; import org.junit.Test; import org.springframework.jdbc.core.JdbcTemplate; @@ -26,7 +26,7 @@ public void setUp() throws Exception { MysqlDataSource dataSource = new MysqlDataSource(); dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); - subject = new JdbcTimeEntryRepository(dataSource); + subject = new TimeEntryRepository_JdbcImpl(dataSource); jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.execute("DELETE FROM time_entries"); diff --git a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java index 26c04a916..3ad4e566d 100644 --- a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java +++ b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java @@ -1,8 +1,8 @@ package test.pivotal.pal.tracker; -import io.pivotal.pal.tracker.TimeEntry; -import io.pivotal.pal.tracker.TimeEntryController; -import io.pivotal.pal.tracker.TimeEntryRepository; +import io.pivotal.pal.tracker.timeEntry.TimeEntry; +import io.pivotal.pal.tracker.timeEntry.TimeEntryController; +import io.pivotal.pal.tracker.timeEntry.TimeEntryRepository; import org.junit.Before; import org.junit.Test; import org.springframework.boot.actuate.metrics.CounterService; diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index 2b7464304..597096f68 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -3,7 +3,7 @@ import com.jayway.jsonpath.DocumentContext; import com.mysql.cj.jdbc.MysqlDataSource; import io.pivotal.pal.tracker.PalTrackerApplication; -import io.pivotal.pal.tracker.TimeEntry; +import io.pivotal.pal.tracker.timeEntry.TimeEntry; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; From 6bd5e617b2bb4d6cdc09f2e7956f6bb02f4b4a39 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 12:50:47 -0600 Subject: [PATCH 25/28] Add tests for Security lab --- .../pal/trackerapi/SecurityApiTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java diff --git a/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java new file mode 100644 index 000000000..72099994b --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java @@ -0,0 +1,52 @@ +package test.pivotal.pal.trackerapi; + +import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class SecurityApiTest { + + @LocalServerPort + private String port; + private TestRestTemplate authorizedRestTemplate; + + @Autowired + private TestRestTemplate unAuthorizedRestTemplate; + + @Before + public void setUp() throws Exception { + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:" + port) + .basicAuthorization("user", "password"); + + authorizedRestTemplate = new TestRestTemplate(builder); + } + + @Test + public void unauthorizedTest() { + ResponseEntity response = this.unAuthorizedRestTemplate.getForEntity("/", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void authorizedTest() { + ResponseEntity response = this.authorizedRestTemplate.getForEntity("/", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } +} From 39fd5d8f92c1f7e0cc06b39c26c5e6f36519b049 Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Thu, 19 Oct 2017 10:54:32 -0700 Subject: [PATCH 26/28] Add Basic Auth --- build.gradle | 3 ++ manifest-production.yml | 2 ++ manifest-review.yml | 2 ++ .../pal/tracker/SecurityConfiguration.java | 32 +++++++++++++++++++ .../pivotal/pal/trackerapi/HealthApiTest.java | 14 +++++++- .../pal/trackerapi/SecurityApiTest.java | 6 ++-- .../pal/trackerapi/TimeEntryApiTest.java | 10 +++++- .../pal/trackerapi/WelcomeApiTest.java | 14 +++++++- 8 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java diff --git a/build.gradle b/build.gradle index a272fa57d..68db2236d 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-jdbc") compile("mysql:mysql-connector-java:6.0.6") compile("org.springframework.boot:spring-boot-starter-actuator") + compile("org.springframework.boot:spring-boot-starter-security") testCompile("org.springframework.boot:spring-boot-starter-test") } @@ -25,6 +26,7 @@ bootRun.environment([ "WELCOME_MESSAGE": "hello", "SPRING_DATASOURCE_URL": developmentDbUrl, "MANAGEMENT_SECURITY_ENABLED": false, + "SECURITY_FORCE_HTTPS": false, ]) def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" @@ -32,6 +34,7 @@ test.environment([ "WELCOME_MESSAGE": "Hello from test", "SPRING_DATASOURCE_URL": testDbUrl, "MANAGEMENT_SECURITY_ENABLED": false, + "SECURITY_FORCE_HTTPS": false, ]) flyway { diff --git a/manifest-production.yml b/manifest-production.yml index dc4af1488..47d0594a7 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -3,5 +3,7 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: swk-pal-tracker + env: + - "SECURITY_FORCE_HTTPS"="true" services: - tracker-database \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml index 582d7adc4..fe44e3bd7 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -3,5 +3,7 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: swk-pal-tracker-review + env: + - "SECURITY_FORCE_HTTPS"="true" services: - tracker-database \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java b/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java new file mode 100644 index 000000000..6f3225359 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java @@ -0,0 +1,32 @@ +package io.pivotal.pal.tracker; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@EnableWebSecurity +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + String forceHttps = System.getenv("SECURITY_FORCE_HTTPS"); + if (forceHttps != null && forceHttps.equals("true")) { + http.requiresChannel().anyRequest().requiresSecure(); + } + + http + .authorizeRequests().antMatchers("/**").hasRole("USER") + .and() + .httpBasic() + .and() + .csrf().disable(); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER"); + } +} \ No newline at end of file diff --git a/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java index b3eef23cc..e3123e905 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java @@ -2,11 +2,14 @@ import com.jayway.jsonpath.DocumentContext; import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; @@ -19,9 +22,18 @@ @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class HealthApiTest { - @Autowired + @LocalServerPort + private String port; private TestRestTemplate restTemplate; + @Before + public void setUp() throws Exception { + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:"+port) + .basicAuthorization("user", "password"); + restTemplate = new TestRestTemplate(builder); + } + @Test public void healthTest() { ResponseEntity response = this.restTemplate.getForEntity("/health", String.class); diff --git a/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java index 72099994b..baa3d196b 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java @@ -27,12 +27,12 @@ public class SecurityApiTest { @Autowired private TestRestTemplate unAuthorizedRestTemplate; + @Before public void setUp() throws Exception { RestTemplateBuilder builder = new RestTemplateBuilder() - .rootUri("http://localhost:" + port) - .basicAuthorization("user", "password"); - + .rootUri("http://localhost:"+port) + .basicAuthorization("user", "password"); authorizedRestTemplate = new TestRestTemplate(builder); } diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index 597096f68..741cf8b3f 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -8,8 +8,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -28,7 +30,8 @@ @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class TimeEntryApiTest { - @Autowired + @LocalServerPort + private String port; private TestRestTemplate restTemplate; private TimeEntry timeEntry = new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8); @@ -40,6 +43,11 @@ public void setUp() throws Exception { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.execute("TRUNCATE time_entries"); + + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:"+port) + .basicAuthorization("user", "password"); + restTemplate = new TestRestTemplate(builder); } @Test diff --git a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java index cc7091ed4..1c5b644a2 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java @@ -1,11 +1,14 @@ package test.pivotal.pal.trackerapi; import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -15,9 +18,18 @@ @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class WelcomeApiTest { - @Autowired + @LocalServerPort + private String port; private TestRestTemplate restTemplate; + @Before + public void setUp() throws Exception { + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:"+port) + .basicAuthorization("user", "password"); + restTemplate = new TestRestTemplate(builder); + } + @Test public void exampleTest() { String body = this.restTemplate.getForObject("/", String.class); From 96a975b76fd2a59c49364622fe18a0b49dc4daae Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Thu, 19 Oct 2017 11:05:03 -0700 Subject: [PATCH 27/28] fix env force https --- manifest-production.yml | 2 +- manifest-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-production.yml b/manifest-production.yml index 47d0594a7..6a2096d83 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -4,6 +4,6 @@ applications: path: build/libs/pal-tracker.jar host: swk-pal-tracker env: - - "SECURITY_FORCE_HTTPS"="true" + SECURITY_FORCE_HTTPS:true services: - tracker-database \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml index fe44e3bd7..b02dd720c 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -4,6 +4,6 @@ applications: path: build/libs/pal-tracker.jar host: swk-pal-tracker-review env: - - "SECURITY_FORCE_HTTPS"="true" + SECURITY_FORCE_HTTPS:true services: - tracker-database \ No newline at end of file From fc2f9bf3b177f94fb84d1b11eb68db47643c4f4c Mon Sep 17 00:00:00 2001 From: Shawn Karr Date: Thu, 19 Oct 2017 12:38:09 -0700 Subject: [PATCH 28/28] fix env in manifest files --- manifest-production.yml | 2 +- manifest-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-production.yml b/manifest-production.yml index 6a2096d83..d4904092d 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -4,6 +4,6 @@ applications: path: build/libs/pal-tracker.jar host: swk-pal-tracker env: - SECURITY_FORCE_HTTPS:true + SECURITY_FORCE_HTTPS: true services: - tracker-database \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml index b02dd720c..57c7adc0c 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -4,6 +4,6 @@ applications: path: build/libs/pal-tracker.jar host: swk-pal-tracker-review env: - SECURITY_FORCE_HTTPS:true + SECURITY_FORCE_HTTPS: true services: - tracker-database \ No newline at end of file