From a10a7cf95f361afb3e52e91dd53a6c54cdbd2d83 Mon Sep 17 00:00:00 2001 From: Ilya Date: Sun, 9 Apr 2017 12:52:25 +0300 Subject: [PATCH 01/24] Init --- build.gradle | 14 ++++++++++++++ settings.gradle | 2 ++ src/main/java/Server.java | 2 ++ 3 files changed, 18 insertions(+) create mode 100644 build.gradle create mode 100644 settings.gradle create mode 100644 src/main/java/Server.java diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d5d5486 --- /dev/null +++ b/build.gradle @@ -0,0 +1,14 @@ +group 'ru.spbau.shevchenko' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.5 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..8af3acb --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'FTP' + diff --git a/src/main/java/Server.java b/src/main/java/Server.java new file mode 100644 index 0000000..90fba1c --- /dev/null +++ b/src/main/java/Server.java @@ -0,0 +1,2 @@ +public class Server { +} From b2eb1705814b55e0ba9e612300c55354f7b449fa Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 04:50:54 +0300 Subject: [PATCH 02/24] Stubbed version --- .gitignore | 2 + build.gradle | 3 +- build/classes/main/console/Main.class | Bin 0 -> 3626 bytes build/classes/main/index | 0 build/classes/main/staged | 0 build/classes/main/vcs/Blob.class | Bin 0 -> 660 bytes build/classes/main/vcs/BlobSHARef.class | Bin 0 -> 1358 bytes build/classes/main/vcs/Branch.class | Bin 0 -> 1759 bytes .../vcs/BranchAlreadyExistsException.class | Bin 0 -> 375 bytes .../main/vcs/BranchNotFoundException.class | Bin 0 -> 592 bytes build/classes/main/vcs/Branches.class | Bin 0 -> 2348 bytes .../vcs/CheckoutStagedNotEmptyException.class | Bin 0 -> 326 bytes build/classes/main/vcs/Commit.class | Bin 0 -> 3636 bytes build/classes/main/vcs/CommitRef.class | Bin 0 -> 787 bytes build/classes/main/vcs/CommitSHARef.class | Bin 0 -> 1554 bytes build/classes/main/vcs/ContentfulBlob.class | Bin 0 -> 2571 bytes build/classes/main/vcs/ContentlessBlob.class | Bin 0 -> 2490 bytes .../vcs/DeleteActiveBranchException.class | Bin 0 -> 372 bytes .../vcs/EmptyCommitMessageException.class | Bin 0 -> 314 bytes build/classes/main/vcs/GitObject.class | Bin 0 -> 510 bytes .../MergeWhenStagedNotEmptyException.class | Bin 0 -> 329 bytes .../main/vcs/NothingToCommitException.class | Bin 0 -> 305 bytes build/classes/main/vcs/RepoState.class | Bin 0 -> 4209 bytes build/classes/main/vcs/SHARef.class | Bin 0 -> 253 bytes build/classes/main/vcs/VCS.class | Bin 0 -> 7976 bytes build/classes/main/vcs/VCSException.class | Bin 0 -> 407 bytes build/classes/main/vcs/VCSFiles.class | Bin 0 -> 3165 bytes .../main/vcs/VCSFilesCorruptedException.class | Bin 0 -> 311 bytes .../main/vcs/WrongArgumentsException.class | Bin 0 -> 360 bytes build/classes/test/vcs/TestFile.class | Bin 0 -> 2097 bytes build/classes/test/vcs/VCSTest.class | Bin 0 -> 4724 bytes gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53556 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++++++++++++++++++ gradlew.bat | 90 ++++++++++ src/main/java/BigMessage.java | 24 +++ src/main/java/CLI.java | 26 +++ src/main/java/Client.java | 83 +++++++++ src/main/java/QueryProcessor.java | 17 ++ src/main/java/ReadableMessage.java | 39 +++++ src/main/java/Server.java | 47 +++++ src/main/java/SimpleFile.java | 10 ++ src/main/java/SmallMessage.java | 27 +++ src/main/java/WritableMessage.java | 29 ++++ 44 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 build/classes/main/console/Main.class create mode 100644 build/classes/main/index create mode 100644 build/classes/main/staged create mode 100644 build/classes/main/vcs/Blob.class create mode 100644 build/classes/main/vcs/BlobSHARef.class create mode 100644 build/classes/main/vcs/Branch.class create mode 100644 build/classes/main/vcs/BranchAlreadyExistsException.class create mode 100644 build/classes/main/vcs/BranchNotFoundException.class create mode 100644 build/classes/main/vcs/Branches.class create mode 100644 build/classes/main/vcs/CheckoutStagedNotEmptyException.class create mode 100644 build/classes/main/vcs/Commit.class create mode 100644 build/classes/main/vcs/CommitRef.class create mode 100644 build/classes/main/vcs/CommitSHARef.class create mode 100644 build/classes/main/vcs/ContentfulBlob.class create mode 100644 build/classes/main/vcs/ContentlessBlob.class create mode 100644 build/classes/main/vcs/DeleteActiveBranchException.class create mode 100644 build/classes/main/vcs/EmptyCommitMessageException.class create mode 100644 build/classes/main/vcs/GitObject.class create mode 100644 build/classes/main/vcs/MergeWhenStagedNotEmptyException.class create mode 100644 build/classes/main/vcs/NothingToCommitException.class create mode 100644 build/classes/main/vcs/RepoState.class create mode 100644 build/classes/main/vcs/SHARef.class create mode 100644 build/classes/main/vcs/VCS.class create mode 100644 build/classes/main/vcs/VCSException.class create mode 100644 build/classes/main/vcs/VCSFiles.class create mode 100644 build/classes/main/vcs/VCSFilesCorruptedException.class create mode 100644 build/classes/main/vcs/WrongArgumentsException.class create mode 100644 build/classes/test/vcs/TestFile.class create mode 100644 build/classes/test/vcs/VCSTest.class 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 src/main/java/BigMessage.java create mode 100644 src/main/java/CLI.java create mode 100644 src/main/java/Client.java create mode 100644 src/main/java/QueryProcessor.java create mode 100644 src/main/java/ReadableMessage.java create mode 100644 src/main/java/SimpleFile.java create mode 100644 src/main/java/SmallMessage.java create mode 100644 src/main/java/WritableMessage.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f4eb5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.gradle +.idea \ No newline at end of file diff --git a/build.gradle b/build.gradle index d5d5486..1768b39 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ version '1.0-SNAPSHOT' apply plugin: 'java' -sourceCompatibility = 1.5 +sourceCompatibility = 1.8 repositories { mavenCentral() @@ -11,4 +11,5 @@ repositories { dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' + compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0' } diff --git a/build/classes/main/console/Main.class b/build/classes/main/console/Main.class new file mode 100644 index 0000000000000000000000000000000000000000..90f3af8828f0534bd176b3c51ff18aab4880b021 GIT binary patch literal 3626 zcma)8Yj6|S75=U)YuEB(JHps8U@&07wroTk2#F1b*kG_LVGZ3|0UA*~!} z)25F!P16@`(-xXO!lMyQO2KJshev5Ulm2O&>CevePiH!r{%B|VJLx2(-`$lZIY6eG z***81d+s^kIrp5S{o|b*w*hRzZ~b@(qZK%SgMJ*sVFgG0@M2WKC;jl@DL)!}Lt9VAj`b>ZjzbJ*z$?Y#Gc;1gTjQa6qydYO!QSqXrFDUq`g0Bhq zw#Je%yGNj`rFB%m+h;{hf$DH9Y3|J?#?17HJ{G4W5Vj0GKB}iHU( zNkGXblHnDzT`;KOyZD}l@8hhzlxS4LIsAY{VOWVo%;sV&ttX910o9l^jVUWjS$V=t zPtdg|ozQSh`fx2TyaWwTVpxMN_dOjn{IXot_#s}>(1#x}6&ikwpD-U9Ucq?{ z7jTg$im2VBYPcjV)%!c{_45lf6;JfV^-N~3W%pazWVFPmA$>;iQw^`;Jn=_lMA4>9 z)-cRWW;`2@pB8B7x8iZ@Y1VAhICGt8ny;8H4LMvUv|h`yGj>`}MNE6rie{>P_*o5J zt1d@bjr8z3ey-sPexc#21Gxyy4W&05i$~40f@>OP5OeAc+Jiw78_@utUTf|^#rT6aFr!u7Lik)akli5 z(z^&KC}w0allJGONegc?+z%5q7s8W*VYq z+_cT!d=Y8}Q5M<2yqiK67=Rip1Yfsce?BSHQ3zzOd5T#i4!ZFs9cCfyJPg*m6TCq4 zy+Z%U43NfSnO%vL%>q)H6Q=^1)_Ew-By@z`e*6l*zEA9&kv^Tpm?FkadB5zhl1u5% zr`U;oJexG+L=5$D4rke)634ie-lfalZOR+CkE$HVoR zGs&siasO15jQV_ecPp6HFo&B0tBclS$!TlK42AW?SX5{K=;OM1lTmhTvwN`n?qV#FEI2sA@m{L_|8Kf3KmKfK zSL7}C>GAk5OO{ExEwJ{k{M;)ZffXOGhgJNxc!*yDKpBMmU*LNaxe%!!EpMBHnBl^M z&3yYDVI>~nT5}b!1&@-ez+>p5gonN7XiVXerETHh?QLZ}jms~fF4)-F<$VL52JbA& zo+}f%5C44=-a~UJ59d%F;ZFN36z=BaX_&uDe3UC!*Ipn!esz267Sbny zs&3M=5U0+Q?u59wlQiiIU6OcSJo0j+pY)mbK*cQl=Ww{JCx`v*fl7x9d&%u?4`>e8 zQ%>%&_P`>C+vp<~Y!6g9+-ikfV|$?5;c8WK)$M^Ahx66=dKzD&7YLa&`SbA!^4ZR3 zn9uWk{>10+)L9d3^LEbSft>Jkc^kZMVsQ@dy0Y3F{!@^>L$$yX$3uOvt(-^da`;PO z`w#As2XnYtkj(^V(IC0&SSrv}-cWuW%LM+?#T+3yHyxK#vSa;wC)&tkk;^AMPKX1mh z(OMMDpb{&o*an}d5%ppz0-{+2#Rl?yVo*d-pQizFKpYi1x zTjGv~t?yAi)t&(J?{zBmpR7k<5gtGl8c0_nK!0`gxS01-OPwXG;(AfTeAT0o2$qTE zOw=l@5Y1REf>=v5Ys5x0i_KUk9!0C@Lr@H21JSjMA#{ilt`DG79K~iLd{`LRBqp&% zOrcwx#8zJmsmzaYlL6Q;TDs1{h8!UO)}wRMCyCjD9`4ZyS2p zi$P|2J9fas$jZ5T9A3sFS39^GDy)pc>I^OQ(u$AxJNd1(lX{m6ySECfw>dO+$z9^g z+asdIMg5?H@cXD#5cvQR z1$)IJ=T{jVP%u=%SGZzCzMRzus8X;`!4vPHrMDn`AB~j$7t5r`Cl%jgarouOraPvv z-D_ZdlR?V5e@h(d8zv>bv}pB;|u=-w$y4c literal 0 HcmV?d00001 diff --git a/build/classes/main/index b/build/classes/main/index new file mode 100644 index 0000000..e69de29 diff --git a/build/classes/main/staged b/build/classes/main/staged new file mode 100644 index 0000000..e69de29 diff --git a/build/classes/main/vcs/Blob.class b/build/classes/main/vcs/Blob.class new file mode 100644 index 0000000000000000000000000000000000000000..b16682c25f541b4e0244e05db574a3b9348a8c38 GIT binary patch literal 660 zcmZWnO-~y!5Pg#`Z$f|&LeiA-(NapdbT8ae^^g=Qq$sHf;=%##E>19Vy;ashsQ;?! zrK$u!fFFfAn{6NwKG@?o&(HHbet7(Q58wa|2d@Rz3wVQ#JhIpn*b>+lcq{OZ;Iulu z*2S0O6GEkP?cI2;QK36fTDspnd(|X}p|o+xgvNeC(ViK)eLGTS$b=Opv_oU1v4gR0 z>Ch)snR*jM?mW)q!r^Toe^?coNMJ|cJ)!)3Udt#-1ZwjF(*!Fxp8~CnvY!d*#{L;0 z(+)2sVY#D>?2fkdXB`jtHfR;TbmqyG4A!UJ-c| z_^Q}KQqK&v=U>af5*Fj^S7oRB%0!J{uP;Z+)9PnZ#p)D3?mc-YY(HP!)?O5KLwgjC z&E*UwIXKr2t~t%GMQAo&cH;ac^uJ$o8qFN|8|09JfCEB3_R3!+lOZgi$N^i!5@(hf xjd>-kFjmGY?;TXI#?n`*z>H_uO+&_x$?v{Re;%ENW;$JPbb)a+Oq((lCUZavBaJ zjax0ajXN6dVnoF~757y<5KwbPJC|LW7Kmkbj6EY=G)mj)tW&m1+oJ-(F{@-b69QfF z|Bn+J0{&Th(-eqitdhA}eVH@MPmNrWo><1t8^sNyY{`Ai>30fNMW8LSm#?H}yvpn5 zmMmnxsv1R}H5FRR?U;FIH1SM8*v%AcM`Yj>3`d|f>lpbLE5@!{irou@3Pz>i@$ho-c`UFwnRUXH z0-o}y_oK-WUL@rd=y1<1JH5Pi(vm>`Y1VAfs8m*MXTh$PHXA~y6E8JHo!kf_H8t%z zDO5xKx;mkHV@#jPtj#>1TVAJTo?<0Z*`h${Cx%mCe#~Xk(R=lv00a-4%1X;wDA&aj|lzzZCW6pb13q*_)7&iqG+S1ovQ`<6?HOx1Rk(rap)H?+H2dQI3 zl85l6INl=QJ4hYDzmog}Nu?%_7SQ?{DzQ%uLvJOd(R9{T@SVI4Ad)(2{LK` ggM_(?Yn&z7G_8aUNX&nM1{;6^HF^Tq-Euen0t_?^+yDRo literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/Branch.class b/build/classes/main/vcs/Branch.class new file mode 100644 index 0000000000000000000000000000000000000000..d88cab5df7e39f1030bb9fcccc8ca71cc205afe9 GIT binary patch literal 1759 zcmZux-&Y$&7`?Me*km(=5(@PfiuDJ{hBnexTTN|+07B(QB^Yh>9M2LKvTexLWI>M) z{x|x>gL*90KJ@tHgMXCAJ3C9V0Zz`&WWJg2-aGgEcK`kF`3nFy@QsBL+{hq>_to%$ zTHZ8q%fg5FNDUPWxACzW?pU~sPb{9A%U~YWEEce6VF{m_ST?a@;+}xjbe)~5zrXJV z0{PX$Mz^e2wdJ`@cef&tY&-j|+I!?2I^~wr-YeIFj@RDf-q3BY?FDxPrVD4c&w>@} zgsb{Hgd17)+V1+n{r6mf*6;kfGLC*<+i-qFZ z`v_!urv!2XM<~-;{Gs7K2|T~uHSw8=Ri1HnBDd#u1GghES%|?_;vCxjp+;02ZGl!q z?Ckd)FL1Z~1+V*9fRbsgRr3!z4R^^?c4oC(S5?&{CNL{;5#thhj7h9voyS$N^(hk@ z5|emc;w`*w;=aT?cvoT?1rwi3Y^ue3m^D$8*uobk>Jne#D-#bS9^z|(3;pKMUen=z zFYzjLFg)V<<(kV9x4a*9*~d=8D;qt#1g83U)t1xkuKU4~f6(5E9Z4(e)tcJc^ggG=6!bQclo{(sLsrX zG6tdoX225bxLMEsCTM93CL6T7!{8b^ioVfu_vI2Hz-PxO1w!?1}+mLg@l>o7eaMJ4E;Sr*Sv93%bfsr<|5=JVwu%Q^X()A`fq|#9^-y$iVgg0gQrI`v3p{ literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/BranchAlreadyExistsException.class b/build/classes/main/vcs/BranchAlreadyExistsException.class new file mode 100644 index 0000000000000000000000000000000000000000..cdfc546677609f80d5c39b2c1ef5f5b1beb67f47 GIT binary patch literal 375 zcmah_O-sW-5PcJ4Qe(9>py%qz4=@*Rl|n7`6ud-o-)@(7C2nN1E$z?pBzW)#_@j!m zDqaL#n4Ni#$9u!PZ(iR3Tp>-cg-(oai~~Y+t&MgA!r^KDAeJJlgqdZ9v)au1XC)yV z)l)_2dEQa^a;em^miyBDe8rl>Q?)hRN)V%Qwt=l|HNMo zRo9nc3Q24eGE=nspiIZrT+&Tp2QJU#`hgti1LMj^H88s=>|)P?76|!&1|Xzuv{`L@ z&qRoptXgr=?@R3yPJWYfM&YkUSrNwsQ_%^lrNf!T3b*F)zu85+xkt7szTF~<2;1V( z3+QfwNH#h~?3j0Uj*l_o6C|EEA%ewEpiOpAV6r8?<)&aPBh4PuAcHJZ6IR%ZVD$qs CV~~IV literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/Branches.class b/build/classes/main/vcs/Branches.class new file mode 100644 index 0000000000000000000000000000000000000000..fdd1a499a705fc568d4cd78733c4d8b83e344654 GIT binary patch literal 2348 zcmah~TUQ%Z7~MmF36r73;Zk}}TWKK$ViA|NX|WK%qG_n4rKVNukQ`uOGJ`V{TKyY7 z>8r0kRDB@Is`%zw{wSCG%mfljT&`I;bIzIXvcLW9bMp5;zyASX8lHw>%xE}`4>jDw ztenlM$Z6=pyxdt(v8W-AkL2RMiX{z+=*!1)=Mxoq6%RBdG1EieLITTp7{{k7Rx}jx zNX26ns|xhFmDxh>{zCEj{L+eof&7-aYi1nF&8%Ct$UHQI&8&j%JC{`CXhRiyS8<-(11y}Q~w~^Tr0a>;jKVvpOW*D$gvu$#u5?ECJXj^b?bM3e zlBTiTex~Dde8JkA+b7u3@eZEq_!7^VtEsB?R4TYAL#^hDvIalrdR}cO5KXRioWWTY zU+FM$TR}V$t)Q-x&DfEuRjlclLYew$!h6GVd2LISsfGT~ndfk6`Ka!O-ys$SryD&v zeqQC-`HwG@+VwVlJgI$o%o8s>D-ef(CfeqTq)3=W!9#Cb-^zoZmEM}{+@bTKc-{co zBIL-DiZjB(j~P;ip733Jml}$AIrf=UXWJC@tR?#4trBe=^JdCR%|ZqSNby*uJQ379C*|!MiOy!S9i#r!M%kZu6-Id;h z@-rtLxWrM9HtBwutBd?~hdVt4%u~p@be`f{I>*y>bkq_1iSt(fINrtx_nQ7X{jcFV zt)%~V=;Q;W>EHQ1^nG-dh@<;J#(OYchq{k=DScxPJ%#a!I<$#bw2p-c(~)N*17lzm zV?;B^C(S}Th*HH3jMHA`zZe$Y$1%yrpjAH!8KBi5J%=z6MplxT_6G?zA;Ib)*gAS8c@eaTyy2GOjS%b^CO^yM=h>4B?Gh%Ka0>4-jAZ9W zt~&T$Y@hJdpXBB*T;7K$85_ptYYdkR%h)QGjBUd%*0HmPuMb}3_mM2^;jIILPcr>o z^fh1(G5QR1Oyj+f&58UwM{7_4LH UR~V9V-oP9q7mcesVc@&}0uW9YUjP6A literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/CheckoutStagedNotEmptyException.class b/build/classes/main/vcs/CheckoutStagedNotEmptyException.class new file mode 100644 index 0000000000000000000000000000000000000000..446de4415661e13077b15f7af277263a29a3a93f GIT binary patch literal 326 zcma)1!A`>d;#LN_-mFhxGs^wIbuVT7j zrIQ*d7P%<3+mE7e=Xy)PEW5BG(=#={N59pgo4I$6qAbMyp PkN*w0Lc|-Pd2q#hg|AMa literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/Commit.class b/build/classes/main/vcs/Commit.class new file mode 100644 index 0000000000000000000000000000000000000000..73ffdee22469951f223ad7d263efff747e10e6c4 GIT binary patch literal 3636 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 GVf;Ugu~=mQ literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/ContentlessBlob.class b/build/classes/main/vcs/ContentlessBlob.class new file mode 100644 index 0000000000000000000000000000000000000000..81178516d7ab5e8e61f3d0bb4536c0f15378cccf GIT binary patch literal 2490 zcmZ`*S$ES$7`?KSNEjs~VhL>uC_w7iPU8YyLP;rENJ*QmF->cB*^zApSqhSz_No7- zed3`TftJ(4Q#q$Us;74>Dc<1ZWF&nv^L_W;@6IIu{`cqK06st?g(H|bgcL4|;S(`j zNnsYB9>!ICmclj6$(R?b7Gx}nnd>Pm;dAlEA%z<Nj+Y7(@S9b?bP+lEuk z7Xr_6s#he8Z8n_1s+-Hsp5CEG1+niUDdD|=->KhMv?jX6d*IWTKER&Z6q#AI*%?&X;+qFr#eO$kR0AI|}v%HH$!xH#04`PGCBLZTh~z!0Jg)z&hVea>MY9x*3>W z2irqcGYE0B*kq;~9EozlMpMgUoPLd(BU})Hs5ESWLBfeBQ)ismQrEvPo4ZY=Wqc`P zg<_eH8ipr!&PH9l1Hf-ek~Lr#JO=%Zp3t13pIsW^@k zD%P+rV?)JPI45IMMFBTue63;&-w=;*Pp3{QzC}^O(YBcwk%|nmGH$8J<9!u6#%aJ- zQ$lGZD6u7(r8#T4`Q`Nk?Iq}~Jqxzs`!x2F+iZg|v4u34sI2OY(Bxb`09n{4lRbUY)Eb?jjkRaDtpCW)WnYS(+fB7}K-EKRZ5#4qLB6U?%P!(4u@5YYv7OTG{+`E&)9XwQUTEdwg@;q&U z*~Z+FkUb#F3#R67cQf?Npy4?Y%UuO$Q*cH8^E9xy%qcq({Na(|`%w}e;PaRuE>2Ka zOksQ!Cpq^P-+qG(A<_NZE2N*}PEot3-Twy7kg~ZaNMxs;Ao&YRBr(QO56j^loZ=S?N3g})X@X7QZALZpVFQD>NqR-nyT?`2 zjQ1finZ_Bu4JIOvAyb}7j58uQ<@uEO?9cv=S(!5X(|Aj~&5)0F(d(w|JfwNRarg7T4!@n|^;BJB0 ziOSCWiar5*AN_w|Ac1F)UqX2!j^Ir=W_oap$2h)|S?gWa93bU1Cc7~4hNJ)j(+`{% z-pFKgKcT+qbsA$VR)Y;)PW3^DI!3VQrqH@?};UHn~ z&|eDua{(8bMD%;8N#_FTUu4%WF?xyrHicvbI??w;w%aj@9w}#r=b+=mXlKiA`xxrp P+~2cV=C+TTa6kSZkc=Fg literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/DeleteActiveBranchException.class b/build/classes/main/vcs/DeleteActiveBranchException.class new file mode 100644 index 0000000000000000000000000000000000000000..816e8f4c00a970ea3729aed02d21674f1225d14c GIT binary patch literal 372 zcmah_!AiqG5PcJ4(#C3Qz+Sbts$eeODuuS{DR_zGzTGbEO5DhH8~iO#f(Jjqj}m89 zya>85JM$io_l9|Ue|ZIPjx@m*x-oh&_6gC2HrkB|2gCeMG$N~nS!RW^+AK%MB_W*E z3q|PW+Nhh&{ao2wF|U|O^ID3k6jpouO@?lz*Mvdd$aQw9D&^EfI^C$L6-KVE9;JG4 zx;7&Mu`d0cNl1!%W2L&*KIP~SlQX|?2O+izhyMf;dR|e^ik}@YK)|Q)kGR-|d&JY2 q`G9+La{3H<;weDPfAYz;Sl332asNxjC}G5U?4ZLQgk44f_C5g0jvpOVmtRUzMZc`C~fk#1Od zighM7i6~Xge?{A@R7D8meO8TT`PO_zI?t7PlT{^_a(2k%)+k*}2>wFv%1q8x9rkj^ zJ6wys2w=k@JpYqR=+=%TTKt_t2#f!#i?}(4(PcKealjcILEkJ`@EBc|!3z(J!EJWX MgwKr7Vzdxk0awFG;{X5v literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/GitObject.class b/build/classes/main/vcs/GitObject.class new file mode 100644 index 0000000000000000000000000000000000000000..992612898042dcaab2c373b76547b18636da1066 GIT binary patch literal 510 zcmZuuT}uK%6g_uaT{Tm)wCuY#4QdH`i6YRT6r>f*M40-5{l*9 zT4kEDdFtOuLbjB~+}cJ*F3KVflVqc8M>T~kiO)6$Va8td3dV-s8KDV+F^Macd8j)ZcV1!Saxa*9o{(~kp%j=n&zO!{k;ejO?10fg;TuLWaPR;C literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/MergeWhenStagedNotEmptyException.class b/build/classes/main/vcs/MergeWhenStagedNotEmptyException.class new file mode 100644 index 0000000000000000000000000000000000000000..1664648bbe9f4de5bdc4bf6599917dc77cc73e1a GIT binary patch literal 329 zcma)1%TB^j5IsXni?uxB7q|eUE@b0Q!-n9hvZ*QSOPRnm&`ZjN;AgoaVc`e(QO3DI z+{z>~=gi5Onauak{tLh(S`j?dLo`A(3Bj{2wCxeR+q;b5_sxU}%|sVGTrb94zNztC zl&i$#YM!Z5yZ$8lcBWT^n`D!(y02VLc{Jl9wQ9qij^Y{>dDjI=$}Yh`SXa;~)%zXjQ%D7&)Zid` Rs0ojc23#PLj&ON!#lNYyPlo^i literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/NothingToCommitException.class b/build/classes/main/vcs/NothingToCommitException.class new file mode 100644 index 0000000000000000000000000000000000000000..13a53472d62208f6f7e1b78463bab139bcb0cd7d GIT binary patch literal 305 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 literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/VCS.class b/build/classes/main/vcs/VCS.class new file mode 100644 index 0000000000000000000000000000000000000000..bd3832ee11b106306939733389f6f0cfef165ec3 GIT binary patch literal 7976 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 BNtyrv literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/VCSFiles.class b/build/classes/main/vcs/VCSFiles.class new file mode 100644 index 0000000000000000000000000000000000000000..07b729a1f1c63e8c67d0751c0922a30001b7c841 GIT binary patch literal 3165 zcma)8SyL2O6#jY!rlDzeWO0dTB7tEU#62iRPz;g9fWaV1%rLarI!u$99$d2T>qB1S zYhLq^rAkPs_?UD%4Y;E;+{-oDGZ-}%lt-#x=$|NQ<0z;S$Q;4rRM;}Bk{ z!7Szs&aUZr)xbQi%kY|x1^M2P<3*XhuHy|^ylKG1O&xC;s7G3cB^ffZozbyuphb=? z1Fg6v!-@<%Zjs~L0%gl^rwVs7^`pnkJo0Iy;}v+p_jI zE8N?5K||f-dZv(~r*Q%o1a#TTS}t$3b(iz%Ia;HC4aTMW8Eeh8om@f3+w?DNG}O+y z>CD}!^qTJ|k;z+W0Zdr;?Sf06yCC1tP;)PDyVlIo9VG1SuhF3t-yAFXsfvGur`Re9I);#%eqG) zWYf8ol(M9kDmxc==mQ=VC~aw=XEpW_P?U*ZIx(g0tjh6WGO(+qWD=T>xlW#VgmLxD!0 z?KVs0T|;v~XJRINs2Vy;kYm|&p)l>Z<4!TR95HF~OhSfId8#6B6`brk>zXX99bezu zZc*WiM7S$CV_ZXJAr_T%L*tSZhI^zp*oBLBo{hoDKhV&qx=OHP3+(Lwg55{n*8 zPJzDKOL);fASNw=_vl^T^jfTj%jJ_by0Z!;f$o+ezavcCy5#rw9Yq%bu{v=kQp0dG zASLQy-(^JSn8lN-=}<|Kx0jtOtG3O$^g z$^c39a#e*s^wUZ%5A!RRYkxs3`5WRfY@(tBV(=KKA%GbHs>OiEBgL}{ULPdnQSwoc zSG_wx7a`++pmGz5@6ix0^hc;ui>ggjUoZKpL!5Z^jIx2zHDSm@kL@)WVad<2kb2Rl zJ`_^F!UzoJTseJ&#&GSqO7eh{8qa6kOHwN;iQU4C?xEjS?zJ@HMueuzX4d!p)Q!cVe*=jr?+lkgJh zO;RnQoune-lO!da#u-L^5^Z?F)6aR-;VjM(yKIt12&4{6g1WMC))Z#U70csB_#rX1T$1uvX`g( zHnBgVGsXK9*`KD$&fp*umt@k8@#paz^COAwR35`bkFX3A{y!2Kh_BRgd zxYE2$iTta=r+%DQY}8UP^oyDELv&o$3h*-(B>HO))`A)klaa|7x3_VN3@% V(fKQ4A|ox*Vyd~C=3Wdl{{l;H!x{hp literal 0 HcmV?d00001 diff --git a/build/classes/main/vcs/VCSFilesCorruptedException.class b/build/classes/main/vcs/VCSFilesCorruptedException.class new file mode 100644 index 0000000000000000000000000000000000000000..0fbf3b7e9ec32828a51867fea192e2206a0bbd57 GIT binary patch literal 311 zcmaKnv2MaZ42FM0NZK?s5f4Dyt?f{jZXlKjricZU{Rk)Zs*ppM1ogF;kQjIX9tz=v zh>cpZ{rlG^TRxo+M*!alT(r@3@a(`R*dvvxVoYeif5ZeU)GJBwBbCWnxn4@M5X+RE zUZfL|#=@v-ZcMBAQ90o?+9vrR4(C&q$~@G@l$%1XCVz?C6iR19f;-oxNn`~OUhbn` zzhWyqSm+S?|8FG(4RAYzH?;VI#8U_)D5k>@Yp^bJE!p>3lAeS<&rK!@~S#7HEaX|=Y z&7C3)GHuj#x0ow?Bj+_UN!FCIE~M2Sf0LoR*Dc{7Tb6BlYa3HdZPhK5aqZ<(sUDqf z%$Pu|Nq*-M;=Ji>sjjq7*#E=e%pclEh#q0@pCH1}Z;M&}lZ`_J{C!`1g}2}?7)_WD n_!TFoFQ8|h0z};7729K-z&hjIFBLX;<4a%@{gu}iqX65VmLyZi literal 0 HcmV?d00001 diff --git a/build/classes/test/vcs/TestFile.class b/build/classes/test/vcs/TestFile.class new file mode 100644 index 0000000000000000000000000000000000000000..369ff1b2438a185da439c8c372b214c0aa079ac5 GIT binary patch literal 2097 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..ca78035ef0501d802d4fc55381ef2d5c3ce0ec6e GIT binary patch literal 53556 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�H$uJM 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 \(.*\)$'` + 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 + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..832fdb6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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 +if "%@eval[2+2]" == "4" goto 4NT_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=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +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/src/main/java/BigMessage.java b/src/main/java/BigMessage.java new file mode 100644 index 0000000..0cd24a8 --- /dev/null +++ b/src/main/java/BigMessage.java @@ -0,0 +1,24 @@ +import com.sun.istack.internal.NotNull; + +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +public class BigMessage extends ReadableMessage { + private String filename; + + BigMessage(@NotNull SocketChannel channel) { + super(channel); + + } + + + @Override + protected void process(ByteBuffer buffer) { + throw new UnsupportedOperationException(); + } + + + public String getFilename() { + return filename; + } +} diff --git a/src/main/java/CLI.java b/src/main/java/CLI.java new file mode 100644 index 0000000..dcedf1b --- /dev/null +++ b/src/main/java/CLI.java @@ -0,0 +1,26 @@ +import java.io.IOException; + +public class CLI { + public static void main(String[] args) throws IOException { + Client client = new Client(); + client.connnect(args[1], Integer.valueOf(args[2])); + switch (args[0]) { + case "get": { + String filename = client.executeGet(args[3]); + System.out.println("Downloaded file to " + filename); + break; + } + case "list": { + for (SimpleFile file : client.executeList(args[3])) { + System.out.println(file.name + " " + file.isDirectory); + } + break; + } + default: { + System.out.println("Usage is: get PATH or list PATH"); + } + + } + client.disconnect(); + } +} diff --git a/src/main/java/Client.java b/src/main/java/Client.java new file mode 100644 index 0000000..adcddf2 --- /dev/null +++ b/src/main/java/Client.java @@ -0,0 +1,83 @@ +import org.apache.commons.lang3.ArrayUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.NotYetConnectedException; +import java.nio.channels.SocketChannel; +import java.util.List; + +public class Client { + private enum Command {LIST, GET} + + private final SocketChannel channel; + + public Client() throws IOException { + channel = SocketChannel.open(); + channel.configureBlocking(false); + } + + public void connnect(String hostname, int port) throws IOException { + if (channel.isConnected()) return; + channel.connect(new InetSocketAddress(hostname, port)); + + //noinspection StatementWithEmptyBody + while(channel.isConnectionPending()); + } + public void disconnect() throws IOException { + channel.close(); + } + + public List executeList(String path) throws IOException { + sendRequest(createSentData((byte) Command.LIST.ordinal(), path.getBytes())); + byte[] response = getSmallResponse(); + ObjectInputStream objectStream = new ObjectInputStream(new ByteArrayInputStream(response)); + try { + //noinspection unchecked + return (List) objectStream.readObject(); + } catch (ClassNotFoundException e) { + // TODO: probably change to custom exception + throw new RuntimeException("Server fucked up!"); + } + } + + + public String executeGet(String path) throws IOException { + sendRequest(createSentData((byte) Command.GET.ordinal(), path.getBytes())); + return getBigResponse().getFilename(); + } + private byte[] getSmallResponse() throws IOException { + SmallMessage message = new SmallMessage(channel); + getResponse(message); + return message.getData(); + } + + private BigMessage getBigResponse() throws IOException { + BigMessage message = new BigMessage(channel); + getResponse(message); + return message; + } + + private void getResponse(ReadableMessage message) throws IOException { + //noinspection StatementWithEmptyBody + while (!message.read()); + } + + private void sendRequest(byte[] data) { + //noinspection StatementWithEmptyBody + if (!channel.isConnected()) { + throw new NotYetConnectedException(); + } + WritableMessage message = new WritableMessage(channel, data); + //noinspection StatementWithEmptyBody + while (!message.write()); + } + + private byte[] createSentData(byte first, byte[] rest) { + long size = Long.BYTES + rest.length; + return ArrayUtils.addAll(ByteBuffer.allocate(4).putLong(size).put(first).array(), rest); + } + +} diff --git a/src/main/java/QueryProcessor.java b/src/main/java/QueryProcessor.java new file mode 100644 index 0000000..d4a710a --- /dev/null +++ b/src/main/java/QueryProcessor.java @@ -0,0 +1,17 @@ +import com.sun.istack.internal.NotNull; + +class QueryProcessor { + byte[] process(@NotNull byte[] data) { + throw new UnsupportedOperationException(); + /* + switch (queryType) { + case 0: { + return ("Hello, " + path).getBytes(); + } + default: { + throw new UnsupportedOperationException(); + } + } + */ + } +} diff --git a/src/main/java/ReadableMessage.java b/src/main/java/ReadableMessage.java new file mode 100644 index 0000000..ba0547a --- /dev/null +++ b/src/main/java/ReadableMessage.java @@ -0,0 +1,39 @@ +import com.sun.istack.internal.NotNull; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +abstract class ReadableMessage { + private static final int BUFFER_SIZE = 4096; + private SocketChannel channel; + private long size = 0; + private int read = 0; + private ByteBuffer buffer; + + ReadableMessage(@NotNull SocketChannel channel) { + this.channel = channel; + buffer = ByteBuffer.allocate(BUFFER_SIZE); + } + + public SocketChannel getChannel() { + return channel; + } + + boolean read() throws IOException { + if (read < 4) { + read += channel.read(buffer); + if (read >= 4) { + size = buffer.getLong(); + } + else { + return false; + } + } + channel.read(buffer); + process(buffer); + return read == size; + } + + protected abstract void process(ByteBuffer buffer); +} diff --git a/src/main/java/Server.java b/src/main/java/Server.java index 90fba1c..5d18367 100644 --- a/src/main/java/Server.java +++ b/src/main/java/Server.java @@ -1,2 +1,49 @@ +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; + public class Server { + void run(int port) throws IOException { + Selector selector = Selector.open(); + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.bind(new InetSocketAddress(port)); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + QueryProcessor queryProcessor = new QueryProcessor(); + while (true) { + selector.selectNow(); + Iterator keyIterator = selector.selectedKeys().iterator(); + while (keyIterator.hasNext()) { + SelectionKey selectionKey = keyIterator.next(); + if (selectionKey.isAcceptable()) { + ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel(); + SocketChannel socketChannel = serverChannel.accept(); + socketChannel.configureBlocking(false); + SelectionKey newSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ); + newSelectionKey.attach(new SmallMessage(socketChannel)); + } + if (selectionKey.isReadable()) { + SmallMessage message = (SmallMessage) selectionKey.attachment(); + if (message.read()) { + selectionKey.cancel(); + SelectionKey newSelectionKey = selectionKey.channel().register(selector, SelectionKey.OP_WRITE); + newSelectionKey.attach(new WritableMessage(message.getChannel(), queryProcessor.process(message.getData()))); + } + + } + if (selectionKey.isWritable()) { + WritableMessage message = (WritableMessage) selectionKey.attachment(); + if (message.write()) { + selectionKey.channel().close(); + selectionKey.cancel(); + } + } + keyIterator.remove(); + } + } + } } diff --git a/src/main/java/SimpleFile.java b/src/main/java/SimpleFile.java new file mode 100644 index 0000000..9f28ea6 --- /dev/null +++ b/src/main/java/SimpleFile.java @@ -0,0 +1,10 @@ +class SimpleFile { + public final String name; + public final boolean isDirectory; + + public SimpleFile(String name, boolean isDirectory) { + this.name = name; + this.isDirectory = isDirectory; + } + +} diff --git a/src/main/java/SmallMessage.java b/src/main/java/SmallMessage.java new file mode 100644 index 0000000..b3c262c --- /dev/null +++ b/src/main/java/SmallMessage.java @@ -0,0 +1,27 @@ +import com.sun.istack.internal.NotNull; +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; + +public class SmallMessage extends ReadableMessage { + private SocketChannel channel; + ArrayList data; + + SmallMessage(@NotNull SocketChannel channel) { + super(channel); + } + + + public byte[] getData() { + return ArrayUtils.toPrimitive(data.toArray(new Byte[0])); + } + + @Override + protected void process(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + data.add(buffer.get()); + } + } +} diff --git a/src/main/java/WritableMessage.java b/src/main/java/WritableMessage.java new file mode 100644 index 0000000..b27015a --- /dev/null +++ b/src/main/java/WritableMessage.java @@ -0,0 +1,29 @@ +import com.sun.istack.internal.NotNull; + +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +class WritableMessage { + private final ByteBuffer buffer; + private int position; + private SocketChannel socket; + private byte[] data; + /** + * @param socket socket to write data to + * @param data data to write to socket + */ + WritableMessage(@NotNull SocketChannel socket, @NotNull byte[] data) { + this.socket = socket; + this.buffer = ByteBuffer.allocate(data.length); + this.buffer.clear(); + } + + /** + * Calls write() on underlying channel with remaining data. + * @return whether it has completed message writing + */ + boolean write() { + + throw new UnsupportedOperationException(); + } +} From abc00408681a32b07195c34338ad2d1ca9e206d5 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 12:17:36 +0300 Subject: [PATCH 03/24] First version done --- .gitignore | 3 +- gradle/wrapper/gradle-wrapper.properties | 4 +- src/main/java/BigMessage.java | 24 ---------- src/main/java/QueryProcessor.java | 17 ------- src/main/java/SmallMessage.java | 27 ----------- src/main/java/WritableMessage.java | 29 ------------ src/main/java/{ => client}/Client.java | 17 ++++--- .../ClientConsoleApp.java} | 8 +++- src/main/java/server/QueryProcessor.java | 27 +++++++++++ src/main/java/{ => server}/Server.java | 14 ++++-- src/main/java/server/ServerConsoleApp.java | 45 +++++++++++++++++++ src/main/java/utils/BigReadableMessage.java | 23 ++++++++++ src/main/java/utils/BigWritableMessage.java | 16 +++++++ .../java/utils/KnownFileOutputStream.java | 18 ++++++++ .../java/{ => utils}/ReadableMessage.java | 21 ++++++--- src/main/java/{ => utils}/SimpleFile.java | 4 +- src/main/java/utils/SmallReadableMessage.java | 18 ++++++++ src/main/java/utils/SmallWritableMessage.java | 13 ++++++ src/main/java/utils/WritableMessage.java | 44 ++++++++++++++++++ 19 files changed, 252 insertions(+), 120 deletions(-) delete mode 100644 src/main/java/BigMessage.java delete mode 100644 src/main/java/QueryProcessor.java delete mode 100644 src/main/java/SmallMessage.java delete mode 100644 src/main/java/WritableMessage.java rename src/main/java/{ => client}/Client.java (81%) rename src/main/java/{CLI.java => client/ClientConsoleApp.java} (84%) create mode 100644 src/main/java/server/QueryProcessor.java rename src/main/java/{ => server}/Server.java (80%) create mode 100644 src/main/java/server/ServerConsoleApp.java create mode 100644 src/main/java/utils/BigReadableMessage.java create mode 100644 src/main/java/utils/BigWritableMessage.java create mode 100644 src/main/java/utils/KnownFileOutputStream.java rename src/main/java/{ => utils}/ReadableMessage.java (58%) rename src/main/java/{ => utils}/SimpleFile.java (82%) create mode 100644 src/main/java/utils/SmallReadableMessage.java create mode 100644 src/main/java/utils/SmallWritableMessage.java create mode 100644 src/main/java/utils/WritableMessage.java diff --git a/.gitignore b/.gitignore index 9f4eb5f..835e8dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .gradle -.idea \ No newline at end of file +.idea +build \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7b9d4e4..e842cab 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 27 02:50:55 MSK 2017 +#Thu Apr 27 12:04:40 MSK 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip diff --git a/src/main/java/BigMessage.java b/src/main/java/BigMessage.java deleted file mode 100644 index 0cd24a8..0000000 --- a/src/main/java/BigMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -import com.sun.istack.internal.NotNull; - -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; - -public class BigMessage extends ReadableMessage { - private String filename; - - BigMessage(@NotNull SocketChannel channel) { - super(channel); - - } - - - @Override - protected void process(ByteBuffer buffer) { - throw new UnsupportedOperationException(); - } - - - public String getFilename() { - return filename; - } -} diff --git a/src/main/java/QueryProcessor.java b/src/main/java/QueryProcessor.java deleted file mode 100644 index d4a710a..0000000 --- a/src/main/java/QueryProcessor.java +++ /dev/null @@ -1,17 +0,0 @@ -import com.sun.istack.internal.NotNull; - -class QueryProcessor { - byte[] process(@NotNull byte[] data) { - throw new UnsupportedOperationException(); - /* - switch (queryType) { - case 0: { - return ("Hello, " + path).getBytes(); - } - default: { - throw new UnsupportedOperationException(); - } - } - */ - } -} diff --git a/src/main/java/SmallMessage.java b/src/main/java/SmallMessage.java deleted file mode 100644 index b3c262c..0000000 --- a/src/main/java/SmallMessage.java +++ /dev/null @@ -1,27 +0,0 @@ -import com.sun.istack.internal.NotNull; -import org.apache.commons.lang3.ArrayUtils; - -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; -import java.util.ArrayList; - -public class SmallMessage extends ReadableMessage { - private SocketChannel channel; - ArrayList data; - - SmallMessage(@NotNull SocketChannel channel) { - super(channel); - } - - - public byte[] getData() { - return ArrayUtils.toPrimitive(data.toArray(new Byte[0])); - } - - @Override - protected void process(ByteBuffer buffer) { - while (buffer.hasRemaining()) { - data.add(buffer.get()); - } - } -} diff --git a/src/main/java/WritableMessage.java b/src/main/java/WritableMessage.java deleted file mode 100644 index b27015a..0000000 --- a/src/main/java/WritableMessage.java +++ /dev/null @@ -1,29 +0,0 @@ -import com.sun.istack.internal.NotNull; - -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; - -class WritableMessage { - private final ByteBuffer buffer; - private int position; - private SocketChannel socket; - private byte[] data; - /** - * @param socket socket to write data to - * @param data data to write to socket - */ - WritableMessage(@NotNull SocketChannel socket, @NotNull byte[] data) { - this.socket = socket; - this.buffer = ByteBuffer.allocate(data.length); - this.buffer.clear(); - } - - /** - * Calls write() on underlying channel with remaining data. - * @return whether it has completed message writing - */ - boolean write() { - - throw new UnsupportedOperationException(); - } -} diff --git a/src/main/java/Client.java b/src/main/java/client/Client.java similarity index 81% rename from src/main/java/Client.java rename to src/main/java/client/Client.java index adcddf2..1340ad9 100644 --- a/src/main/java/Client.java +++ b/src/main/java/client/Client.java @@ -1,4 +1,7 @@ +package client; + import org.apache.commons.lang3.ArrayUtils; +import utils.*; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -19,7 +22,7 @@ public Client() throws IOException { channel.configureBlocking(false); } - public void connnect(String hostname, int port) throws IOException { + public void connect(String hostname, int port) throws IOException { if (channel.isConnected()) return; channel.connect(new InetSocketAddress(hostname, port)); @@ -39,7 +42,7 @@ public List executeList(String path) throws IOException { return (List) objectStream.readObject(); } catch (ClassNotFoundException e) { // TODO: probably change to custom exception - throw new RuntimeException("Server fucked up!"); + throw new RuntimeException("server.Server fucked up!"); } } @@ -49,13 +52,13 @@ public String executeGet(String path) throws IOException { return getBigResponse().getFilename(); } private byte[] getSmallResponse() throws IOException { - SmallMessage message = new SmallMessage(channel); + SmallReadableMessage message = new SmallReadableMessage(channel); getResponse(message); return message.getData(); } - private BigMessage getBigResponse() throws IOException { - BigMessage message = new BigMessage(channel); + private BigReadableMessage getBigResponse() throws IOException { + BigReadableMessage message = new BigReadableMessage(channel); getResponse(message); return message; } @@ -65,12 +68,12 @@ private void getResponse(ReadableMessage message) throws IOException { while (!message.read()); } - private void sendRequest(byte[] data) { + private void sendRequest(byte[] data) throws IOException { //noinspection StatementWithEmptyBody if (!channel.isConnected()) { throw new NotYetConnectedException(); } - WritableMessage message = new WritableMessage(channel, data); + WritableMessage message = new SmallWritableMessage(channel, data); //noinspection StatementWithEmptyBody while (!message.write()); } diff --git a/src/main/java/CLI.java b/src/main/java/client/ClientConsoleApp.java similarity index 84% rename from src/main/java/CLI.java rename to src/main/java/client/ClientConsoleApp.java index dcedf1b..04506a8 100644 --- a/src/main/java/CLI.java +++ b/src/main/java/client/ClientConsoleApp.java @@ -1,9 +1,13 @@ +package client; + +import utils.SimpleFile; + import java.io.IOException; -public class CLI { +public class ClientConsoleApp { public static void main(String[] args) throws IOException { Client client = new Client(); - client.connnect(args[1], Integer.valueOf(args[2])); + client.connect(args[1], Integer.valueOf(args[2])); switch (args[0]) { case "get": { String filename = client.executeGet(args[3]); diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java new file mode 100644 index 0000000..095a311 --- /dev/null +++ b/src/main/java/server/QueryProcessor.java @@ -0,0 +1,27 @@ +package server; + +import com.sun.istack.internal.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; + +class QueryProcessor { + byte[] process(@NotNull byte[] data) throws IOException { + DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(data)); + int queryType = inputStream.readInt(); + String path = inputStream.readUTF(); + + switch (queryType) { + case 0: { + return ("Hello, " + path).getBytes(); + } + case 1: { + return ("Goodbye, " + path).getBytes(); + } + default: { + throw new UnsupportedOperationException(); + } + } + } +} diff --git a/src/main/java/Server.java b/src/main/java/server/Server.java similarity index 80% rename from src/main/java/Server.java rename to src/main/java/server/Server.java index 5d18367..1d37917 100644 --- a/src/main/java/Server.java +++ b/src/main/java/server/Server.java @@ -1,3 +1,9 @@ +package server; + +import utils.SmallReadableMessage; +import utils.SmallWritableMessage; +import utils.WritableMessage; + import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; @@ -14,7 +20,7 @@ void run(int port) throws IOException { serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); QueryProcessor queryProcessor = new QueryProcessor(); - while (true) { + while (!Thread.interrupted()) { selector.selectNow(); Iterator keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { @@ -24,14 +30,14 @@ void run(int port) throws IOException { SocketChannel socketChannel = serverChannel.accept(); socketChannel.configureBlocking(false); SelectionKey newSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ); - newSelectionKey.attach(new SmallMessage(socketChannel)); + newSelectionKey.attach(new SmallReadableMessage(socketChannel)); } if (selectionKey.isReadable()) { - SmallMessage message = (SmallMessage) selectionKey.attachment(); + SmallReadableMessage message = (SmallReadableMessage) selectionKey.attachment(); if (message.read()) { selectionKey.cancel(); SelectionKey newSelectionKey = selectionKey.channel().register(selector, SelectionKey.OP_WRITE); - newSelectionKey.attach(new WritableMessage(message.getChannel(), queryProcessor.process(message.getData()))); + newSelectionKey.attach(new SmallWritableMessage(message.getChannel(), queryProcessor.process(message.getData()))); } } diff --git a/src/main/java/server/ServerConsoleApp.java b/src/main/java/server/ServerConsoleApp.java new file mode 100644 index 0000000..b6bf908 --- /dev/null +++ b/src/main/java/server/ServerConsoleApp.java @@ -0,0 +1,45 @@ +package server; + + +import org.jetbrains.annotations.Nullable; + +import java.io.DataInputStream; +import java.io.IOException; + +public class ServerConsoleApp { + public static void main(String[] args) throws IOException { + DataInputStream inputStream = new DataInputStream(System.in); + @Nullable + Thread serverThread = null; + while (true) { + if (inputStream.available() > 0) { + String command = inputStream.readUTF(); + switch(command) { + case "start": { + int port = inputStream.readInt(); + serverThread = new Thread(new Runnable() { + @Override + public void run() { + Server server = new Server(); + try { + server.run(port); + } catch (IOException e) { + System.err.println("Server run failed: " + e.getMessage()); + } + } + }); + serverThread.start(); + } + case "stop": { + if (serverThread == null) { + System.err.println("Server not started!"); + } + else { + serverThread.interrupt(); + } + } + } + } + } + } +} diff --git a/src/main/java/utils/BigReadableMessage.java b/src/main/java/utils/BigReadableMessage.java new file mode 100644 index 0000000..492b208 --- /dev/null +++ b/src/main/java/utils/BigReadableMessage.java @@ -0,0 +1,23 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.nio.file.Files; + +public class BigReadableMessage extends ReadableMessage { + @NotNull + private String filename; + + public BigReadableMessage(@NotNull SocketChannel channel) throws IOException { + super(channel, new KnownFileOutputStream(Files.createTempFile("dwnl", "").toFile())); + filename = ((KnownFileOutputStream) getDestination()).getFile().getName(); + } + + + @NotNull + public String getFilename() { + return filename; + } +} diff --git a/src/main/java/utils/BigWritableMessage.java b/src/main/java/utils/BigWritableMessage.java new file mode 100644 index 0000000..d2152a6 --- /dev/null +++ b/src/main/java/utils/BigWritableMessage.java @@ -0,0 +1,16 @@ +package utils; + + +import org.jetbrains.annotations.NotNull; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; + +public class BigWritableMessage extends WritableMessage { + public BigWritableMessage(@NotNull SocketChannel channel, @NotNull Path source) throws FileNotFoundException { + super(channel, new FileInputStream(source.toFile())); + } +} diff --git a/src/main/java/utils/KnownFileOutputStream.java b/src/main/java/utils/KnownFileOutputStream.java new file mode 100644 index 0000000..28d72e0 --- /dev/null +++ b/src/main/java/utils/KnownFileOutputStream.java @@ -0,0 +1,18 @@ +package utils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; + +public class KnownFileOutputStream extends FileOutputStream { + private final File file; + + public KnownFileOutputStream(File file) throws FileNotFoundException { + super(file); + this.file = file; + } + public File getFile() { + return file; + } +} diff --git a/src/main/java/ReadableMessage.java b/src/main/java/utils/ReadableMessage.java similarity index 58% rename from src/main/java/ReadableMessage.java rename to src/main/java/utils/ReadableMessage.java index ba0547a..1e27dd3 100644 --- a/src/main/java/ReadableMessage.java +++ b/src/main/java/utils/ReadableMessage.java @@ -1,18 +1,23 @@ -import com.sun.istack.internal.NotNull; +package utils; + +import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; -abstract class ReadableMessage { +public class ReadableMessage { private static final int BUFFER_SIZE = 4096; + private final OutputStream destination; private SocketChannel channel; private long size = 0; private int read = 0; private ByteBuffer buffer; - ReadableMessage(@NotNull SocketChannel channel) { + ReadableMessage(@NotNull SocketChannel channel, OutputStream destination) { this.channel = channel; + this.destination = destination; buffer = ByteBuffer.allocate(BUFFER_SIZE); } @@ -20,7 +25,7 @@ public SocketChannel getChannel() { return channel; } - boolean read() throws IOException { + public boolean read() throws IOException { if (read < 4) { read += channel.read(buffer); if (read >= 4) { @@ -31,9 +36,13 @@ boolean read() throws IOException { } } channel.read(buffer); - process(buffer); + while (buffer.hasRemaining()) { + destination.write(buffer.get()); + } return read == size; } - protected abstract void process(ByteBuffer buffer); + protected OutputStream getDestination() { + return destination; + } } diff --git a/src/main/java/SimpleFile.java b/src/main/java/utils/SimpleFile.java similarity index 82% rename from src/main/java/SimpleFile.java rename to src/main/java/utils/SimpleFile.java index 9f28ea6..03114e1 100644 --- a/src/main/java/SimpleFile.java +++ b/src/main/java/utils/SimpleFile.java @@ -1,4 +1,6 @@ -class SimpleFile { +package utils; + +public class SimpleFile { public final String name; public final boolean isDirectory; diff --git a/src/main/java/utils/SmallReadableMessage.java b/src/main/java/utils/SmallReadableMessage.java new file mode 100644 index 0000000..d467a95 --- /dev/null +++ b/src/main/java/utils/SmallReadableMessage.java @@ -0,0 +1,18 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.nio.channels.SocketChannel; + +public class SmallReadableMessage extends ReadableMessage { + public SmallReadableMessage(@NotNull SocketChannel channel) { + super(channel, new ByteArrayOutputStream()); + } + + @NotNull + public byte[] getData() { + return ((ByteArrayOutputStream) getDestination()).toByteArray(); + } + +} diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java new file mode 100644 index 0000000..f4235ab --- /dev/null +++ b/src/main/java/utils/SmallWritableMessage.java @@ -0,0 +1,13 @@ +package utils; + + +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.nio.channels.SocketChannel; + +public class SmallWritableMessage extends WritableMessage { + public SmallWritableMessage(@NotNull SocketChannel channel, @NotNull byte[] data) { + super(channel, new ByteArrayInputStream(data)); + } +} diff --git a/src/main/java/utils/WritableMessage.java b/src/main/java/utils/WritableMessage.java new file mode 100644 index 0000000..6def42a --- /dev/null +++ b/src/main/java/utils/WritableMessage.java @@ -0,0 +1,44 @@ +package utils; + +import com.sun.istack.internal.NotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +public class WritableMessage { + private static final int BUFFER_SIZE = 4096; + private final ByteBuffer buffer; + private int position; + private SocketChannel channel; + private InputStream source; + public WritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source) { + this.channel = channel; + this.source = source; + this.buffer = ByteBuffer.allocate(BUFFER_SIZE); + this.buffer.clear(); + } + + /** + * Calls write() on underlying channel with remaining data. + * @return whether it has completed message writing + */ + public boolean write() throws IOException { + if (buffer.hasRemaining()) { + channel.write(buffer); + return false; + } + else { + buffer.clear(); + } + if (source.available() == 0) { + return true; + } + + //noinspection ResultOfMethodCallIgnored + source.read(buffer.array()); + buffer.flip(); + return false; + } +} From 0862f988f893a825af0875bbc594f0482234ff35 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 12:18:45 +0300 Subject: [PATCH 04/24] Travis config added --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a1866fa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: java + +jdk: +- oraclejdk8 \ No newline at end of file From 96ac345b45923ddb342263a9bb96cf174d0ca3b0 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 12:23:01 +0300 Subject: [PATCH 05/24] Gradle build updated --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 1768b39..57d0b70 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ repositories { } dependencies { + compile group: 'org.jetbrains', name: 'annotations', version: '13.0' testCompile group: 'junit', name: 'junit', version: '4.11' compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0' } From ec25dbe11f2a7fcfd42bc674cc853089c2097b95 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 12:28:09 +0300 Subject: [PATCH 06/24] Fixed wrong lib for annotations --- src/main/java/server/QueryProcessor.java | 2 +- src/main/java/utils/WritableMessage.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index 095a311..2a6b949 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -1,6 +1,6 @@ package server; -import com.sun.istack.internal.NotNull; +import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; import java.io.DataInputStream; diff --git a/src/main/java/utils/WritableMessage.java b/src/main/java/utils/WritableMessage.java index 6def42a..88251c6 100644 --- a/src/main/java/utils/WritableMessage.java +++ b/src/main/java/utils/WritableMessage.java @@ -1,6 +1,7 @@ package utils; -import com.sun.istack.internal.NotNull; + +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.InputStream; From d707091f6f17f68747f93f839e3ebfd34222edb2 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 13:35:56 +0300 Subject: [PATCH 07/24] Bug fixes --- src/main/java/client/Client.java | 12 +++-- src/main/java/client/ClientConsoleApp.java | 15 ++++++ src/main/java/server/Server.java | 15 ++++-- src/main/java/server/ServerConsoleApp.java | 52 ++++++++++--------- src/main/java/utils/ReadableMessage.java | 4 ++ src/main/java/utils/SmallWritableMessage.java | 1 + src/main/java/utils/WritableMessage.java | 14 ++--- 7 files changed, 71 insertions(+), 42 deletions(-) diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java index 1340ad9..2014c25 100644 --- a/src/main/java/client/Client.java +++ b/src/main/java/client/Client.java @@ -10,6 +10,7 @@ import java.nio.ByteBuffer; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; import java.util.List; public class Client { @@ -23,11 +24,14 @@ public Client() throws IOException { } public void connect(String hostname, int port) throws IOException { + System.out.println("connecting to " + hostname + ":" + port); if (channel.isConnected()) return; channel.connect(new InetSocketAddress(hostname, port)); //noinspection StatementWithEmptyBody - while(channel.isConnectionPending()); + while(!channel.finishConnect()) { + //System.out.println("not connected"); + } } public void disconnect() throws IOException { channel.close(); @@ -49,7 +53,7 @@ public List executeList(String path) throws IOException { public String executeGet(String path) throws IOException { sendRequest(createSentData((byte) Command.GET.ordinal(), path.getBytes())); - return getBigResponse().getFilename(); + return new String(getSmallResponse(), StandardCharsets.UTF_8); } private byte[] getSmallResponse() throws IOException { SmallReadableMessage message = new SmallReadableMessage(channel); @@ -79,8 +83,8 @@ private void sendRequest(byte[] data) throws IOException { } private byte[] createSentData(byte first, byte[] rest) { - long size = Long.BYTES + rest.length; - return ArrayUtils.addAll(ByteBuffer.allocate(4).putLong(size).put(first).array(), rest); + long size = Long.BYTES + 1 + rest.length; + return ArrayUtils.addAll(ByteBuffer.allocate(1 + Long.BYTES).putLong(size).put(first).array(), rest); } } diff --git a/src/main/java/client/ClientConsoleApp.java b/src/main/java/client/ClientConsoleApp.java index 04506a8..842ada5 100644 --- a/src/main/java/client/ClientConsoleApp.java +++ b/src/main/java/client/ClientConsoleApp.java @@ -1,13 +1,28 @@ package client; +import server.Server; import utils.SimpleFile; import java.io.IOException; public class ClientConsoleApp { public static void main(String[] args) throws IOException { + String hostname = args[1]; + /*int port = Integer.valueOf(args[2]); + Thread serverThread = new Thread(() -> { + Server server = new Server(); + try { + server.run(hostname, port); + } catch (IOException e) { + System.err.println("Server run failed: " + e.getMessage()); + } + }); + serverThread.start(); + */ Client client = new Client(); + System.out.println("heh"); client.connect(args[1], Integer.valueOf(args[2])); + System.out.println("meh"); switch (args[0]) { case "get": { String filename = client.executeGet(args[3]); diff --git a/src/main/java/server/Server.java b/src/main/java/server/Server.java index 1d37917..7337892 100644 --- a/src/main/java/server/Server.java +++ b/src/main/java/server/Server.java @@ -13,14 +13,23 @@ import java.util.Iterator; public class Server { - void run(int port) throws IOException { + public void run(String hostname, int port) throws IOException { + System.out.println("run"); Selector selector = Selector.open(); + System.out.println("1"); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); - serverSocketChannel.bind(new InetSocketAddress(port)); + System.out.println("2"); + System.out.println("binded to " + hostname + ":" + port); + serverSocketChannel.bind(new InetSocketAddress(hostname, port)); + System.out.println("3"); serverSocketChannel.configureBlocking(false); + System.out.println("4"); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + System.out.println("5"); QueryProcessor queryProcessor = new QueryProcessor(); - while (!Thread.interrupted()) { + System.out.println("6"); + while (true) { + //System.out.println("hey"); selector.selectNow(); Iterator keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { diff --git a/src/main/java/server/ServerConsoleApp.java b/src/main/java/server/ServerConsoleApp.java index b6bf908..a855714 100644 --- a/src/main/java/server/ServerConsoleApp.java +++ b/src/main/java/server/ServerConsoleApp.java @@ -5,39 +5,41 @@ import java.io.DataInputStream; import java.io.IOException; +import java.util.Scanner; public class ServerConsoleApp { public static void main(String[] args) throws IOException { - DataInputStream inputStream = new DataInputStream(System.in); + Scanner scanner = new Scanner(System.in); @Nullable Thread serverThread = null; while (true) { - if (inputStream.available() > 0) { - String command = inputStream.readUTF(); - switch(command) { - case "start": { - int port = inputStream.readInt(); - serverThread = new Thread(new Runnable() { - @Override - public void run() { - Server server = new Server(); - try { - server.run(port); - } catch (IOException e) { - System.err.println("Server run failed: " + e.getMessage()); - } - } - }); - serverThread.start(); - } - case "stop": { - if (serverThread == null) { - System.err.println("Server not started!"); - } - else { - serverThread.interrupt(); + System.out.println("cycling"); + String command = scanner.next(); + switch(command) { + case "start": { + //String hostname = scanner.next(); + //int port = scanner.nextInt(); + String hostname = "localhost"; + int port = 1234; + serverThread = new Thread(() -> { + Server server = new Server(); + try { + server.run(hostname, port); + } catch (IOException e) { + System.err.println("Server run failed: " + e.getMessage()); } + }); + serverThread.start(); + break; + } + case "stop": { + if (serverThread == null) { + System.err.println("Server not started!"); + } + else { + serverThread.interrupt(); } + break; } } } diff --git a/src/main/java/utils/ReadableMessage.java b/src/main/java/utils/ReadableMessage.java index 1e27dd3..2900a50 100644 --- a/src/main/java/utils/ReadableMessage.java +++ b/src/main/java/utils/ReadableMessage.java @@ -29,16 +29,20 @@ public boolean read() throws IOException { if (read < 4) { read += channel.read(buffer); if (read >= 4) { + buffer.flip(); size = buffer.getLong(); + buffer.flip(); } else { return false; } } channel.read(buffer); + buffer.flip(); while (buffer.hasRemaining()) { destination.write(buffer.get()); } + buffer.flip(); return read == size; } diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java index f4235ab..189212b 100644 --- a/src/main/java/utils/SmallWritableMessage.java +++ b/src/main/java/utils/SmallWritableMessage.java @@ -9,5 +9,6 @@ public class SmallWritableMessage extends WritableMessage { public SmallWritableMessage(@NotNull SocketChannel channel, @NotNull byte[] data) { super(channel, new ByteArrayInputStream(data)); + System.out.println("SmallMessage of " + data.length + " bytes"); } } diff --git a/src/main/java/utils/WritableMessage.java b/src/main/java/utils/WritableMessage.java index 88251c6..954ca30 100644 --- a/src/main/java/utils/WritableMessage.java +++ b/src/main/java/utils/WritableMessage.java @@ -18,7 +18,7 @@ public WritableMessage(@NotNull SocketChannel channel, @NotNull InputStream sour this.channel = channel; this.source = source; this.buffer = ByteBuffer.allocate(BUFFER_SIZE); - this.buffer.clear(); + this.buffer.limit(0); } /** @@ -30,16 +30,10 @@ public boolean write() throws IOException { channel.write(buffer); return false; } - else { + else if (source.available() > 0){ buffer.clear(); + buffer.limit(source.read(buffer.array())); } - if (source.available() == 0) { - return true; - } - - //noinspection ResultOfMethodCallIgnored - source.read(buffer.array()); - buffer.flip(); - return false; + return source.available() == 0 && !buffer.hasRemaining(); } } From 37bb0ad8ea2349fa5d9732a6196dece19a0ad9b1 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 14:19:35 +0300 Subject: [PATCH 08/24] Fixed more bugs, now works --- src/main/java/client/Client.java | 9 +++---- src/main/java/client/ClientConsoleApp.java | 7 +++-- src/main/java/server/QueryProcessor.java | 27 ++++++++++++++----- src/main/java/server/Server.java | 2 -- src/main/java/utils/ReadableMessage.java | 9 ++++--- src/main/java/utils/SimpleFile.java | 4 ++- src/main/java/utils/SmallWritableMessage.java | 9 ++++++- 7 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java index 2014c25..b8b9203 100644 --- a/src/main/java/client/Client.java +++ b/src/main/java/client/Client.java @@ -14,7 +14,7 @@ import java.util.List; public class Client { - private enum Command {LIST, GET} + public enum Query {LIST, GET} private final SocketChannel channel; @@ -38,7 +38,7 @@ public void disconnect() throws IOException { } public List executeList(String path) throws IOException { - sendRequest(createSentData((byte) Command.LIST.ordinal(), path.getBytes())); + sendRequest(createSentData((byte) Query.LIST.ordinal(), path.getBytes())); byte[] response = getSmallResponse(); ObjectInputStream objectStream = new ObjectInputStream(new ByteArrayInputStream(response)); try { @@ -52,7 +52,7 @@ public List executeList(String path) throws IOException { public String executeGet(String path) throws IOException { - sendRequest(createSentData((byte) Command.GET.ordinal(), path.getBytes())); + sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); return new String(getSmallResponse(), StandardCharsets.UTF_8); } private byte[] getSmallResponse() throws IOException { @@ -83,8 +83,7 @@ private void sendRequest(byte[] data) throws IOException { } private byte[] createSentData(byte first, byte[] rest) { - long size = Long.BYTES + 1 + rest.length; - return ArrayUtils.addAll(ByteBuffer.allocate(1 + Long.BYTES).putLong(size).put(first).array(), rest); + return ArrayUtils.addAll(ByteBuffer.allocate(1).put(first).array(), rest); } } diff --git a/src/main/java/client/ClientConsoleApp.java b/src/main/java/client/ClientConsoleApp.java index 842ada5..41e4aae 100644 --- a/src/main/java/client/ClientConsoleApp.java +++ b/src/main/java/client/ClientConsoleApp.java @@ -8,7 +8,8 @@ public class ClientConsoleApp { public static void main(String[] args) throws IOException { String hostname = args[1]; - /*int port = Integer.valueOf(args[2]); + /* + int port = Integer.valueOf(args[2]); Thread serverThread = new Thread(() -> { Server server = new Server(); try { @@ -18,7 +19,9 @@ public static void main(String[] args) throws IOException { } }); serverThread.start(); - */ + + //*/ + Client client = new Client(); System.out.println("heh"); client.connect(args[1], Integer.valueOf(args[2])); diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index 2a6b949..2a3de8c 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -1,23 +1,38 @@ package server; +import client.Client; +import client.Client.Query; +import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream; import org.jetbrains.annotations.NotNull; +import utils.SimpleFile; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; +import java.io.ObjectOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +import static client.Client.Query; class QueryProcessor { byte[] process(@NotNull byte[] data) throws IOException { - DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(data)); - int queryType = inputStream.readInt(); - String path = inputStream.readUTF(); + Query queryType = Query.values()[data[0]]; + String path = new String(data, 1, data.length - 1, StandardCharsets.UTF_8); switch (queryType) { - case 0: { + case GET: { return ("Hello, " + path).getBytes(); } - case 1: { - return ("Goodbye, " + path).getBytes(); + case LIST: { + ByteOutputStream byteStream = new ByteOutputStream(); + ObjectOutputStream objectStream = new ObjectOutputStream(byteStream); + ArrayList files = new ArrayList<>(); + files.add(new SimpleFile("heh", false)); + files.add(new SimpleFile("meh", true)); + objectStream.writeObject(files); + objectStream.flush(); + return byteStream.getBytes(); } default: { throw new UnsupportedOperationException(); diff --git a/src/main/java/server/Server.java b/src/main/java/server/Server.java index 7337892..726c9db 100644 --- a/src/main/java/server/Server.java +++ b/src/main/java/server/Server.java @@ -44,11 +44,9 @@ public void run(String hostname, int port) throws IOException { if (selectionKey.isReadable()) { SmallReadableMessage message = (SmallReadableMessage) selectionKey.attachment(); if (message.read()) { - selectionKey.cancel(); SelectionKey newSelectionKey = selectionKey.channel().register(selector, SelectionKey.OP_WRITE); newSelectionKey.attach(new SmallWritableMessage(message.getChannel(), queryProcessor.process(message.getData()))); } - } if (selectionKey.isWritable()) { WritableMessage message = (WritableMessage) selectionKey.attachment(); diff --git a/src/main/java/utils/ReadableMessage.java b/src/main/java/utils/ReadableMessage.java index 2900a50..f7f2146 100644 --- a/src/main/java/utils/ReadableMessage.java +++ b/src/main/java/utils/ReadableMessage.java @@ -31,19 +31,20 @@ public boolean read() throws IOException { if (read >= 4) { buffer.flip(); size = buffer.getLong(); - buffer.flip(); } else { return false; } } - channel.read(buffer); - buffer.flip(); while (buffer.hasRemaining()) { destination.write(buffer.get()); } + buffer.clear(); + if (read < size) { + read += channel.read(buffer); + } buffer.flip(); - return read == size; + return read == size && !buffer.hasRemaining(); } protected OutputStream getDestination() { diff --git a/src/main/java/utils/SimpleFile.java b/src/main/java/utils/SimpleFile.java index 03114e1..bd5dde8 100644 --- a/src/main/java/utils/SimpleFile.java +++ b/src/main/java/utils/SimpleFile.java @@ -1,6 +1,8 @@ package utils; -public class SimpleFile { +import java.io.Serializable; + +public class SimpleFile implements Serializable{ public final String name; public final boolean isDirectory; diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java index 189212b..96f22c5 100644 --- a/src/main/java/utils/SmallWritableMessage.java +++ b/src/main/java/utils/SmallWritableMessage.java @@ -1,14 +1,21 @@ package utils; +import org.apache.commons.lang3.ArrayUtils; import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; +import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class SmallWritableMessage extends WritableMessage { public SmallWritableMessage(@NotNull SocketChannel channel, @NotNull byte[] data) { - super(channel, new ByteArrayInputStream(data)); + super(channel, new ByteArrayInputStream(addHeader(data))); System.out.println("SmallMessage of " + data.length + " bytes"); } + + private static byte[] addHeader(byte[] data) { + long size = Long.BYTES + data.length; + return ArrayUtils.addAll(ByteBuffer.allocate(Long.BYTES).putLong(size).array(), data); + } } From 8943c904349e2ae8473477cd3ef01ea670bc6ebe Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 14:23:06 +0300 Subject: [PATCH 09/24] Fixed wrong import --- src/main/java/server/QueryProcessor.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index 2a3de8c..6368aa8 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -1,20 +1,15 @@ package server; -import client.Client; import client.Client.Query; -import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream; import org.jetbrains.annotations.NotNull; import utils.SimpleFile; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import static client.Client.Query; - class QueryProcessor { byte[] process(@NotNull byte[] data) throws IOException { Query queryType = Query.values()[data[0]]; @@ -25,14 +20,14 @@ byte[] process(@NotNull byte[] data) throws IOException { return ("Hello, " + path).getBytes(); } case LIST: { - ByteOutputStream byteStream = new ByteOutputStream(); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); ObjectOutputStream objectStream = new ObjectOutputStream(byteStream); ArrayList files = new ArrayList<>(); files.add(new SimpleFile("heh", false)); files.add(new SimpleFile("meh", true)); objectStream.writeObject(files); objectStream.flush(); - return byteStream.getBytes(); + return byteStream.toByteArray(); } default: { throw new UnsupportedOperationException(); From 1688940434b99c38f6d73ca00cb610ca07960f13 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 14:34:15 +0300 Subject: [PATCH 10/24] Added list functionality --- src/main/java/server/QueryProcessor.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index 6368aa8..025063b 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -8,7 +8,10 @@ import java.io.IOException; import java.io.ObjectOutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.stream.Collectors; class QueryProcessor { byte[] process(@NotNull byte[] data) throws IOException { @@ -22,12 +25,11 @@ byte[] process(@NotNull byte[] data) throws IOException { case LIST: { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); ObjectOutputStream objectStream = new ObjectOutputStream(byteStream); - ArrayList files = new ArrayList<>(); - files.add(new SimpleFile("heh", false)); - files.add(new SimpleFile("meh", true)); + ArrayList files = new ArrayList<>(Files.list(Paths.get(path)) + .map(p -> new SimpleFile(p.toString(), Files.isDirectory(p))).collect(Collectors.toList())); objectStream.writeObject(files); objectStream.flush(); - return byteStream.toByteArray(); + return byteStream.toByteArray() ; } default: { throw new UnsupportedOperationException(); From 4163b57cfbb992f30143afe1f06519d162371f8d Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 15:10:29 +0300 Subject: [PATCH 11/24] Added get query --- src/main/java/client/Client.java | 2 +- src/main/java/server/QueryProcessor.java | 9 ++++++--- src/main/java/server/Server.java | 8 +------- src/main/java/utils/BigReadableMessage.java | 2 +- src/main/java/utils/BigWritableMessage.java | 19 +++++++++++++----- src/main/java/utils/ByteUtils.java | 18 +++++++++++++++++ src/main/java/utils/GetResponse.java | 20 +++++++++++++++++++ src/main/java/utils/ListResponse.java | 19 ++++++++++++++++++ src/main/java/utils/Response.java | 9 +++++++++ src/main/java/utils/SmallWritableMessage.java | 9 ++++++--- src/main/java/utils/WritableMessage.java | 13 +++++++----- 11 files changed, 103 insertions(+), 25 deletions(-) create mode 100644 src/main/java/utils/ByteUtils.java create mode 100644 src/main/java/utils/GetResponse.java create mode 100644 src/main/java/utils/ListResponse.java create mode 100644 src/main/java/utils/Response.java diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java index b8b9203..8879a23 100644 --- a/src/main/java/client/Client.java +++ b/src/main/java/client/Client.java @@ -53,7 +53,7 @@ public List executeList(String path) throws IOException { public String executeGet(String path) throws IOException { sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); - return new String(getSmallResponse(), StandardCharsets.UTF_8); + return getBigResponse().getFilename(); } private byte[] getSmallResponse() throws IOException { SmallReadableMessage message = new SmallReadableMessage(channel); diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index 025063b..a6f36bc 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -2,6 +2,9 @@ import client.Client.Query; import org.jetbrains.annotations.NotNull; +import utils.GetResponse; +import utils.ListResponse; +import utils.Response; import utils.SimpleFile; import java.io.ByteArrayOutputStream; @@ -14,13 +17,13 @@ import java.util.stream.Collectors; class QueryProcessor { - byte[] process(@NotNull byte[] data) throws IOException { + Response process(@NotNull byte[] data) throws IOException { Query queryType = Query.values()[data[0]]; String path = new String(data, 1, data.length - 1, StandardCharsets.UTF_8); switch (queryType) { case GET: { - return ("Hello, " + path).getBytes(); + return new GetResponse(Paths.get(path)); } case LIST: { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); @@ -29,7 +32,7 @@ byte[] process(@NotNull byte[] data) throws IOException { .map(p -> new SimpleFile(p.toString(), Files.isDirectory(p))).collect(Collectors.toList())); objectStream.writeObject(files); objectStream.flush(); - return byteStream.toByteArray() ; + return new ListResponse(byteStream.toByteArray()); } default: { throw new UnsupportedOperationException(); diff --git a/src/main/java/server/Server.java b/src/main/java/server/Server.java index 726c9db..eaa790f 100644 --- a/src/main/java/server/Server.java +++ b/src/main/java/server/Server.java @@ -16,18 +16,12 @@ public class Server { public void run(String hostname, int port) throws IOException { System.out.println("run"); Selector selector = Selector.open(); - System.out.println("1"); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); - System.out.println("2"); System.out.println("binded to " + hostname + ":" + port); serverSocketChannel.bind(new InetSocketAddress(hostname, port)); - System.out.println("3"); serverSocketChannel.configureBlocking(false); - System.out.println("4"); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); - System.out.println("5"); QueryProcessor queryProcessor = new QueryProcessor(); - System.out.println("6"); while (true) { //System.out.println("hey"); selector.selectNow(); @@ -45,7 +39,7 @@ public void run(String hostname, int port) throws IOException { SmallReadableMessage message = (SmallReadableMessage) selectionKey.attachment(); if (message.read()) { SelectionKey newSelectionKey = selectionKey.channel().register(selector, SelectionKey.OP_WRITE); - newSelectionKey.attach(new SmallWritableMessage(message.getChannel(), queryProcessor.process(message.getData()))); + newSelectionKey.attach(queryProcessor.process(message.getData()).generateMessage(message.getChannel())); } } if (selectionKey.isWritable()) { diff --git a/src/main/java/utils/BigReadableMessage.java b/src/main/java/utils/BigReadableMessage.java index 492b208..95d6d06 100644 --- a/src/main/java/utils/BigReadableMessage.java +++ b/src/main/java/utils/BigReadableMessage.java @@ -12,7 +12,7 @@ public class BigReadableMessage extends ReadableMessage { public BigReadableMessage(@NotNull SocketChannel channel) throws IOException { super(channel, new KnownFileOutputStream(Files.createTempFile("dwnl", "").toFile())); - filename = ((KnownFileOutputStream) getDestination()).getFile().getName(); + filename = ((KnownFileOutputStream) getDestination()).getFile().getAbsolutePath(); } diff --git a/src/main/java/utils/BigWritableMessage.java b/src/main/java/utils/BigWritableMessage.java index d2152a6..216309e 100644 --- a/src/main/java/utils/BigWritableMessage.java +++ b/src/main/java/utils/BigWritableMessage.java @@ -3,14 +3,23 @@ import org.jetbrains.annotations.NotNull; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; +import java.io.*; import java.nio.channels.SocketChannel; +import java.nio.file.Files; import java.nio.file.Path; public class BigWritableMessage extends WritableMessage { - public BigWritableMessage(@NotNull SocketChannel channel, @NotNull Path source) throws FileNotFoundException { - super(channel, new FileInputStream(source.toFile())); + public BigWritableMessage(@NotNull SocketChannel channel, @NotNull Path source) throws IOException { + super(channel, createMessageInputStream(source), getSize(source)); + } + + private static InputStream createMessageInputStream(Path source) throws IOException { + InputStream stream1 = new ByteArrayInputStream(ByteUtils.longToBytes(getSize(source))); + InputStream stream2 = new FileInputStream(source.toFile()); + return new SequenceInputStream(stream1, stream2); + } + + private static long getSize(Path source) throws IOException { + return Long.BYTES + Files.size(source); } } diff --git a/src/main/java/utils/ByteUtils.java b/src/main/java/utils/ByteUtils.java new file mode 100644 index 0000000..3068f95 --- /dev/null +++ b/src/main/java/utils/ByteUtils.java @@ -0,0 +1,18 @@ +package utils; + +import java.nio.ByteBuffer; + +public class ByteUtils { + private static ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); + + public static byte[] longToBytes(long x) { + buffer.putLong(0, x); + return buffer.array(); + } + + public static long bytesToLong(byte[] bytes) { + buffer.put(bytes, 0, bytes.length); + buffer.flip(); + return buffer.getLong(); + } +} \ No newline at end of file diff --git a/src/main/java/utils/GetResponse.java b/src/main/java/utils/GetResponse.java new file mode 100644 index 0000000..afc5423 --- /dev/null +++ b/src/main/java/utils/GetResponse.java @@ -0,0 +1,20 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; + +public class GetResponse implements Response { + @NotNull private Path path; + + public GetResponse(@NotNull Path path) { + this.path = path; + } + + @Override + public WritableMessage generateMessage(@NotNull SocketChannel channel) throws IOException { + return new BigWritableMessage(channel, path); + } +} diff --git a/src/main/java/utils/ListResponse.java b/src/main/java/utils/ListResponse.java new file mode 100644 index 0000000..611f5b0 --- /dev/null +++ b/src/main/java/utils/ListResponse.java @@ -0,0 +1,19 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.FileNotFoundException; +import java.nio.channels.SocketChannel; + +public class ListResponse implements Response { + @NotNull private byte[] data; + + public ListResponse(@NotNull byte[] data) { + this.data = data; + } + + @Override + public WritableMessage generateMessage(SocketChannel channel) throws FileNotFoundException { + return new SmallWritableMessage(channel, data); + } +} diff --git a/src/main/java/utils/Response.java b/src/main/java/utils/Response.java new file mode 100644 index 0000000..74de29f --- /dev/null +++ b/src/main/java/utils/Response.java @@ -0,0 +1,9 @@ +package utils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.channels.SocketChannel; + +public interface Response { + WritableMessage generateMessage(SocketChannel channel) throws IOException; +} diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java index 96f22c5..5a40f5f 100644 --- a/src/main/java/utils/SmallWritableMessage.java +++ b/src/main/java/utils/SmallWritableMessage.java @@ -10,12 +10,15 @@ public class SmallWritableMessage extends WritableMessage { public SmallWritableMessage(@NotNull SocketChannel channel, @NotNull byte[] data) { - super(channel, new ByteArrayInputStream(addHeader(data))); + super(channel, new ByteArrayInputStream(addHeader(data)), getSize(data)); System.out.println("SmallMessage of " + data.length + " bytes"); } + private static long getSize(byte[] data) { + return Long.BYTES + data.length; + } + private static byte[] addHeader(byte[] data) { - long size = Long.BYTES + data.length; - return ArrayUtils.addAll(ByteBuffer.allocate(Long.BYTES).putLong(size).array(), data); + return ArrayUtils.addAll(ByteUtils.longToBytes(getSize(data)), data); } } diff --git a/src/main/java/utils/WritableMessage.java b/src/main/java/utils/WritableMessage.java index 954ca30..a282ece 100644 --- a/src/main/java/utils/WritableMessage.java +++ b/src/main/java/utils/WritableMessage.java @@ -11,10 +11,11 @@ public class WritableMessage { private static final int BUFFER_SIZE = 4096; private final ByteBuffer buffer; - private int position; + private long leftSource; private SocketChannel channel; private InputStream source; - public WritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source) { + public WritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source, long size) { + this.leftSource = size; this.channel = channel; this.source = source; this.buffer = ByteBuffer.allocate(BUFFER_SIZE); @@ -30,10 +31,12 @@ public boolean write() throws IOException { channel.write(buffer); return false; } - else if (source.available() > 0){ + else if (leftSource > 0){ buffer.clear(); - buffer.limit(source.read(buffer.array())); + int read = source.read(buffer.array()); + buffer.limit(read); + leftSource -= read; } - return source.available() == 0 && !buffer.hasRemaining(); + return leftSource == 0 && !buffer.hasRemaining(); } } From c4d4fc7ab7aa7c0cbdde673d1e603c054fdc36dc Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 18:55:10 +0300 Subject: [PATCH 12/24] Warnings fixed --- src/main/java/client/Client.java | 9 ++-- src/main/java/client/ClientConsoleApp.java | 16 ------- src/main/java/server/FileSystem.java | 42 +++++++++++++++++++ src/main/java/server/QueryProcessor.java | 9 ++-- src/main/java/server/Server.java | 15 +++++-- src/main/java/server/ServerConsoleApp.java | 26 ++++++++---- src/main/java/utils/BigWritableMessage.java | 22 +++++----- src/main/java/utils/ByteUtils.java | 18 ++++---- src/main/java/utils/GetResponse.java | 10 ++--- .../java/utils/KnownFileOutputStream.java | 7 ++-- src/main/java/utils/ReadableMessage.java | 3 +- src/main/java/utils/Response.java | 1 - src/main/java/utils/SmallWritableMessage.java | 1 - src/main/java/utils/WritableMessage.java | 3 +- src/test/java/IntegrationTest.java | 23 ++++++++++ src/test/java/server/QueryProcessorTest.java | 12 ++++++ 16 files changed, 148 insertions(+), 69 deletions(-) create mode 100644 src/main/java/server/FileSystem.java create mode 100644 src/test/java/IntegrationTest.java create mode 100644 src/test/java/server/QueryProcessorTest.java diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java index 8879a23..1237b8c 100644 --- a/src/main/java/client/Client.java +++ b/src/main/java/client/Client.java @@ -7,10 +7,8 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.net.InetSocketAddress; -import java.nio.ByteBuffer; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SocketChannel; -import java.nio.charset.StandardCharsets; import java.util.List; public class Client { @@ -18,11 +16,13 @@ public enum Query {LIST, GET} private final SocketChannel channel; + @SuppressWarnings("WeakerAccess") public Client() throws IOException { channel = SocketChannel.open(); channel.configureBlocking(false); } + @SuppressWarnings("WeakerAccess") public void connect(String hostname, int port) throws IOException { System.out.println("connecting to " + hostname + ":" + port); if (channel.isConnected()) return; @@ -33,10 +33,12 @@ public void connect(String hostname, int port) throws IOException { //System.out.println("not connected"); } } + @SuppressWarnings("WeakerAccess") public void disconnect() throws IOException { channel.close(); } + @SuppressWarnings("WeakerAccess") public List executeList(String path) throws IOException { sendRequest(createSentData((byte) Query.LIST.ordinal(), path.getBytes())); byte[] response = getSmallResponse(); @@ -51,6 +53,7 @@ public List executeList(String path) throws IOException { } + @SuppressWarnings("WeakerAccess") public String executeGet(String path) throws IOException { sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); return getBigResponse().getFilename(); @@ -83,7 +86,7 @@ private void sendRequest(byte[] data) throws IOException { } private byte[] createSentData(byte first, byte[] rest) { - return ArrayUtils.addAll(ByteBuffer.allocate(1).put(first).array(), rest); + return ArrayUtils.addAll(ByteUtils.byteToBytes(first), rest); } } diff --git a/src/main/java/client/ClientConsoleApp.java b/src/main/java/client/ClientConsoleApp.java index 41e4aae..4d3c49b 100644 --- a/src/main/java/client/ClientConsoleApp.java +++ b/src/main/java/client/ClientConsoleApp.java @@ -1,27 +1,11 @@ package client; -import server.Server; import utils.SimpleFile; import java.io.IOException; public class ClientConsoleApp { public static void main(String[] args) throws IOException { - String hostname = args[1]; - /* - int port = Integer.valueOf(args[2]); - Thread serverThread = new Thread(() -> { - Server server = new Server(); - try { - server.run(hostname, port); - } catch (IOException e) { - System.err.println("Server run failed: " + e.getMessage()); - } - }); - serverThread.start(); - - //*/ - Client client = new Client(); System.out.println("heh"); client.connect(args[1], Integer.valueOf(args[2])); diff --git a/src/main/java/server/FileSystem.java b/src/main/java/server/FileSystem.java new file mode 100644 index 0000000..a60662b --- /dev/null +++ b/src/main/java/server/FileSystem.java @@ -0,0 +1,42 @@ +package server; + +import utils.SimpleFile; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.stream.Collectors; + + +public class FileSystem { + private final Path root; + + @SuppressWarnings("WeakerAccess") + public FileSystem(Path root) { + this.root = root; + } + + @SuppressWarnings("WeakerAccess") + public ArrayList list(Path path) throws IOException { + assertChild(path, root); + return new ArrayList<>(Files.list(path).map(p -> new SimpleFile(p.toString(), Files.isDirectory(p))).collect(Collectors.toList())); + } + + private static void assertChild(Path path, Path root) { + if (!isChild(path, root)) { + throw new SecurityException("Trying to go out of set root"); + } + } + + + private static boolean isChild(Path path, Path root) { + return path.toAbsolutePath().startsWith(root.toAbsolutePath()); + } + + @SuppressWarnings("WeakerAccess") + public InputStream getOutputStream(Path path) throws IOException { + assertChild(path, root); + return Files.newInputStream(path); + } +} \ No newline at end of file diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index a6f36bc..48b53e2 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -11,25 +11,22 @@ import java.io.IOException; import java.io.ObjectOutputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.stream.Collectors; class QueryProcessor { - Response process(@NotNull byte[] data) throws IOException { + Response process(@NotNull byte[] data, FileSystem fileSystem) throws IOException { Query queryType = Query.values()[data[0]]; String path = new String(data, 1, data.length - 1, StandardCharsets.UTF_8); switch (queryType) { case GET: { - return new GetResponse(Paths.get(path)); + return new GetResponse(fileSystem.getOutputStream(Paths.get(path))); } case LIST: { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); ObjectOutputStream objectStream = new ObjectOutputStream(byteStream); - ArrayList files = new ArrayList<>(Files.list(Paths.get(path)) - .map(p -> new SimpleFile(p.toString(), Files.isDirectory(p))).collect(Collectors.toList())); + ArrayList files = fileSystem.list(Paths.get(path)); objectStream.writeObject(files); objectStream.flush(); return new ListResponse(byteStream.toByteArray()); diff --git a/src/main/java/server/Server.java b/src/main/java/server/Server.java index eaa790f..9db4267 100644 --- a/src/main/java/server/Server.java +++ b/src/main/java/server/Server.java @@ -1,7 +1,6 @@ package server; import utils.SmallReadableMessage; -import utils.SmallWritableMessage; import utils.WritableMessage; import java.io.IOException; @@ -10,10 +9,19 @@ import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +import java.nio.file.Path; import java.util.Iterator; public class Server { + private final Path root; + + public Server(Path root) { + this.root = root; + } + + @SuppressWarnings("WeakerAccess") public void run(String hostname, int port) throws IOException { + FileSystem fileSystem = new FileSystem(root); System.out.println("run"); Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); @@ -22,8 +30,7 @@ public void run(String hostname, int port) throws IOException { serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); QueryProcessor queryProcessor = new QueryProcessor(); - while (true) { - //System.out.println("hey"); + while (!Thread.interrupted()) { selector.selectNow(); Iterator keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { @@ -39,7 +46,7 @@ public void run(String hostname, int port) throws IOException { SmallReadableMessage message = (SmallReadableMessage) selectionKey.attachment(); if (message.read()) { SelectionKey newSelectionKey = selectionKey.channel().register(selector, SelectionKey.OP_WRITE); - newSelectionKey.attach(queryProcessor.process(message.getData()).generateMessage(message.getChannel())); + newSelectionKey.attach(queryProcessor.process(message.getData(), fileSystem).generateMessage(message.getChannel())); } } if (selectionKey.isWritable()) { diff --git a/src/main/java/server/ServerConsoleApp.java b/src/main/java/server/ServerConsoleApp.java index a855714..7a8332f 100644 --- a/src/main/java/server/ServerConsoleApp.java +++ b/src/main/java/server/ServerConsoleApp.java @@ -3,26 +3,26 @@ import org.jetbrains.annotations.Nullable; -import java.io.DataInputStream; import java.io.IOException; +import java.nio.file.Paths; import java.util.Scanner; public class ServerConsoleApp { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); - @Nullable - Thread serverThread = null; + + @Nullable Thread serverThread = null; + while (true) { System.out.println("cycling"); String command = scanner.next(); + boolean shutdown = false; switch(command) { case "start": { - //String hostname = scanner.next(); - //int port = scanner.nextInt(); - String hostname = "localhost"; - int port = 1234; + String hostname = scanner.next(); + int port = scanner.nextInt(); serverThread = new Thread(() -> { - Server server = new Server(); + Server server = new Server(Paths.get(".")); try { server.run(hostname, port); } catch (IOException e) { @@ -41,6 +41,16 @@ public static void main(String[] args) throws IOException { } break; } + case "shutdown": { + shutdown = true; + break; + } + default: { + System.out.println("Usage: start HOST PORT | stop | shutdown"); + } + } + if (shutdown) { + break; } } } diff --git a/src/main/java/utils/BigWritableMessage.java b/src/main/java/utils/BigWritableMessage.java index 216309e..42188f8 100644 --- a/src/main/java/utils/BigWritableMessage.java +++ b/src/main/java/utils/BigWritableMessage.java @@ -3,23 +3,23 @@ import org.jetbrains.annotations.NotNull; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; import java.nio.channels.SocketChannel; -import java.nio.file.Files; -import java.nio.file.Path; -public class BigWritableMessage extends WritableMessage { - public BigWritableMessage(@NotNull SocketChannel channel, @NotNull Path source) throws IOException { +class BigWritableMessage extends WritableMessage { + BigWritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source) throws IOException { super(channel, createMessageInputStream(source), getSize(source)); } - private static InputStream createMessageInputStream(Path source) throws IOException { - InputStream stream1 = new ByteArrayInputStream(ByteUtils.longToBytes(getSize(source))); - InputStream stream2 = new FileInputStream(source.toFile()); - return new SequenceInputStream(stream1, stream2); + private static InputStream createMessageInputStream(InputStream source) throws IOException { + InputStream stream = new ByteArrayInputStream(ByteUtils.longToBytes(getSize(source))); + return new SequenceInputStream(stream, source); } - private static long getSize(Path source) throws IOException { - return Long.BYTES + Files.size(source); + private static long getSize(InputStream source) throws IOException { + return Long.BYTES + source.available(); } } diff --git a/src/main/java/utils/ByteUtils.java b/src/main/java/utils/ByteUtils.java index 3068f95..9bc89c5 100644 --- a/src/main/java/utils/ByteUtils.java +++ b/src/main/java/utils/ByteUtils.java @@ -3,16 +3,18 @@ import java.nio.ByteBuffer; public class ByteUtils { - private static ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); + private static final ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES); + private static final ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES); + @SuppressWarnings("WeakerAccess") public static byte[] longToBytes(long x) { - buffer.putLong(0, x); - return buffer.array(); + longBuffer.putLong(0, x); + longBuffer.clear(); + return longBuffer.array().clone(); } - - public static long bytesToLong(byte[] bytes) { - buffer.put(bytes, 0, bytes.length); - buffer.flip(); - return buffer.getLong(); + public static byte[] byteToBytes(byte x) { + byteBuffer.put(x); + byteBuffer.clear(); + return byteBuffer.array().clone(); } } \ No newline at end of file diff --git a/src/main/java/utils/GetResponse.java b/src/main/java/utils/GetResponse.java index afc5423..8d9438e 100644 --- a/src/main/java/utils/GetResponse.java +++ b/src/main/java/utils/GetResponse.java @@ -3,18 +3,18 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.io.InputStream; import java.nio.channels.SocketChannel; -import java.nio.file.Path; public class GetResponse implements Response { - @NotNull private Path path; + private final InputStream source; - public GetResponse(@NotNull Path path) { - this.path = path; + public GetResponse(@NotNull InputStream source) { + this.source = source; } @Override public WritableMessage generateMessage(@NotNull SocketChannel channel) throws IOException { - return new BigWritableMessage(channel, path); + return new BigWritableMessage(channel, source); } } diff --git a/src/main/java/utils/KnownFileOutputStream.java b/src/main/java/utils/KnownFileOutputStream.java index 28d72e0..9e5c328 100644 --- a/src/main/java/utils/KnownFileOutputStream.java +++ b/src/main/java/utils/KnownFileOutputStream.java @@ -3,16 +3,15 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; -import java.io.OutputStream; -public class KnownFileOutputStream extends FileOutputStream { +class KnownFileOutputStream extends FileOutputStream { private final File file; - public KnownFileOutputStream(File file) throws FileNotFoundException { + KnownFileOutputStream(File file) throws FileNotFoundException { super(file); this.file = file; } - public File getFile() { + File getFile() { return file; } } diff --git a/src/main/java/utils/ReadableMessage.java b/src/main/java/utils/ReadableMessage.java index f7f2146..154e462 100644 --- a/src/main/java/utils/ReadableMessage.java +++ b/src/main/java/utils/ReadableMessage.java @@ -17,6 +17,7 @@ public class ReadableMessage { ReadableMessage(@NotNull SocketChannel channel, OutputStream destination) { this.channel = channel; + // TODO: add buffered this.destination = destination; buffer = ByteBuffer.allocate(BUFFER_SIZE); } @@ -47,7 +48,7 @@ public boolean read() throws IOException { return read == size && !buffer.hasRemaining(); } - protected OutputStream getDestination() { + OutputStream getDestination() { return destination; } } diff --git a/src/main/java/utils/Response.java b/src/main/java/utils/Response.java index 74de29f..dcf13c7 100644 --- a/src/main/java/utils/Response.java +++ b/src/main/java/utils/Response.java @@ -1,6 +1,5 @@ package utils; -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.SocketChannel; diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java index 5a40f5f..00686ce 100644 --- a/src/main/java/utils/SmallWritableMessage.java +++ b/src/main/java/utils/SmallWritableMessage.java @@ -5,7 +5,6 @@ import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; -import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class SmallWritableMessage extends WritableMessage { diff --git a/src/main/java/utils/WritableMessage.java b/src/main/java/utils/WritableMessage.java index a282ece..e4bf5b9 100644 --- a/src/main/java/utils/WritableMessage.java +++ b/src/main/java/utils/WritableMessage.java @@ -14,9 +14,10 @@ public class WritableMessage { private long leftSource; private SocketChannel channel; private InputStream source; - public WritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source, long size) { + WritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source, long size) { this.leftSource = size; this.channel = channel; + // TODO: add buffered this.source = source; this.buffer = ByteBuffer.allocate(BUFFER_SIZE); this.buffer.limit(0); diff --git a/src/test/java/IntegrationTest.java b/src/test/java/IntegrationTest.java new file mode 100644 index 0000000..724f8ae --- /dev/null +++ b/src/test/java/IntegrationTest.java @@ -0,0 +1,23 @@ + +import client.Client; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.Test; +import server.FileSystem; +import server.Server; +import utils.ByteUtils; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import static org.junit.Assert.*; + +public class IntegrationTest { + @Test + public void processGet() throws Exception { + Server server = new Server(Paths.get(".")); + FileSystem fileSystem = new FileSystem(Paths.get(".")); + //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) Client.Query.GET.ordinal()), + } + +} \ No newline at end of file diff --git a/src/test/java/server/QueryProcessorTest.java b/src/test/java/server/QueryProcessorTest.java new file mode 100644 index 0000000..3e0fc35 --- /dev/null +++ b/src/test/java/server/QueryProcessorTest.java @@ -0,0 +1,12 @@ +package server; + +import org.junit.Test; + +public class QueryProcessorTest { + @Test + public void processGet() throws Exception { + + //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) Client.Query.GET.ordinal()), + } + +} \ No newline at end of file From 08c0a3d0c0978436de48eeda2b7e2640c437b69d Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 19:48:14 +0300 Subject: [PATCH 13/24] JavaDocs for client package added --- src/main/java/client/Client.java | 132 ++++++++----------- src/main/java/client/ClientConsoleApp.java | 16 ++- src/main/java/client/NonBlockingClient.java | 97 ++++++++++++++ src/main/java/client/package-info.java | 5 + src/main/java/server/QueryProcessor.java | 2 +- src/test/java/IntegrationTest.java | 9 +- src/test/java/server/QueryProcessorTest.java | 2 +- 7 files changed, 174 insertions(+), 89 deletions(-) create mode 100644 src/main/java/client/NonBlockingClient.java create mode 100644 src/main/java/client/package-info.java diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java index 1237b8c..c989a1f 100644 --- a/src/main/java/client/Client.java +++ b/src/main/java/client/Client.java @@ -1,92 +1,70 @@ package client; -import org.apache.commons.lang3.ArrayUtils; -import utils.*; +import org.jetbrains.annotations.NotNull; +import utils.SimpleFile; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.ObjectInputStream; -import java.net.InetSocketAddress; -import java.nio.channels.NotYetConnectedException; -import java.nio.channels.SocketChannel; +import java.nio.file.Path; import java.util.List; -public class Client { - public enum Query {LIST, GET} +/** + * Interface of a simple FTP server. + */ - private final SocketChannel channel; - - @SuppressWarnings("WeakerAccess") - public Client() throws IOException { - channel = SocketChannel.open(); - channel.configureBlocking(false); - } - - @SuppressWarnings("WeakerAccess") - public void connect(String hostname, int port) throws IOException { - System.out.println("connecting to " + hostname + ":" + port); - if (channel.isConnected()) return; - channel.connect(new InetSocketAddress(hostname, port)); - - //noinspection StatementWithEmptyBody - while(!channel.finishConnect()) { - //System.out.println("not connected"); - } - } - @SuppressWarnings("WeakerAccess") - public void disconnect() throws IOException { - channel.close(); - } - - @SuppressWarnings("WeakerAccess") - public List executeList(String path) throws IOException { - sendRequest(createSentData((byte) Query.LIST.ordinal(), path.getBytes())); - byte[] response = getSmallResponse(); - ObjectInputStream objectStream = new ObjectInputStream(new ByteArrayInputStream(response)); - try { - //noinspection unchecked - return (List) objectStream.readObject(); - } catch (ClassNotFoundException e) { - // TODO: probably change to custom exception - throw new RuntimeException("server.Server fucked up!"); - } +public interface Client { + /** + * Enum of all possible client queries. + */ + enum Query { + /** + * Query to list all files in directory + */ + LIST, + /** + * Query to download file. + */ + GET } + /** + * Tries to establish connection with server hostname:port. + * @param hostname the hostname of server + * @param port the port number + * @throws IOException If some I/O error during connection occurs + */ + void connect(@NotNull String hostname, int port) throws IOException; - @SuppressWarnings("WeakerAccess") - public String executeGet(String path) throws IOException { - sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); - return getBigResponse().getFilename(); - } - private byte[] getSmallResponse() throws IOException { - SmallReadableMessage message = new SmallReadableMessage(channel); - getResponse(message); - return message.getData(); - } + /** + * Closes connection with server. + * @throws IOException If an I/O error during connection close occurs + */ + void disconnect() throws IOException; - private BigReadableMessage getBigResponse() throws IOException { - BigReadableMessage message = new BigReadableMessage(channel); - getResponse(message); - return message; - } + /** + * Return list of files located in the specified directory. + * @param path The path to the directory + * @return List with all files in the directory. + * @throws IOException If an I/O error occurs + */ + @NotNull + List executeList(@NotNull String path) throws IOException; - private void getResponse(ReadableMessage message) throws IOException { - //noinspection StatementWithEmptyBody - while (!message.read()); - } + /** + * Downloads file. + * @param path path to file on server + * @return path to downloaded file on client + * @throws IOException If an I/O error occurs + */ + @NotNull + Path executeGet(@NotNull String path) throws IOException ; - private void sendRequest(byte[] data) throws IOException { - //noinspection StatementWithEmptyBody - if (!channel.isConnected()) { - throw new NotYetConnectedException(); - } - WritableMessage message = new SmallWritableMessage(channel, data); - //noinspection StatementWithEmptyBody - while (!message.write()); + /** + * Creates non-blocking FTP client. + * @return A new FTP client + * @throws IOException If an I/O error occurs + */ + @NotNull + static Client getNonBlocking() throws IOException { + return new NonBlockingClient(); } - - private byte[] createSentData(byte first, byte[] rest) { - return ArrayUtils.addAll(ByteUtils.byteToBytes(first), rest); - } - } diff --git a/src/main/java/client/ClientConsoleApp.java b/src/main/java/client/ClientConsoleApp.java index 4d3c49b..665d786 100644 --- a/src/main/java/client/ClientConsoleApp.java +++ b/src/main/java/client/ClientConsoleApp.java @@ -3,17 +3,27 @@ import utils.SimpleFile; import java.io.IOException; +import java.nio.file.Path; + +/** + * Console application for communication with FTP server. + */ public class ClientConsoleApp { + /** + * Main method. + * @param args command line args + * @throws IOException If an I/O error occurs + */ public static void main(String[] args) throws IOException { - Client client = new Client(); + Client client = Client.getNonBlocking(); System.out.println("heh"); client.connect(args[1], Integer.valueOf(args[2])); System.out.println("meh"); switch (args[0]) { case "get": { - String filename = client.executeGet(args[3]); - System.out.println("Downloaded file to " + filename); + Path path = client.executeGet(args[3]); + System.out.println("Downloaded file to " + path); break; } case "list": { diff --git a/src/main/java/client/NonBlockingClient.java b/src/main/java/client/NonBlockingClient.java new file mode 100644 index 0000000..8c013fd --- /dev/null +++ b/src/main/java/client/NonBlockingClient.java @@ -0,0 +1,97 @@ +package client; + +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.NotNull; +import utils.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.InetSocketAddress; +import java.nio.channels.NotYetConnectedException; +import java.nio.channels.SocketChannel; +import java.util.List; + +/** + * Class that implements non-blocking FTP client. + */ + +public class NonBlockingClient implements Client{ + + private final SocketChannel channel; + + NonBlockingClient() throws IOException { + channel = SocketChannel.open(); + channel.configureBlocking(false); + } + + @Override + public void connect(@NotNull String hostname, int port) throws IOException { + System.out.println("connecting to " + hostname + ":" + port); + if (channel.isConnected()) return; + channel.connect(new InetSocketAddress(hostname, port)); + + //noinspection StatementWithEmptyBody + while(!channel.finishConnect()) { + //System.out.println("not connected"); + } + } + @Override + public void disconnect() throws IOException { + channel.close(); + } + + @NotNull + @Override + public List executeList(@NotNull String path) throws IOException { + sendRequest(createSentData((byte) Query.LIST.ordinal(), path.getBytes())); + byte[] response = getSmallResponse(); + ObjectInputStream objectStream = new ObjectInputStream(new ByteArrayInputStream(response)); + try { + //noinspection unchecked + return (List) objectStream.readObject(); + } catch (ClassNotFoundException e) { + // TODO: probably change to custom exception + throw new RuntimeException("server.Server fucked up!"); + } + } + + + @NotNull + @Override + public String executeGet(@NotNull String path) throws IOException { + sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); + return getBigResponse().getFilename(); + } + private byte[] getSmallResponse() throws IOException { + SmallReadableMessage message = new SmallReadableMessage(channel); + getResponse(message); + return message.getData(); + } + + private BigReadableMessage getBigResponse() throws IOException { + BigReadableMessage message = new BigReadableMessage(channel); + getResponse(message); + return message; + } + + private void getResponse(ReadableMessage message) throws IOException { + //noinspection StatementWithEmptyBody + while (!message.read()); + } + + private void sendRequest(byte[] data) throws IOException { + //noinspection StatementWithEmptyBody + if (!channel.isConnected()) { + throw new NotYetConnectedException(); + } + WritableMessage message = new SmallWritableMessage(channel, data); + //noinspection StatementWithEmptyBody + while (!message.write()); + } + + private byte[] createSentData(byte first, byte[] rest) { + return ArrayUtils.addAll(ByteUtils.byteToBytes(first), rest); + } + +} diff --git a/src/main/java/client/package-info.java b/src/main/java/client/package-info.java new file mode 100644 index 0000000..dfaa115 --- /dev/null +++ b/src/main/java/client/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides client console app and interface for FTP clients. + */ + +package client; \ No newline at end of file diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index 48b53e2..ef01b0c 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -1,6 +1,6 @@ package server; -import client.Client.Query; +import client.NonBlockingClient.Query; import org.jetbrains.annotations.NotNull; import utils.GetResponse; import utils.ListResponse; diff --git a/src/test/java/IntegrationTest.java b/src/test/java/IntegrationTest.java index 724f8ae..e70796a 100644 --- a/src/test/java/IntegrationTest.java +++ b/src/test/java/IntegrationTest.java @@ -1,23 +1,18 @@ import client.Client; -import org.apache.commons.lang3.ArrayUtils; import org.junit.Test; import server.FileSystem; import server.Server; -import utils.ByteUtils; -import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; - -import static org.junit.Assert.*; public class IntegrationTest { @Test public void processGet() throws Exception { Server server = new Server(Paths.get(".")); FileSystem fileSystem = new FileSystem(Paths.get(".")); - //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) Client.Query.GET.ordinal()), + Client client = Client.getNonBlocking(); + //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) NonBlockingClient.Query.GET.ordinal()), } } \ No newline at end of file diff --git a/src/test/java/server/QueryProcessorTest.java b/src/test/java/server/QueryProcessorTest.java index 3e0fc35..f634290 100644 --- a/src/test/java/server/QueryProcessorTest.java +++ b/src/test/java/server/QueryProcessorTest.java @@ -6,7 +6,7 @@ public class QueryProcessorTest { @Test public void processGet() throws Exception { - //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) Client.Query.GET.ordinal()), + //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) NonBlockingClient.Query.GET.ordinal()), } } \ No newline at end of file From 8fd9614d635c02f75f8dfbe9ce74dbea6adef295 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 19:56:34 +0300 Subject: [PATCH 14/24] Fixed compilation errors --- src/main/java/client/NonBlockingClient.java | 3 ++- src/main/java/server/QueryProcessor.java | 4 ++-- src/main/java/utils/BigReadableMessage.java | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/client/NonBlockingClient.java b/src/main/java/client/NonBlockingClient.java index 8c013fd..875e49b 100644 --- a/src/main/java/client/NonBlockingClient.java +++ b/src/main/java/client/NonBlockingClient.java @@ -10,6 +10,7 @@ import java.net.InetSocketAddress; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SocketChannel; +import java.nio.file.Path; import java.util.List; /** @@ -59,7 +60,7 @@ public List executeList(@NotNull String path) throws IOException { @NotNull @Override - public String executeGet(@NotNull String path) throws IOException { + public Path executeGet(@NotNull String path) throws IOException { sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); return getBigResponse().getFilename(); } diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index ef01b0c..d6afa47 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -1,6 +1,6 @@ package server; -import client.NonBlockingClient.Query; +import client.Client; import org.jetbrains.annotations.NotNull; import utils.GetResponse; import utils.ListResponse; @@ -16,7 +16,7 @@ class QueryProcessor { Response process(@NotNull byte[] data, FileSystem fileSystem) throws IOException { - Query queryType = Query.values()[data[0]]; + Client.Query queryType = Client.Query.values()[data[0]]; String path = new String(data, 1, data.length - 1, StandardCharsets.UTF_8); switch (queryType) { diff --git a/src/main/java/utils/BigReadableMessage.java b/src/main/java/utils/BigReadableMessage.java index 95d6d06..83f1901 100644 --- a/src/main/java/utils/BigReadableMessage.java +++ b/src/main/java/utils/BigReadableMessage.java @@ -5,6 +5,8 @@ import java.io.IOException; import java.nio.channels.SocketChannel; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; public class BigReadableMessage extends ReadableMessage { @NotNull @@ -17,7 +19,7 @@ public BigReadableMessage(@NotNull SocketChannel channel) throws IOException { @NotNull - public String getFilename() { - return filename; + public Path getFilename() { + return Paths.get(filename); } } From ad1348a0767f0094aeedee6c3a692d14fb4e274e Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 27 Apr 2017 23:54:45 +0300 Subject: [PATCH 15/24] JavaDocs for server package added --- src/main/java/client/Client.java | 2 +- src/main/java/client/NonBlockingClient.java | 2 +- src/main/java/server/FTPServer.java | 29 +++++++++++++++++ src/main/java/server/FileSystem.java | 31 +++++++++++++++---- .../{Server.java => NonBlockingServer.java} | 6 ++-- src/main/java/server/ServerConsoleApp.java | 16 ++++++++-- src/main/java/server/package-info.java | 5 +++ src/test/java/IntegrationTest.java | 4 +-- 8 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 src/main/java/server/FTPServer.java rename src/main/java/server/{Server.java => NonBlockingServer.java} (96%) create mode 100644 src/main/java/server/package-info.java diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java index c989a1f..8218709 100644 --- a/src/main/java/client/Client.java +++ b/src/main/java/client/Client.java @@ -8,7 +8,7 @@ import java.util.List; /** - * Interface of a simple FTP server. + * Interface of a simple FTP client. */ public interface Client { diff --git a/src/main/java/client/NonBlockingClient.java b/src/main/java/client/NonBlockingClient.java index 875e49b..83a01ec 100644 --- a/src/main/java/client/NonBlockingClient.java +++ b/src/main/java/client/NonBlockingClient.java @@ -53,7 +53,7 @@ public List executeList(@NotNull String path) throws IOException { return (List) objectStream.readObject(); } catch (ClassNotFoundException e) { // TODO: probably change to custom exception - throw new RuntimeException("server.Server fucked up!"); + throw new RuntimeException("server.NonBlockingServer fucked up!"); } } diff --git a/src/main/java/server/FTPServer.java b/src/main/java/server/FTPServer.java new file mode 100644 index 0000000..79f3091 --- /dev/null +++ b/src/main/java/server/FTPServer.java @@ -0,0 +1,29 @@ +package server; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Interface of a simple FTP server. + */ + +public interface FTPServer { + /** + * Launches server. + * + * After launch it will loop indefinitely until its thread will be interrupted. + * @param hostname the hostname to listen to + * @param port the port to listen to + * @throws IOException If an I/O error occurs + */ + void run(String hostname, int port) throws IOException; + + /** + * Creates non-blocking FTP server. + * @param root path to root directory for server + * @return A new FTP server + */ + static FTPServer getNonBlocking(Path root) { + return new NonBlockingServer(root); + } +} diff --git a/src/main/java/server/FileSystem.java b/src/main/java/server/FileSystem.java index a60662b..27b3289 100644 --- a/src/main/java/server/FileSystem.java +++ b/src/main/java/server/FileSystem.java @@ -8,21 +8,45 @@ import java.util.ArrayList; import java.util.stream.Collectors; - +/** + * Class that provides means to interact with file system. + */ public class FileSystem { private final Path root; + /** + * Creates file system manager with specified root. + * @param root path to desired root directory + */ @SuppressWarnings("WeakerAccess") public FileSystem(Path root) { this.root = root; } + /** + * Returns list of entries in specified directory. + * @param path path to directory + * @return list of entries in specified directory + * @throws IOException If an I/O error occurs + */ @SuppressWarnings("WeakerAccess") public ArrayList list(Path path) throws IOException { assertChild(path, root); return new ArrayList<>(Files.list(path).map(p -> new SimpleFile(p.toString(), Files.isDirectory(p))).collect(Collectors.toList())); } + /** + * Returns {@link InputStream} to read from the file. + * @param path path to the file + * @return a new input stream + * @throws IOException If an I/O error occurs + */ + @SuppressWarnings("WeakerAccess") + public InputStream getOutputStream(Path path) throws IOException { + assertChild(path, root); + return Files.newInputStream(path); + } + private static void assertChild(Path path, Path root) { if (!isChild(path, root)) { throw new SecurityException("Trying to go out of set root"); @@ -34,9 +58,4 @@ private static boolean isChild(Path path, Path root) { return path.toAbsolutePath().startsWith(root.toAbsolutePath()); } - @SuppressWarnings("WeakerAccess") - public InputStream getOutputStream(Path path) throws IOException { - assertChild(path, root); - return Files.newInputStream(path); - } } \ No newline at end of file diff --git a/src/main/java/server/Server.java b/src/main/java/server/NonBlockingServer.java similarity index 96% rename from src/main/java/server/Server.java rename to src/main/java/server/NonBlockingServer.java index 9db4267..082dae2 100644 --- a/src/main/java/server/Server.java +++ b/src/main/java/server/NonBlockingServer.java @@ -12,14 +12,14 @@ import java.nio.file.Path; import java.util.Iterator; -public class Server { +class NonBlockingServer implements FTPServer { private final Path root; - public Server(Path root) { + NonBlockingServer(Path root) { this.root = root; } - @SuppressWarnings("WeakerAccess") + @Override public void run(String hostname, int port) throws IOException { FileSystem fileSystem = new FileSystem(root); System.out.println("run"); diff --git a/src/main/java/server/ServerConsoleApp.java b/src/main/java/server/ServerConsoleApp.java index 7a8332f..03147de 100644 --- a/src/main/java/server/ServerConsoleApp.java +++ b/src/main/java/server/ServerConsoleApp.java @@ -7,7 +7,17 @@ import java.nio.file.Paths; import java.util.Scanner; +/** + * Console application providing start/stop methods for FTP server. + */ + public class ServerConsoleApp { + /** + * Main method. + * @param args command line args + * @throws IOException If an I/O error occurs + */ + public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); @@ -22,11 +32,11 @@ public static void main(String[] args) throws IOException { String hostname = scanner.next(); int port = scanner.nextInt(); serverThread = new Thread(() -> { - Server server = new Server(Paths.get(".")); + FTPServer server = FTPServer.getNonBlocking(Paths.get(".")); try { server.run(hostname, port); } catch (IOException e) { - System.err.println("Server run failed: " + e.getMessage()); + System.err.println("NonBlockingServer run failed: " + e.getMessage()); } }); serverThread.start(); @@ -34,7 +44,7 @@ public static void main(String[] args) throws IOException { } case "stop": { if (serverThread == null) { - System.err.println("Server not started!"); + System.err.println("NonBlockingServer not started!"); } else { serverThread.interrupt(); diff --git a/src/main/java/server/package-info.java b/src/main/java/server/package-info.java new file mode 100644 index 0000000..bdff820 --- /dev/null +++ b/src/main/java/server/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides server console app and interface for FTP servers. + */ + +package server; \ No newline at end of file diff --git a/src/test/java/IntegrationTest.java b/src/test/java/IntegrationTest.java index e70796a..714fc58 100644 --- a/src/test/java/IntegrationTest.java +++ b/src/test/java/IntegrationTest.java @@ -1,15 +1,15 @@ import client.Client; import org.junit.Test; +import server.FTPServer; import server.FileSystem; -import server.Server; import java.nio.file.Paths; public class IntegrationTest { @Test public void processGet() throws Exception { - Server server = new Server(Paths.get(".")); + FTPServer server = FTPServer.getNonBlocking(Paths.get(".")); FileSystem fileSystem = new FileSystem(Paths.get(".")); Client client = Client.getNonBlocking(); //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) NonBlockingClient.Query.GET.ordinal()), From 92e4f56ac146486e72c2be5e4364164db807f1b9 Mon Sep 17 00:00:00 2001 From: Ilya Date: Fri, 28 Apr 2017 02:47:43 +0300 Subject: [PATCH 16/24] JavaDocs for utils package added --- src/main/java/client/NonBlockingClient.java | 2 +- src/main/java/utils/BigReadableMessage.java | 21 ++++++++++++++----- src/main/java/utils/ByteUtils.java | 14 +++++++++++++ src/main/java/utils/GetResponse.java | 8 +++++++ src/main/java/utils/ListResponse.java | 9 +++++++- src/main/java/utils/ReadableMessage.java | 12 +++++++++++ src/main/java/utils/Response.java | 8 +++++++ src/main/java/utils/SimpleFile.java | 14 +++++++++++++ src/main/java/utils/SmallReadableMessage.java | 11 ++++++++++ src/main/java/utils/SmallWritableMessage.java | 7 +++++++ src/main/java/utils/WritableMessage.java | 5 ++++- src/main/java/utils/package-info.java | 4 ++++ 12 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 src/main/java/utils/package-info.java diff --git a/src/main/java/client/NonBlockingClient.java b/src/main/java/client/NonBlockingClient.java index 83a01ec..bad321d 100644 --- a/src/main/java/client/NonBlockingClient.java +++ b/src/main/java/client/NonBlockingClient.java @@ -62,7 +62,7 @@ public List executeList(@NotNull String path) throws IOException { @Override public Path executeGet(@NotNull String path) throws IOException { sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); - return getBigResponse().getFilename(); + return getBigResponse().getPath(); } private byte[] getSmallResponse() throws IOException { SmallReadableMessage message = new SmallReadableMessage(channel); diff --git a/src/main/java/utils/BigReadableMessage.java b/src/main/java/utils/BigReadableMessage.java index 83f1901..d521d0f 100644 --- a/src/main/java/utils/BigReadableMessage.java +++ b/src/main/java/utils/BigReadableMessage.java @@ -8,18 +8,29 @@ import java.nio.file.Path; import java.nio.file.Paths; +/** + * Implementation of {@link ReadableMessage} suitable for reading big messages. + */ public class BigReadableMessage extends ReadableMessage { @NotNull - private String filename; + private String path; + /** + * Creates new message that reads its content from provided channel. + * @param channel channel to read data from + * @throws IOException If an I/O error occurs + */ public BigReadableMessage(@NotNull SocketChannel channel) throws IOException { super(channel, new KnownFileOutputStream(Files.createTempFile("dwnl", "").toFile())); - filename = ((KnownFileOutputStream) getDestination()).getFile().getAbsolutePath(); + path = ((KnownFileOutputStream) getDestination()).getFile().getAbsolutePath(); } - + /** + * Returns path to downloaded file. + * @return path to downloaded file + */ @NotNull - public Path getFilename() { - return Paths.get(filename); + public Path getPath() { + return Paths.get(path); } } diff --git a/src/main/java/utils/ByteUtils.java b/src/main/java/utils/ByteUtils.java index 9bc89c5..79f91ba 100644 --- a/src/main/java/utils/ByteUtils.java +++ b/src/main/java/utils/ByteUtils.java @@ -2,16 +2,30 @@ import java.nio.ByteBuffer; +/** + * Class that provides utility functions for work with byte arrays. + */ public class ByteUtils { private static final ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES); private static final ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES); + /** + * Converts long to byte array. + * @param x number to convert + * @return byte representation of given number + */ @SuppressWarnings("WeakerAccess") public static byte[] longToBytes(long x) { longBuffer.putLong(0, x); longBuffer.clear(); return longBuffer.array().clone(); } + + /** + * Converts byte to byte array. + * @param x byte to convert + * @return byte array consisting of single given byte + */ public static byte[] byteToBytes(byte x) { byteBuffer.put(x); byteBuffer.clear(); diff --git a/src/main/java/utils/GetResponse.java b/src/main/java/utils/GetResponse.java index 8d9438e..e9faa58 100644 --- a/src/main/java/utils/GetResponse.java +++ b/src/main/java/utils/GetResponse.java @@ -6,13 +6,21 @@ import java.io.InputStream; import java.nio.channels.SocketChannel; +/** + * Class encapsulating response to Query.GET query. + */ public class GetResponse implements Response { private final InputStream source; + /** + * Creates new response encapsulating given data. + * @param source {@link InputStream} to read response data from + */ public GetResponse(@NotNull InputStream source) { this.source = source; } + @Override public WritableMessage generateMessage(@NotNull SocketChannel channel) throws IOException { return new BigWritableMessage(channel, source); diff --git a/src/main/java/utils/ListResponse.java b/src/main/java/utils/ListResponse.java index 611f5b0..21af864 100644 --- a/src/main/java/utils/ListResponse.java +++ b/src/main/java/utils/ListResponse.java @@ -5,15 +5,22 @@ import java.io.FileNotFoundException; import java.nio.channels.SocketChannel; +/** + * Class encapsulating response to Query.LIST query. + */ public class ListResponse implements Response { @NotNull private byte[] data; + /** + * Creates new response encapsulating given data. + * @param data response data + */ public ListResponse(@NotNull byte[] data) { this.data = data; } @Override - public WritableMessage generateMessage(SocketChannel channel) throws FileNotFoundException { + public WritableMessage generateMessage(SocketChannel channel) { return new SmallWritableMessage(channel, data); } } diff --git a/src/main/java/utils/ReadableMessage.java b/src/main/java/utils/ReadableMessage.java index 154e462..fa5edd8 100644 --- a/src/main/java/utils/ReadableMessage.java +++ b/src/main/java/utils/ReadableMessage.java @@ -7,6 +7,9 @@ import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; +/** + * Object that can read message data from specified channel. + */ public class ReadableMessage { private static final int BUFFER_SIZE = 4096; private final OutputStream destination; @@ -22,10 +25,19 @@ public class ReadableMessage { buffer = ByteBuffer.allocate(BUFFER_SIZE); } + /** + * Returns associated channel. + * @return channel from which data is being read + */ public SocketChannel getChannel() { return channel; } + /** + * Calls read() on associated channel + * @return whether there is data left to read + * @throws IOException If an I/O error occurs. + */ public boolean read() throws IOException { if (read < 4) { read += channel.read(buffer); diff --git a/src/main/java/utils/Response.java b/src/main/java/utils/Response.java index dcf13c7..98b5f0d 100644 --- a/src/main/java/utils/Response.java +++ b/src/main/java/utils/Response.java @@ -3,6 +3,14 @@ import java.io.IOException; import java.nio.channels.SocketChannel; +/** + * Response to client query based on which message could be generated. + */ public interface Response { + /** + * Returns {@link WritableMessage} based on this response + * @param channel channel to associate message with + * @return message with this response + */ WritableMessage generateMessage(SocketChannel channel) throws IOException; } diff --git a/src/main/java/utils/SimpleFile.java b/src/main/java/utils/SimpleFile.java index bd5dde8..4f8caca 100644 --- a/src/main/java/utils/SimpleFile.java +++ b/src/main/java/utils/SimpleFile.java @@ -2,10 +2,24 @@ import java.io.Serializable; +/** + * Object containing pair (name, isDirectory) for file. + */ public class SimpleFile implements Serializable{ + /** + * Filename + */ public final String name; + /** + * Whether this file is a directory + */ public final boolean isDirectory; + /** + * Creates new file info with given data. + * @param name filename + * @param isDirectory whether given file is a directory + */ public SimpleFile(String name, boolean isDirectory) { this.name = name; this.isDirectory = isDirectory; diff --git a/src/main/java/utils/SmallReadableMessage.java b/src/main/java/utils/SmallReadableMessage.java index d467a95..650019a 100644 --- a/src/main/java/utils/SmallReadableMessage.java +++ b/src/main/java/utils/SmallReadableMessage.java @@ -5,11 +5,22 @@ import java.io.ByteArrayOutputStream; import java.nio.channels.SocketChannel; +/** + * Implementation of {@link ReadableMessage} suitable for reading small messages. + */ public class SmallReadableMessage extends ReadableMessage { + /** + * Creates new message that reads its content from provided channel. + * @param channel channel to read data from + */ public SmallReadableMessage(@NotNull SocketChannel channel) { super(channel, new ByteArrayOutputStream()); } + /** + * Return read data. + * @return data byte array + */ @NotNull public byte[] getData() { return ((ByteArrayOutputStream) getDestination()).toByteArray(); diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java index 00686ce..b62e3fe 100644 --- a/src/main/java/utils/SmallWritableMessage.java +++ b/src/main/java/utils/SmallWritableMessage.java @@ -7,7 +7,14 @@ import java.io.ByteArrayInputStream; import java.nio.channels.SocketChannel; +/** + * Implementation of {@link WritableMessage} suitable for writing small messages. + */ public class SmallWritableMessage extends WritableMessage { + /** + * Creates new message that writes its content to provided channel. + * @param channel channel to write data to + */ public SmallWritableMessage(@NotNull SocketChannel channel, @NotNull byte[] data) { super(channel, new ByteArrayInputStream(addHeader(data)), getSize(data)); System.out.println("SmallMessage of " + data.length + " bytes"); diff --git a/src/main/java/utils/WritableMessage.java b/src/main/java/utils/WritableMessage.java index e4bf5b9..820602d 100644 --- a/src/main/java/utils/WritableMessage.java +++ b/src/main/java/utils/WritableMessage.java @@ -8,6 +8,9 @@ import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; +/** + * Object that can write message data to specified channel. + */ public class WritableMessage { private static final int BUFFER_SIZE = 4096; private final ByteBuffer buffer; @@ -24,7 +27,7 @@ public class WritableMessage { } /** - * Calls write() on underlying channel with remaining data. + * Calls write() on associated channel with remaining data. * @return whether it has completed message writing */ public boolean write() throws IOException { diff --git a/src/main/java/utils/package-info.java b/src/main/java/utils/package-info.java new file mode 100644 index 0000000..9e0ba5a --- /dev/null +++ b/src/main/java/utils/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides utility classes for client/server communication and internal needs. + */ +package utils; \ No newline at end of file From d057332716fc0177cd9215c4e8f07ead911d8db2 Mon Sep 17 00:00:00 2001 From: Ilya Date: Fri, 28 Apr 2017 02:59:08 +0300 Subject: [PATCH 17/24] Added missing @NotNull/@Nullable --- src/main/java/client/NonBlockingClient.java | 12 ++++++++---- src/main/java/server/FTPServer.java | 7 +++++-- src/main/java/server/FileSystem.java | 14 +++++++++----- src/main/java/server/NonBlockingServer.java | 3 ++- src/main/java/server/QueryProcessor.java | 2 +- src/main/java/utils/BigWritableMessage.java | 5 +++-- src/main/java/utils/ByteUtils.java | 4 ++++ src/main/java/utils/KnownFileOutputStream.java | 6 +++++- src/main/java/utils/ListResponse.java | 3 +-- src/main/java/utils/ReadableMessage.java | 7 ++++++- src/main/java/utils/Response.java | 4 +++- src/main/java/utils/SimpleFile.java | 5 ++++- src/main/java/utils/SmallWritableMessage.java | 5 +++-- src/main/java/utils/WritableMessage.java | 3 +++ 14 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/main/java/client/NonBlockingClient.java b/src/main/java/client/NonBlockingClient.java index bad321d..b55bc73 100644 --- a/src/main/java/client/NonBlockingClient.java +++ b/src/main/java/client/NonBlockingClient.java @@ -17,7 +17,7 @@ * Class that implements non-blocking FTP client. */ -public class NonBlockingClient implements Client{ +class NonBlockingClient implements Client{ private final SocketChannel channel; @@ -64,24 +64,27 @@ public Path executeGet(@NotNull String path) throws IOException { sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); return getBigResponse().getPath(); } + + @NotNull private byte[] getSmallResponse() throws IOException { SmallReadableMessage message = new SmallReadableMessage(channel); getResponse(message); return message.getData(); } + @NotNull private BigReadableMessage getBigResponse() throws IOException { BigReadableMessage message = new BigReadableMessage(channel); getResponse(message); return message; } - private void getResponse(ReadableMessage message) throws IOException { + private void getResponse(@NotNull ReadableMessage message) throws IOException { //noinspection StatementWithEmptyBody while (!message.read()); } - private void sendRequest(byte[] data) throws IOException { + private void sendRequest(@NotNull byte[] data) throws IOException { //noinspection StatementWithEmptyBody if (!channel.isConnected()) { throw new NotYetConnectedException(); @@ -91,7 +94,8 @@ private void sendRequest(byte[] data) throws IOException { while (!message.write()); } - private byte[] createSentData(byte first, byte[] rest) { + @NotNull + private byte[] createSentData(byte first, @NotNull byte[] rest) { return ArrayUtils.addAll(ByteUtils.byteToBytes(first), rest); } diff --git a/src/main/java/server/FTPServer.java b/src/main/java/server/FTPServer.java index 79f3091..1c799d0 100644 --- a/src/main/java/server/FTPServer.java +++ b/src/main/java/server/FTPServer.java @@ -1,5 +1,7 @@ package server; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.nio.file.Path; @@ -16,14 +18,15 @@ public interface FTPServer { * @param port the port to listen to * @throws IOException If an I/O error occurs */ - void run(String hostname, int port) throws IOException; + void run(@NotNull String hostname, int port) throws IOException; /** * Creates non-blocking FTP server. * @param root path to root directory for server * @return A new FTP server */ - static FTPServer getNonBlocking(Path root) { + @NotNull + static FTPServer getNonBlocking(@NotNull Path root) { return new NonBlockingServer(root); } } diff --git a/src/main/java/server/FileSystem.java b/src/main/java/server/FileSystem.java index 27b3289..4cde354 100644 --- a/src/main/java/server/FileSystem.java +++ b/src/main/java/server/FileSystem.java @@ -1,5 +1,6 @@ package server; +import org.jetbrains.annotations.NotNull; import utils.SimpleFile; import java.io.*; @@ -12,6 +13,7 @@ * Class that provides means to interact with file system. */ public class FileSystem { + @NotNull private final Path root; /** @@ -19,7 +21,7 @@ public class FileSystem { * @param root path to desired root directory */ @SuppressWarnings("WeakerAccess") - public FileSystem(Path root) { + public FileSystem(@NotNull Path root) { this.root = root; } @@ -30,7 +32,8 @@ public FileSystem(Path root) { * @throws IOException If an I/O error occurs */ @SuppressWarnings("WeakerAccess") - public ArrayList list(Path path) throws IOException { + @NotNull + public ArrayList list(@NotNull Path path) throws IOException { assertChild(path, root); return new ArrayList<>(Files.list(path).map(p -> new SimpleFile(p.toString(), Files.isDirectory(p))).collect(Collectors.toList())); } @@ -42,19 +45,20 @@ public ArrayList list(Path path) throws IOException { * @throws IOException If an I/O error occurs */ @SuppressWarnings("WeakerAccess") - public InputStream getOutputStream(Path path) throws IOException { + @NotNull + public InputStream getOutputStream(@NotNull Path path) throws IOException { assertChild(path, root); return Files.newInputStream(path); } - private static void assertChild(Path path, Path root) { + private static void assertChild(@NotNull Path path, @NotNull Path root) { if (!isChild(path, root)) { throw new SecurityException("Trying to go out of set root"); } } - private static boolean isChild(Path path, Path root) { + private static boolean isChild(@NotNull Path path, @NotNull Path root) { return path.toAbsolutePath().startsWith(root.toAbsolutePath()); } diff --git a/src/main/java/server/NonBlockingServer.java b/src/main/java/server/NonBlockingServer.java index 082dae2..56f8297 100644 --- a/src/main/java/server/NonBlockingServer.java +++ b/src/main/java/server/NonBlockingServer.java @@ -1,5 +1,6 @@ package server; +import org.jetbrains.annotations.NotNull; import utils.SmallReadableMessage; import utils.WritableMessage; @@ -20,7 +21,7 @@ class NonBlockingServer implements FTPServer { } @Override - public void run(String hostname, int port) throws IOException { + public void run(@NotNull String hostname, int port) throws IOException { FileSystem fileSystem = new FileSystem(root); System.out.println("run"); Selector selector = Selector.open(); diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java index d6afa47..d58aef4 100644 --- a/src/main/java/server/QueryProcessor.java +++ b/src/main/java/server/QueryProcessor.java @@ -15,7 +15,7 @@ import java.util.ArrayList; class QueryProcessor { - Response process(@NotNull byte[] data, FileSystem fileSystem) throws IOException { + Response process(@NotNull byte[] data, @NotNull FileSystem fileSystem) throws IOException { Client.Query queryType = Client.Query.values()[data[0]]; String path = new String(data, 1, data.length - 1, StandardCharsets.UTF_8); diff --git a/src/main/java/utils/BigWritableMessage.java b/src/main/java/utils/BigWritableMessage.java index 42188f8..96cfc9c 100644 --- a/src/main/java/utils/BigWritableMessage.java +++ b/src/main/java/utils/BigWritableMessage.java @@ -14,12 +14,13 @@ class BigWritableMessage extends WritableMessage { super(channel, createMessageInputStream(source), getSize(source)); } - private static InputStream createMessageInputStream(InputStream source) throws IOException { + @NotNull + private static InputStream createMessageInputStream(@NotNull InputStream source) throws IOException { InputStream stream = new ByteArrayInputStream(ByteUtils.longToBytes(getSize(source))); return new SequenceInputStream(stream, source); } - private static long getSize(InputStream source) throws IOException { + private static long getSize(@NotNull InputStream source) throws IOException { return Long.BYTES + source.available(); } } diff --git a/src/main/java/utils/ByteUtils.java b/src/main/java/utils/ByteUtils.java index 79f91ba..edbc3fd 100644 --- a/src/main/java/utils/ByteUtils.java +++ b/src/main/java/utils/ByteUtils.java @@ -1,5 +1,7 @@ package utils; +import org.jetbrains.annotations.NotNull; + import java.nio.ByteBuffer; /** @@ -15,6 +17,7 @@ public class ByteUtils { * @return byte representation of given number */ @SuppressWarnings("WeakerAccess") + @NotNull public static byte[] longToBytes(long x) { longBuffer.putLong(0, x); longBuffer.clear(); @@ -26,6 +29,7 @@ public static byte[] longToBytes(long x) { * @param x byte to convert * @return byte array consisting of single given byte */ + @NotNull public static byte[] byteToBytes(byte x) { byteBuffer.put(x); byteBuffer.clear(); diff --git a/src/main/java/utils/KnownFileOutputStream.java b/src/main/java/utils/KnownFileOutputStream.java index 9e5c328..7342f13 100644 --- a/src/main/java/utils/KnownFileOutputStream.java +++ b/src/main/java/utils/KnownFileOutputStream.java @@ -1,16 +1,20 @@ package utils; +import org.jetbrains.annotations.NotNull; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; class KnownFileOutputStream extends FileOutputStream { + @NotNull private final File file; - KnownFileOutputStream(File file) throws FileNotFoundException { + KnownFileOutputStream(@NotNull File file) throws FileNotFoundException { super(file); this.file = file; } + @NotNull File getFile() { return file; } diff --git a/src/main/java/utils/ListResponse.java b/src/main/java/utils/ListResponse.java index 21af864..fdb2615 100644 --- a/src/main/java/utils/ListResponse.java +++ b/src/main/java/utils/ListResponse.java @@ -2,7 +2,6 @@ import org.jetbrains.annotations.NotNull; -import java.io.FileNotFoundException; import java.nio.channels.SocketChannel; /** @@ -20,7 +19,7 @@ public ListResponse(@NotNull byte[] data) { } @Override - public WritableMessage generateMessage(SocketChannel channel) { + public WritableMessage generateMessage(@NotNull SocketChannel channel) { return new SmallWritableMessage(channel, data); } } diff --git a/src/main/java/utils/ReadableMessage.java b/src/main/java/utils/ReadableMessage.java index fa5edd8..343304c 100644 --- a/src/main/java/utils/ReadableMessage.java +++ b/src/main/java/utils/ReadableMessage.java @@ -12,13 +12,16 @@ */ public class ReadableMessage { private static final int BUFFER_SIZE = 4096; + @NotNull private final OutputStream destination; + @NotNull private SocketChannel channel; private long size = 0; private int read = 0; + @NotNull private ByteBuffer buffer; - ReadableMessage(@NotNull SocketChannel channel, OutputStream destination) { + ReadableMessage(@NotNull SocketChannel channel, @NotNull OutputStream destination) { this.channel = channel; // TODO: add buffered this.destination = destination; @@ -29,6 +32,7 @@ public class ReadableMessage { * Returns associated channel. * @return channel from which data is being read */ + @NotNull public SocketChannel getChannel() { return channel; } @@ -60,6 +64,7 @@ public boolean read() throws IOException { return read == size && !buffer.hasRemaining(); } + @NotNull OutputStream getDestination() { return destination; } diff --git a/src/main/java/utils/Response.java b/src/main/java/utils/Response.java index 98b5f0d..2136f44 100644 --- a/src/main/java/utils/Response.java +++ b/src/main/java/utils/Response.java @@ -1,5 +1,7 @@ package utils; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.nio.channels.SocketChannel; @@ -12,5 +14,5 @@ public interface Response { * @param channel channel to associate message with * @return message with this response */ - WritableMessage generateMessage(SocketChannel channel) throws IOException; + WritableMessage generateMessage(@NotNull SocketChannel channel) throws IOException; } diff --git a/src/main/java/utils/SimpleFile.java b/src/main/java/utils/SimpleFile.java index 4f8caca..2c2a6f9 100644 --- a/src/main/java/utils/SimpleFile.java +++ b/src/main/java/utils/SimpleFile.java @@ -1,5 +1,7 @@ package utils; +import org.jetbrains.annotations.NotNull; + import java.io.Serializable; /** @@ -9,6 +11,7 @@ public class SimpleFile implements Serializable{ /** * Filename */ + @NotNull public final String name; /** * Whether this file is a directory @@ -20,7 +23,7 @@ public class SimpleFile implements Serializable{ * @param name filename * @param isDirectory whether given file is a directory */ - public SimpleFile(String name, boolean isDirectory) { + public SimpleFile(@NotNull String name, boolean isDirectory) { this.name = name; this.isDirectory = isDirectory; } diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java index b62e3fe..92ba519 100644 --- a/src/main/java/utils/SmallWritableMessage.java +++ b/src/main/java/utils/SmallWritableMessage.java @@ -20,11 +20,12 @@ public SmallWritableMessage(@NotNull SocketChannel channel, @NotNull byte[] data System.out.println("SmallMessage of " + data.length + " bytes"); } - private static long getSize(byte[] data) { + private static long getSize(@NotNull byte[] data) { return Long.BYTES + data.length; } - private static byte[] addHeader(byte[] data) { + @NotNull + private static byte[] addHeader(@NotNull byte[] data) { return ArrayUtils.addAll(ByteUtils.longToBytes(getSize(data)), data); } } diff --git a/src/main/java/utils/WritableMessage.java b/src/main/java/utils/WritableMessage.java index 820602d..629fd51 100644 --- a/src/main/java/utils/WritableMessage.java +++ b/src/main/java/utils/WritableMessage.java @@ -13,9 +13,12 @@ */ public class WritableMessage { private static final int BUFFER_SIZE = 4096; + @NotNull private final ByteBuffer buffer; private long leftSource; + @NotNull private SocketChannel channel; + @NotNull private InputStream source; WritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source, long size) { this.leftSource = size; From aa6ba719ad6a49fb2583b653f33422dcea63ce79 Mon Sep 17 00:00:00 2001 From: Ilya Date: Sat, 29 Apr 2017 23:34:25 +0300 Subject: [PATCH 18/24] Added JAR stuff --- build.gradle | 22 ++++++++++++++++++++ src/test/java/server/QueryProcessorTest.java | 12 ----------- 2 files changed, 22 insertions(+), 12 deletions(-) delete mode 100644 src/test/java/server/QueryProcessorTest.java diff --git a/build.gradle b/build.gradle index 57d0b70..8dcc7e5 100644 --- a/build.gradle +++ b/build.gradle @@ -14,3 +14,25 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0' } + +task serverJar(type: Jar) { + manifest { + attributes 'Implementation-Title': 'Ftp server jar file', + 'Implementation-Version': version, + 'Main-Class': 'ru.spbau.zhidkov.server.MainServer' + } + baseName = 'ftp-server' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} + +task clientJar(type: Jar) { + manifest { + attributes 'Implementation-Title': 'Ftp client jar file', + 'Implementation-Version': version, + 'Main-Class': 'ru.spbau.zhidkov.client.MainClient' + } + baseName = 'ftp-client' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} \ No newline at end of file diff --git a/src/test/java/server/QueryProcessorTest.java b/src/test/java/server/QueryProcessorTest.java deleted file mode 100644 index f634290..0000000 --- a/src/test/java/server/QueryProcessorTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package server; - -import org.junit.Test; - -public class QueryProcessorTest { - @Test - public void processGet() throws Exception { - - //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) NonBlockingClient.Query.GET.ordinal()), - } - -} \ No newline at end of file From 60915ab68c6ec85482cdefd15b295617ff4a9d7a Mon Sep 17 00:00:00 2001 From: Ilya Date: Sat, 29 Apr 2017 23:34:43 +0300 Subject: [PATCH 19/24] Fixed some bugs and added test --- src/main/java/client/NonBlockingClient.java | 6 +- src/main/java/server/FTPServer.java | 6 ++ src/main/java/server/NonBlockingServer.java | 11 ++- src/main/java/utils/ByteUtils.java | 2 +- src/main/java/utils/ReadableMessage.java | 4 +- src/main/java/utils/SmallWritableMessage.java | 1 - src/test/java/IntegrationTest.java | 97 +++++++++++++++++-- 7 files changed, 112 insertions(+), 15 deletions(-) diff --git a/src/main/java/client/NonBlockingClient.java b/src/main/java/client/NonBlockingClient.java index b55bc73..1e4655b 100644 --- a/src/main/java/client/NonBlockingClient.java +++ b/src/main/java/client/NonBlockingClient.java @@ -10,6 +10,7 @@ import java.net.InetSocketAddress; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.List; @@ -28,7 +29,6 @@ class NonBlockingClient implements Client{ @Override public void connect(@NotNull String hostname, int port) throws IOException { - System.out.println("connecting to " + hostname + ":" + port); if (channel.isConnected()) return; channel.connect(new InetSocketAddress(hostname, port)); @@ -45,7 +45,7 @@ public void disconnect() throws IOException { @NotNull @Override public List executeList(@NotNull String path) throws IOException { - sendRequest(createSentData((byte) Query.LIST.ordinal(), path.getBytes())); + sendRequest(createSentData((byte) Query.LIST.ordinal(), path.getBytes(StandardCharsets.UTF_8))); byte[] response = getSmallResponse(); ObjectInputStream objectStream = new ObjectInputStream(new ByteArrayInputStream(response)); try { @@ -61,7 +61,7 @@ public List executeList(@NotNull String path) throws IOException { @NotNull @Override public Path executeGet(@NotNull String path) throws IOException { - sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes())); + sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes(StandardCharsets.UTF_8))); return getBigResponse().getPath(); } diff --git a/src/main/java/server/FTPServer.java b/src/main/java/server/FTPServer.java index 1c799d0..b3cc902 100644 --- a/src/main/java/server/FTPServer.java +++ b/src/main/java/server/FTPServer.java @@ -29,4 +29,10 @@ public interface FTPServer { static FTPServer getNonBlocking(@NotNull Path root) { return new NonBlockingServer(root); } + + /** + * Returns whether server is ready to accept connections. + * @return true if server is ready to accept incoming connections and false otherwise + */ + boolean isReady(); } diff --git a/src/main/java/server/NonBlockingServer.java b/src/main/java/server/NonBlockingServer.java index 56f8297..6a16dd5 100644 --- a/src/main/java/server/NonBlockingServer.java +++ b/src/main/java/server/NonBlockingServer.java @@ -16,6 +16,12 @@ class NonBlockingServer implements FTPServer { private final Path root; + public boolean isReady() { + return ready; + } + + private Boolean ready = false; + NonBlockingServer(Path root) { this.root = root; } @@ -23,14 +29,15 @@ class NonBlockingServer implements FTPServer { @Override public void run(@NotNull String hostname, int port) throws IOException { FileSystem fileSystem = new FileSystem(root); - System.out.println("run"); Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); - System.out.println("binded to " + hostname + ":" + port); serverSocketChannel.bind(new InetSocketAddress(hostname, port)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); QueryProcessor queryProcessor = new QueryProcessor(); + synchronized (this) { + ready = true; + } while (!Thread.interrupted()) { selector.selectNow(); Iterator keyIterator = selector.selectedKeys().iterator(); diff --git a/src/main/java/utils/ByteUtils.java b/src/main/java/utils/ByteUtils.java index edbc3fd..df2dbc9 100644 --- a/src/main/java/utils/ByteUtils.java +++ b/src/main/java/utils/ByteUtils.java @@ -9,7 +9,7 @@ */ public class ByteUtils { private static final ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES); - private static final ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES); + private static final ByteBuffer byteBuffer = ByteBuffer.allocate(Byte.BYTES); /** * Converts long to byte array. diff --git a/src/main/java/utils/ReadableMessage.java b/src/main/java/utils/ReadableMessage.java index 343304c..36337d9 100644 --- a/src/main/java/utils/ReadableMessage.java +++ b/src/main/java/utils/ReadableMessage.java @@ -43,9 +43,9 @@ public SocketChannel getChannel() { * @throws IOException If an I/O error occurs. */ public boolean read() throws IOException { - if (read < 4) { + if (read < Long.BYTES) { read += channel.read(buffer); - if (read >= 4) { + if (read >= Long.BYTES) { buffer.flip(); size = buffer.getLong(); } diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java index 92ba519..ad49376 100644 --- a/src/main/java/utils/SmallWritableMessage.java +++ b/src/main/java/utils/SmallWritableMessage.java @@ -17,7 +17,6 @@ public class SmallWritableMessage extends WritableMessage { */ public SmallWritableMessage(@NotNull SocketChannel channel, @NotNull byte[] data) { super(channel, new ByteArrayInputStream(addHeader(data)), getSize(data)); - System.out.println("SmallMessage of " + data.length + " bytes"); } private static long getSize(@NotNull byte[] data) { diff --git a/src/test/java/IntegrationTest.java b/src/test/java/IntegrationTest.java index 714fc58..3f60dd9 100644 --- a/src/test/java/IntegrationTest.java +++ b/src/test/java/IntegrationTest.java @@ -1,18 +1,103 @@ import client.Client; +import org.junit.Assert; import org.junit.Test; import server.FTPServer; -import server.FileSystem; -import java.nio.file.Paths; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; public class IntegrationTest { + private static final String HOSTNAME = "localhost"; + private static final int PORT = 1234; + private static final TestFile A = new TestFile("A.txt", "A text"); + private static final TestFile B = new TestFile("B.txt", "B text"); + private static final TestFile A_B = new TestFile("A/B.txt", "A/B.txt text"); + private static final TestFile A_B_C = new TestFile("A/B/C.txt", "A/B/C.txt text"); + + private Client client; + private Thread serverThread; + @Test public void processGet() throws Exception { - FTPServer server = FTPServer.getNonBlocking(Paths.get(".")); - FileSystem fileSystem = new FileSystem(Paths.get(".")); - Client client = Client.getNonBlocking(); - //ArrayUtils.addAll(ByteUtils.byteToBytes((byte) NonBlockingClient.Query.GET.ordinal()), + prepare(); + + A.create(); + String path = A.getPath().toString(); + + Path downloadedPath = client.executeGet(A.getPath().toString()); + + Assert.assertArrayEquals(Files.readAllBytes(downloadedPath), A.getContent()); + Files.delete(downloadedPath); + + finish(); + } + + + + private void finish() throws IOException { + client.disconnect(); + serverThread.interrupt(); + } + + private void prepare() throws IOException { + Path serverRoot = TestFile.getTestDir(); + FTPServer server = FTPServer.getNonBlocking(serverRoot); + client = Client.getNonBlocking(); + serverThread = new Thread(() -> { + try { + server.run(HOSTNAME, PORT); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + }); + serverThread.start(); + + //noinspection StatementWithEmptyBody + while(!server.isReady()); + client.connect(HOSTNAME, PORT); } +} +class TestFile { + private static final Path TEST_DIR; + + static Path getTestDir() { + return TEST_DIR; + } + + static { + try { + TEST_DIR = Files.createTempDirectory("FTP_root"); + } catch (IOException e) { + throw new RuntimeException("Failed to create test directory"); + } + } + + private String content; + private Path path; + + TestFile(String path, String content){ + this.content = content; + this.path = TEST_DIR.resolve(path); + } + void create() throws IOException { + Path parent = path.getParent(); + if (parent != null && Files.notExists(parent)) { + Files.createDirectories(parent); + } + if (Files.notExists(path)) { + Files.createFile(path); + } + Files.write(path, content.getBytes()); + } + + Path getPath() { + return path; + } + + byte[] getContent() { + return content.getBytes(); + } } \ No newline at end of file From 5245c7456bc39f34ada05789c4b3d4088580a232 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 4 May 2017 14:16:36 +0300 Subject: [PATCH 20/24] First version of GUI --- src/main/java/client/gui/ClientGuiApp.java | 19 ++++++ src/main/java/client/gui/SceneFactory.java | 67 ++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/main/java/client/gui/ClientGuiApp.java create mode 100644 src/main/java/client/gui/SceneFactory.java diff --git a/src/main/java/client/gui/ClientGuiApp.java b/src/main/java/client/gui/ClientGuiApp.java new file mode 100644 index 0000000..e85998a --- /dev/null +++ b/src/main/java/client/gui/ClientGuiApp.java @@ -0,0 +1,19 @@ +package client.gui; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class ClientGuiApp extends Application { + @Override + public void start(Stage stage) throws Exception { + Scene scene = SceneFactory.create(); + stage.setScene(scene); + stage.show(); + } + + public static void main(String[] args) { + launch(args); + } + +} \ No newline at end of file diff --git a/src/main/java/client/gui/SceneFactory.java b/src/main/java/client/gui/SceneFactory.java new file mode 100644 index 0000000..603549e --- /dev/null +++ b/src/main/java/client/gui/SceneFactory.java @@ -0,0 +1,67 @@ +package client.gui; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import utils.SimpleFile; + +import java.util.ArrayList; +import java.util.List; + +public class SceneFactory { + private static class VBoxBuilder { + private List nodeList; + + public VBoxBuilder() { + this.nodeList = new ArrayList<>(); + } + + private VBoxBuilder addNodesHorizontally(Node... nodes) { + nodeList.add(new HBox(10, nodes)); + return this; + } + + private VBox build() { + Node[] nodes = new Node[nodeList.size()]; + nodes = nodeList.toArray(nodes); + VBox vbox = new VBox(nodes); + vbox.setMinWidth(MIN_WIDTH); + vbox.setSpacing(10); + return vbox; + } + } + + private static final double MIN_WIDTH = 400; + + public static Scene create() { + VBoxBuilder vBoxBuilder = new VBoxBuilder(); + + + TableView table = new TableView<>(); + + + ObservableList teamMembers = FXCollections.observableArrayList(); + table.setItems(teamMembers); + + + + TableColumn firstNameCol = new TableColumn<>("Filename"); + firstNameCol.setCellValueFactory(new PropertyValueFactory("name")); + TableColumn lastNameCol = new TableColumn<>("Is directory"); + lastNameCol.setCellValueFactory(new PropertyValueFactory("isDirectory")); + + table.getColumns().setAll(firstNameCol, lastNameCol); + + VBox root = vBoxBuilder.addNodesHorizontally(new Label("Hostname:"), new TextField(), new Label("Port:"), new TextField(), new Button("Connect")) + .addNodesHorizontally(new Label("Current directory:"), new TextField()) + .addNodesHorizontally(table) + .build(); + return new Scene(root); + } + +} From 9824f987c5423759a019fe26dbb7147ac7ee2b01 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 4 May 2017 14:21:26 +0300 Subject: [PATCH 21/24] build.gradle fixed --- build.gradle | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 8dcc7e5..98839f4 100644 --- a/build.gradle +++ b/build.gradle @@ -19,20 +19,30 @@ task serverJar(type: Jar) { manifest { attributes 'Implementation-Title': 'Ftp server jar file', 'Implementation-Version': version, - 'Main-Class': 'ru.spbau.zhidkov.server.MainServer' + 'Main-Class': 'server.ServerConsoleApp' } baseName = 'ftp-server' from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } with jar } -task clientJar(type: Jar) { +task consoleClientJar(type: Jar) { manifest { - attributes 'Implementation-Title': 'Ftp client jar file', + attributes 'Implementation-Title': 'Ftp console client jar file', 'Implementation-Version': version, - 'Main-Class': 'ru.spbau.zhidkov.client.MainClient' + 'Main-Class': 'client.ClientConsoleApp' } - baseName = 'ftp-client' + baseName = 'ftp-client-console' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} +task guiClientJar(type: Jar) { + manifest { + attributes 'Implementation-Title': 'Ftp gui client jar file', + 'Implementation-Version': version, + 'Main-Class': 'client.gui.ClientGuiApp' + } + baseName = 'ftp-client-gui' from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } with jar } \ No newline at end of file From 321e764a0a9b2f79223679e05a3f590e68a3d305 Mon Sep 17 00:00:00 2001 From: Ilya Date: Fri, 5 May 2017 00:39:48 +0300 Subject: [PATCH 22/24] List request fixed --- src/main/java/client/gui/ClientGuiApp.java | 2 +- src/main/java/client/gui/SceneFactory.java | 58 ++++++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/main/java/client/gui/ClientGuiApp.java b/src/main/java/client/gui/ClientGuiApp.java index e85998a..e166992 100644 --- a/src/main/java/client/gui/ClientGuiApp.java +++ b/src/main/java/client/gui/ClientGuiApp.java @@ -7,7 +7,7 @@ public class ClientGuiApp extends Application { @Override public void start(Stage stage) throws Exception { - Scene scene = SceneFactory.create(); + Scene scene = new SceneFactory().create(); stage.setScene(scene); stage.show(); } diff --git a/src/main/java/client/gui/SceneFactory.java b/src/main/java/client/gui/SceneFactory.java index 603549e..f3fbf7b 100644 --- a/src/main/java/client/gui/SceneFactory.java +++ b/src/main/java/client/gui/SceneFactory.java @@ -1,19 +1,28 @@ package client.gui; +import client.Client; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import utils.SimpleFile; +import java.io.IOException; import java.util.ArrayList; import java.util.List; public class SceneFactory { + private TextField hostnameTextField; + private TextField portTextField; + private TextField currentDirectoryTextField; + private TextField filePathTextField; + private TextArea resultTextArea; + private static class VBoxBuilder { private List nodeList; @@ -38,7 +47,7 @@ private VBox build() { private static final double MIN_WIDTH = 400; - public static Scene create() { + public Scene create() { VBoxBuilder vBoxBuilder = new VBoxBuilder(); @@ -48,6 +57,16 @@ public static Scene create() { ObservableList teamMembers = FXCollections.observableArrayList(); table.setItems(teamMembers); + hostnameTextField = new TextField(); + portTextField = new TextField(); + currentDirectoryTextField = new TextField(); + currentDirectoryTextField.setEditable(false); + filePathTextField = new TextField(); + Button getRequestButton = new Button("Get file"); + getRequestButton.setOnMouseClicked(this::performGetRequest); + Button listRequestButton = new Button("List content"); + listRequestButton.setOnMouseClicked(this::performListRequest); + resultTextArea = new TextArea("hey\nmay"); TableColumn firstNameCol = new TableColumn<>("Filename"); @@ -57,11 +76,42 @@ public static Scene create() { table.getColumns().setAll(firstNameCol, lastNameCol); - VBox root = vBoxBuilder.addNodesHorizontally(new Label("Hostname:"), new TextField(), new Label("Port:"), new TextField(), new Button("Connect")) - .addNodesHorizontally(new Label("Current directory:"), new TextField()) - .addNodesHorizontally(table) + VBox root = vBoxBuilder.addNodesHorizontally(new Label("Hostname:"), hostnameTextField, new Label("Port:"), portTextField) + .addNodesHorizontally(new Label("Current directory:"), currentDirectoryTextField) + .addNodesHorizontally(filePathTextField, getRequestButton, listRequestButton) + .addNodesHorizontally(resultTextArea) .build(); return new Scene(root); } + private void performListRequest(MouseEvent mouseEvent) { + System.err.println("listing"); + try { + Client client = Client.getNonBlocking(); + System.err.println("connecting"); + client.connect(hostnameTextField.getText(), Integer.parseInt(portTextField.getText())); + System.err.println("requesting"); + List result = client.executeList(filePathTextField.getText()); + StringBuilder resultString = new StringBuilder(); + for (SimpleFile file : result) { + //noinspection StringConcatenationInsideStringBufferAppend + resultString.append(file.name + " " + (file.isDirectory ? "directory" : "file") + "\n"); + } + System.err.println("result:"); + System.err.println(resultString.toString()); + resultTextArea.setText(resultString.toString()); + client.disconnect(); + } catch (IOException e) { + showAlert(Alert.AlertType.ERROR, "Error while performing list request: " + e.getMessage()); + } + } + + private void showAlert(Alert.AlertType alertType, String message) { + Alert alert = new Alert(alertType, message); + alert.show(); + } + + private void performGetRequest(MouseEvent mouseEvent) { + } + } From 24294c6f048bfbdb1df6dd0727b6f6669ee8f06d Mon Sep 17 00:00:00 2001 From: Ilya Date: Mon, 8 May 2017 19:23:12 +0300 Subject: [PATCH 23/24] Some improvements in GUI app --- src/main/java/client/Client.java | 15 +++++++++- src/main/java/client/NonBlockingClient.java | 11 +++++--- src/main/java/client/gui/ClientGuiApp.java | 7 +++++ src/main/java/client/gui/SceneFactory.java | 31 +++++++++++++++------ src/main/java/client/gui/package-info.java | 4 +++ src/main/java/utils/BigReadableMessage.java | 7 +++-- 6 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 src/main/java/client/gui/package-info.java diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java index 8218709..c64a749 100644 --- a/src/main/java/client/Client.java +++ b/src/main/java/client/Client.java @@ -1,6 +1,7 @@ package client; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import utils.SimpleFile; import java.io.IOException; @@ -56,7 +57,19 @@ enum Query { * @throws IOException If an I/O error occurs */ @NotNull - Path executeGet(@NotNull String path) throws IOException ; + default Path executeGet(@NotNull String path) throws IOException { + return executeGet(path, null); + } + + /** + * Downloads file to a specified directory. + * @param path path to file on server + * @param destinationFolder path to destination folder on client + * @return path to downloaded file on client + * @throws IOException If an I/O error occurs + */ + @NotNull + Path executeGet(@NotNull String path, @Nullable String destinationFolder) throws IOException ; /** * Creates non-blocking FTP client. diff --git a/src/main/java/client/NonBlockingClient.java b/src/main/java/client/NonBlockingClient.java index 1e4655b..b02c617 100644 --- a/src/main/java/client/NonBlockingClient.java +++ b/src/main/java/client/NonBlockingClient.java @@ -2,6 +2,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import utils.*; import java.io.ByteArrayInputStream; @@ -12,6 +13,7 @@ import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; /** @@ -60,9 +62,10 @@ public List executeList(@NotNull String path) throws IOException { @NotNull @Override - public Path executeGet(@NotNull String path) throws IOException { + public Path executeGet(@NotNull String path, @Nullable String destination) throws IOException { sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes(StandardCharsets.UTF_8))); - return getBigResponse().getPath(); + Path destinationPath = (destination == null ? null : Paths.get(destination)); + return getBigResponse(destinationPath).getPath(); } @NotNull @@ -73,8 +76,8 @@ private byte[] getSmallResponse() throws IOException { } @NotNull - private BigReadableMessage getBigResponse() throws IOException { - BigReadableMessage message = new BigReadableMessage(channel); + private BigReadableMessage getBigResponse(@Nullable Path destination) throws IOException { + BigReadableMessage message = new BigReadableMessage(channel, destination); getResponse(message); return message; } diff --git a/src/main/java/client/gui/ClientGuiApp.java b/src/main/java/client/gui/ClientGuiApp.java index e166992..69b5c0a 100644 --- a/src/main/java/client/gui/ClientGuiApp.java +++ b/src/main/java/client/gui/ClientGuiApp.java @@ -4,6 +4,9 @@ import javafx.scene.Scene; import javafx.stage.Stage; +/** + * Gui application for communication with FTP server. + */ public class ClientGuiApp extends Application { @Override public void start(Stage stage) throws Exception { @@ -12,6 +15,10 @@ public void start(Stage stage) throws Exception { stage.show(); } + /** + * Main method to launch GUI app. + * @param args console arguments + */ public static void main(String[] args) { launch(args); } diff --git a/src/main/java/client/gui/SceneFactory.java b/src/main/java/client/gui/SceneFactory.java index f3fbf7b..294c796 100644 --- a/src/main/java/client/gui/SceneFactory.java +++ b/src/main/java/client/gui/SceneFactory.java @@ -13,20 +13,21 @@ import utils.SimpleFile; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -public class SceneFactory { +class SceneFactory { private TextField hostnameTextField; private TextField portTextField; - private TextField currentDirectoryTextField; private TextField filePathTextField; private TextArea resultTextArea; + private TextField destinationTextField; private static class VBoxBuilder { private List nodeList; - public VBoxBuilder() { + VBoxBuilder() { this.nodeList = new ArrayList<>(); } @@ -47,7 +48,7 @@ private VBox build() { private static final double MIN_WIDTH = 400; - public Scene create() { + Scene create() { VBoxBuilder vBoxBuilder = new VBoxBuilder(); @@ -59,8 +60,7 @@ public Scene create() { hostnameTextField = new TextField(); portTextField = new TextField(); - currentDirectoryTextField = new TextField(); - currentDirectoryTextField.setEditable(false); + destinationTextField = new TextField(); filePathTextField = new TextField(); Button getRequestButton = new Button("Get file"); getRequestButton.setOnMouseClicked(this::performGetRequest); @@ -77,7 +77,7 @@ public Scene create() { table.getColumns().setAll(firstNameCol, lastNameCol); VBox root = vBoxBuilder.addNodesHorizontally(new Label("Hostname:"), hostnameTextField, new Label("Port:"), portTextField) - .addNodesHorizontally(new Label("Current directory:"), currentDirectoryTextField) + .addNodesHorizontally(new Label("Download directory:"), destinationTextField) .addNodesHorizontally(filePathTextField, getRequestButton, listRequestButton) .addNodesHorizontally(resultTextArea) .build(); @@ -112,6 +112,21 @@ private void showAlert(Alert.AlertType alertType, String message) { } private void performGetRequest(MouseEvent mouseEvent) { + System.err.println("listing"); + try { + Client client = Client.getNonBlocking(); + System.err.println("connecting"); + client.connect(hostnameTextField.getText(), Integer.parseInt(portTextField.getText())); + System.err.println("requesting"); + String destination = destinationTextField.getText(); + Path result = client.executeGet(filePathTextField.getText(), + destination.isEmpty() ? null : destination); + System.err.println("result:"); + System.err.println(result.toAbsolutePath().toString()); + showAlert(Alert.AlertType.INFORMATION, result.toAbsolutePath().toString()); + client.disconnect(); + } catch (IOException e) { + showAlert(Alert.AlertType.ERROR, "Error while performing list request: " + e.getMessage()); + } } - } diff --git a/src/main/java/client/gui/package-info.java b/src/main/java/client/gui/package-info.java new file mode 100644 index 0000000..28d971e --- /dev/null +++ b/src/main/java/client/gui/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides GUI application for FTP client. + */ +package client.gui; \ No newline at end of file diff --git a/src/main/java/utils/BigReadableMessage.java b/src/main/java/utils/BigReadableMessage.java index d521d0f..ab8a458 100644 --- a/src/main/java/utils/BigReadableMessage.java +++ b/src/main/java/utils/BigReadableMessage.java @@ -1,6 +1,7 @@ package utils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.channels.SocketChannel; @@ -20,8 +21,10 @@ public class BigReadableMessage extends ReadableMessage { * @param channel channel to read data from * @throws IOException If an I/O error occurs */ - public BigReadableMessage(@NotNull SocketChannel channel) throws IOException { - super(channel, new KnownFileOutputStream(Files.createTempFile("dwnl", "").toFile())); + public BigReadableMessage(@NotNull SocketChannel channel, @Nullable Path destination) throws IOException { + super(channel, (destination != null ? + new KnownFileOutputStream(Files.createTempFile(destination, "dwnl", "").toFile()) : + new KnownFileOutputStream(Files.createTempFile("dwnl", "").toFile()))); path = ((KnownFileOutputStream) getDestination()).getFile().getAbsolutePath(); } From a507778c3f02b5df028b9db3fe92e55d4fe73e17 Mon Sep 17 00:00:00 2001 From: Ilya Date: Tue, 9 May 2017 18:09:30 +0300 Subject: [PATCH 24/24] Fixed travis --- .travis.yml | 8 +++++++- src/test/java/FileSystemTest.java | 4 ++++ src/test/java/TestFile.java | 4 ++++ src/test/java/server/QueryProcessorTest.java | 5 +++++ 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/test/java/FileSystemTest.java create mode 100644 src/test/java/TestFile.java create mode 100644 src/test/java/server/QueryProcessorTest.java diff --git a/.travis.yml b/.travis.yml index a1866fa..2066b7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,10 @@ language: java jdk: -- oraclejdk8 \ No newline at end of file +- oraclejdk8 + +sudo: false +addons: + apt: + packages: + - oracle-java8-installer \ No newline at end of file diff --git a/src/test/java/FileSystemTest.java b/src/test/java/FileSystemTest.java new file mode 100644 index 0000000..7c43013 --- /dev/null +++ b/src/test/java/FileSystemTest.java @@ -0,0 +1,4 @@ +package PACKAGE_NAME; + +public class FileSystemTest { +} diff --git a/src/test/java/TestFile.java b/src/test/java/TestFile.java new file mode 100644 index 0000000..c91a5c7 --- /dev/null +++ b/src/test/java/TestFile.java @@ -0,0 +1,4 @@ +package PACKAGE_NAME; + +public class TestFile { +} diff --git a/src/test/java/server/QueryProcessorTest.java b/src/test/java/server/QueryProcessorTest.java new file mode 100644 index 0000000..a44d8b8 --- /dev/null +++ b/src/test/java/server/QueryProcessorTest.java @@ -0,0 +1,5 @@ +import static org.junit.Assert.*; + +public class QueryProcessorTest { + +} \ No newline at end of file