commit 702a724422797b8081a8bed0277b319b13ddc735 Author: Lucio Lelii Date: Fri Nov 25 14:42:29 2016 +0000 branch for release 4.2 git-svn-id: https://svn.d4science-ii.research-infrastructures.eu/gcube/branches/common/common-smartgears/2.1@134815 82a268e6-3cf1-43bd-a215-b396298e98cf diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..50217e7 --- /dev/null +++ b/.classpath @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..a4930ed --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + common-service + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/distro/LICENSE b/distro/LICENSE new file mode 100644 index 0000000..3695e26 --- /dev/null +++ b/distro/LICENSE @@ -0,0 +1 @@ +${gcube.license} diff --git a/distro/README b/distro/README new file mode 100644 index 0000000..5b353ed --- /dev/null +++ b/distro/README @@ -0,0 +1,70 @@ +The gCube System - ${name} +-------------------------------------------------- + +${description} + + +${gcube.description} + +${gcube.funding} + + +Version +-------------------------------------------------- + +${version} (${buildDate}) + +Please see the file named "changelog.xml" in this directory for the release notes. + + +Authors +-------------------------------------------------- + +* Fabio Simeoni (fabio.simeoni@fao.org), FAO of the UN, Italy +* Luca Frosini (luca.frosini@isti.cnr.it), CNR, Italy +* Lucio Lelii (lucio.lelii@isti.cnr.it), CNT, Italy + + +Maintainers +----------- + +* Luca Frosini (luca.frosini@isti.cnr.it), CNR, Italy +* Lucio Lelii (lucio.lelii@isti.cnr.it), CNT, Italy + + +Download information +-------------------------------------------------- + +Source code is available from SVN: + ${scm.url} + +Binaries can be downloaded from the gCube website: + ${gcube.website} + + +Installation +-------------------------------------------------- + +Installation documentation is available on-line in the gCube Wiki: + ${gcube.wikiRoot}/Smartgears + + +Documentation +-------------------------------------------------- + +Documentation is available on-line in the gCube Wiki: + ${gcube.wikiRoot}/Smartgears + + +Support +-------------------------------------------------- + +Bugs and support requests can be reported in the gCube issue tracking tool: + ${gcube.issueTracking} + + +Licensing +-------------------------------------------------- + +This software is licensed under the terms you may find in the file named "LICENSE" in this directory. + diff --git a/distro/changelog.xml b/distro/changelog.xml new file mode 100644 index 0000000..dd3b43e --- /dev/null +++ b/distro/changelog.xml @@ -0,0 +1,39 @@ + + + proxy configuration added + solved a bug in events registration for ProfileManager + added a scheduler for period update of GCoreEnpoints + Exclude modified to support exclude for sub-group of handlers + + + integration with Authorization 2.0 + + + Added flush of accounting data + + + Changed accounting version + + + Transparent accounting added on service calls + + + Authorization token control added + Added support to HTTP Basic authorization + + + Fixed available space information on ghn profile + + + scopes can be removed from container + node profile set to static + internal adjustments for move to Java 7 + wildcard allowed in exclude directives + domain corrected derived in gHN profile + cleaner shutdown + further improvement in shutdown handling + + + First Release + + \ No newline at end of file diff --git a/distro/descriptor.xml b/distro/descriptor.xml new file mode 100644 index 0000000..e395580 --- /dev/null +++ b/distro/descriptor.xml @@ -0,0 +1,32 @@ + + servicearchive + + tar.gz + + / + + + ${distroDirectory} + / + true + + README + LICENSE + changelog.xml + profile.xml + + 755 + true + + + + + target/${build.finalName}.${project.packaging} + /${artifactId} + + + + \ No newline at end of file diff --git a/distro/profile.xml b/distro/profile.xml new file mode 100644 index 0000000..91c49e4 --- /dev/null +++ b/distro/profile.xml @@ -0,0 +1,26 @@ + + + + Service + + ${description} + Common + ${artifactId} + 1.0.0 + + + ${artifactId} + ${version} + + ${groupId} + ${artifactId} + ${version} + + + ${build.finalName}.jar + + + + + + diff --git a/distro/smartgears-config.xml b/distro/smartgears-config.xml new file mode 100644 index 0000000..ffa7124 --- /dev/null +++ b/distro/smartgears-config.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/endpoint.xml b/endpoint.xml new file mode 100644 index 0000000..283934f Binary files /dev/null and b/endpoint.xml differ diff --git a/endpoint.xml.backup b/endpoint.xml.backup new file mode 100644 index 0000000..573db75 Binary files /dev/null and b/endpoint.xml.backup differ diff --git a/extra/originals.graffle b/extra/originals.graffle new file mode 100644 index 0000000..e396639 --- /dev/null +++ b/extra/originals.graffle @@ -0,0 +1,612 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 138.17.0.133677 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {559, 783}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2013-08-26 14:09:56 +0000 + Creator + fabio + DisplayScale + 1 0/72 in = 1 0/72 in + GraphDocumentVersion + 6 + GraphicsList + + + Bounds + {{182, 101.9996}, {27.164064, 25.574612}} + Class + ShapedGraphic + ID + 8 + Shape + Circle + Style + + fill + + Color + + b + 0.863685 + g + 0.863671 + r + 0.863697 + + + shadow + + Draws + NO + + stroke + + Color + + b + 0.491337 + g + 0.325267 + r + 0.192584 + + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fnil\fcharset0 HelveticaNeue-Light;} +{\colortbl;\red255\green255\blue255;\red21\green111\blue181;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs42 \cf2 C} + + + + Bounds + {{132.60156, 101.9996}, {27.164064, 25.574612}} + Class + ShapedGraphic + ID + 3 + Shape + Circle + Style + + fill + + Color + + b + 0.863685 + g + 0.863671 + r + 0.863697 + + + shadow + + Draws + NO + + stroke + + Color + + b + 0.491337 + g + 0.325267 + r + 0.192584 + + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fnil\fcharset0 HelveticaNeue-Light;} +{\colortbl;\red255\green255\blue255;\red21\green111\blue181;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs42 \cf2 P} + + + + GridInfo + + GuidesLocked + NO + GuidesVisible + YES + HPages + 1 + ImageCounter + 4 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2013-08-27 13:29:54 +0000 + Modifier + fabio + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {595, 842} + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + QuickLookPreview + + JVBERi0xLjMKJcTl8uXrp/Og0MTGCjUgMCBvYmoKPDwgL0xlbmd0aCA2IDAgUiAvRmls + dGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAGlk02O2zAMhfc+BZftIhqREiVq26Cb + rjqIgR7A6KAYTAaY5v5An2Lrp+h0UTRGYvlFpMjHz2/0SG/kcakWyhbo53f6Rq/0cL4x + bTfi+3Xb6OSd1g34vWKz9aeX/Wmp/7/s2/blj99jnmpOqTm9s+K5JH53ddsWVnXZslLK + 2Ykq40hO7HzhDE2cJI6TkqLjFAuNOC2OM/OyQfMuWoiUFLtyRlxkZxZsUkJymgtyH3GE + uMBOsqKGI/sylF5Bj2t1Iq5nN9QZzaYKDuWd/jZ6gtFf8H3ebTpfYI7E7BUZ0IEPRThh + pamKhS5nDA4O4jrVG0azbLCpWcceHkaYMlnH7F0OloZ1zOYswOARJ8GZWJmtY8EuEcS1 + 5obSLOAjbrauZR/WjQp6XKtzso496lSWYV1ThnW9v40ugLjx6p03CSolVtfui4pZ9sVg + IQHkyTWOybGFhBmbE8y3OvhpJTl8xf2kzpIm9J5dLPgIrVd6WFfQCPvXJ/rw9SOtz/R5 + RRUgO4UEkPZFZrwVVTGtJwtaZbg/US2YSMTLMKgeSqP6CFtmOEsBwlwmhAtwxYwmxdSV + bEg9QW3epRyXxjTxLozjuUdNSPfcHel+fkP6j94q0feh3EnmImp1JEFUUvUnFg4h/w3j + lq5P+YqhHFZ1hibloLiHTRC3UgeyrZ2htKZniA9nBsNdaC9Rj5oQ7rk7wv38prR59t52 + gv+N3KJODdP+H3DPA9zHX03XNKcKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjU1NApl + bmRvYmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDQgMCBSIC9SZXNvdXJj + ZXMgNyAwIFIgL0NvbnRlbnRzIDUgMCBSIC9NZWRpYUJveCBbMCAwIDU1OSA3ODNdCj4+ + CmVuZG9iago3IDAgb2JqCjw8IC9Qcm9jU2V0IFsgL1BERiAvVGV4dCBdIC9Db2xvclNw + YWNlIDw8IC9DczEgOCAwIFIgL0NzMiA5IDAgUiA+PiAvRm9udCA8PAovVFQxLjAgMTAg + MCBSID4+ID4+CmVuZG9iagoxMSAwIG9iago8PCAvTGVuZ3RoIDEyIDAgUiAvTiAzIC9B + bHRlcm5hdGUgL0RldmljZVJHQiAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0K + eAGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb + 6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6 + yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO + 6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQC + qwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/h + L49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4 + H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4 + tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjcz + ohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrY + BbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1G + NjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtn + E6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqw + Syv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL4 + 2K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszS + ueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9 + nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0 + CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInv + i0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8Ookmr + dtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5b + RIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4 + gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4CmVuZHN0cmVhbQplbmRvYmoKMTIgMCBv + YmoKMTA0NwplbmRvYmoKOCAwIG9iagpbIC9JQ0NCYXNlZCAxMSAwIFIgXQplbmRvYmoK + MTMgMCBvYmoKPDwgL0xlbmd0aCAxNCAwIFIgL04gMyAvQWx0ZXJuYXRlIC9EZXZpY2VS + R0IgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngBnZZ3VFPZFofPvTe90BIi + ICX0GnoJINI7SBUEUYlJgFAChoQmdkQFRhQRKVZkVMABR4ciY0UUC4OCYtcJ8hBQxsFR + REXl3YxrCe+tNfPemv3HWd/Z57fX2Wfvfde6AFD8ggTCdFgBgDShWBTu68FcEhPLxPcC + GBABDlgBwOFmZgRH+EQC1Py9PZmZqEjGs/buLoBku9ssv1Amc9b/f5EiN0MkBgAKRdU2 + PH4mF+UClFOzxRky/wTK9JUpMoYxMhahCaKsIuPEr2z2p+Yru8mYlybkoRpZzhm8NJ6M + u1DemiXho4wEoVyYJeBno3wHZb1USZoA5fco09P4nEwAMBSZX8znJqFsiTJFFBnuifIC + AAiUxDm8cg6L+TlongB4pmfkigSJSWKmEdeYaeXoyGb68bNT+WIxK5TDTeGIeEzP9LQM + jjAXgK9vlkUBJVltmWiR7a0c7e1Z1uZo+b/Z3x5+U/09yHr7VfEm7M+eQYyeWd9s7Kwv + vRYA9iRamx2zvpVVALRtBkDl4axP7yAA8gUAtN6c8x6GbF6SxOIMJwuL7OxscwGfay4r + 6Df7n4Jvyr+GOfeZy+77VjumFz+BI0kVM2VF5aanpktEzMwMDpfPZP33EP/jwDlpzcnD + LJyfwBfxhehVUeiUCYSJaLuFPIFYkC5kCoR/1eF/GDYnBxl+nWsUaHVfAH2FOVC4SQfI + bz0AQyMDJG4/egJ961sQMQrIvrxorZGvc48yev7n+h8LXIpu4UxBIlPm9gyPZHIloiwZ + o9+EbMECEpAHdKAKNIEuMAIsYA0cgDNwA94gAISASBADlgMuSAJpQASyQT7YAApBMdgB + doNqcADUgXrQBE6CNnAGXARXwA1wCwyAR0AKhsFLMAHegWkIgvAQFaJBqpAWpA+ZQtYQ + G1oIeUNBUDgUA8VDiZAQkkD50CaoGCqDqqFDUD30I3Qaughdg/qgB9AgNAb9AX2EEZgC + 02EN2AC2gNmwOxwIR8LL4ER4FZwHF8Db4Uq4Fj4Ot8IX4RvwACyFX8KTCEDICAPRRlgI + G/FEQpBYJAERIWuRIqQCqUWakA6kG7mNSJFx5AMGh6FhmBgWxhnjh1mM4WJWYdZiSjDV + mGOYVkwX5jZmEDOB+YKlYtWxplgnrD92CTYRm40txFZgj2BbsJexA9hh7DscDsfAGeIc + cH64GFwybjWuBLcP14y7gOvDDeEm8Xi8Kt4U74IPwXPwYnwhvgp/HH8e348fxr8nkAla + BGuCDyGWICRsJFQQGgjnCP2EEcI0UYGoT3QihhB5xFxiKbGO2EG8SRwmTpMUSYYkF1Ik + KZm0gVRJaiJdJj0mvSGTyTpkR3IYWUBeT64knyBfJQ+SP1CUKCYUT0ocRULZTjlKuUB5 + QHlDpVINqG7UWKqYup1aT71EfUp9L0eTM5fzl+PJrZOrkWuV65d7JU+U15d3l18unydf + IX9K/qb8uAJRwUDBU4GjsFahRuG0wj2FSUWaopViiGKaYolig+I1xVElvJKBkrcST6lA + 6bDSJaUhGkLTpXnSuLRNtDraZdowHUc3pPvTk+nF9B/ovfQJZSVlW+Uo5RzlGuWzylIG + wjBg+DNSGaWMk4y7jI/zNOa5z+PP2zavaV7/vCmV+SpuKnyVIpVmlQGVj6pMVW/VFNWd + qm2qT9QwaiZqYWrZavvVLquNz6fPd57PnV80/+T8h+qwuol6uPpq9cPqPeqTGpoavhoZ + GlUalzTGNRmabprJmuWa5zTHtGhaC7UEWuVa57VeMJWZ7sxUZiWzizmhra7tpy3RPqTd + qz2tY6izWGejTrPOE12SLls3Qbdct1N3Qk9LL1gvX69R76E+UZ+tn6S/R79bf8rA0CDa + YItBm8GooYqhv2GeYaPhYyOqkavRKqNaozvGOGO2cYrxPuNbJrCJnUmSSY3JTVPY1N5U + YLrPtM8Ma+ZoJjSrNbvHorDcWVmsRtagOcM8yHyjeZv5Kws9i1iLnRbdFl8s7SxTLess + H1kpWQVYbbTqsPrD2sSaa11jfceGauNjs86m3ea1rakt33a/7X07ml2w3Ra7TrvP9g72 + Ivsm+zEHPYd4h70O99h0dii7hH3VEevo4bjO8YzjByd7J7HTSaffnVnOKc4NzqMLDBfw + F9QtGHLRceG4HHKRLmQujF94cKHUVduV41rr+sxN143ndsRtxN3YPdn9uPsrD0sPkUeL + x5Snk+cazwteiJevV5FXr7eS92Lvau+nPjo+iT6NPhO+dr6rfS/4Yf0C/Xb63fPX8Of6 + 1/tPBDgErAnoCqQERgRWBz4LMgkSBXUEw8EBwbuCHy/SXyRc1BYCQvxDdoU8CTUMXRX6 + cxguLDSsJux5uFV4fnh3BC1iRURDxLtIj8jSyEeLjRZLFndGyUfFRdVHTUV7RZdFS5dY + LFmz5EaMWowgpj0WHxsVeyR2cqn30t1Lh+Ps4grj7i4zXJaz7NpyteWpy8+ukF/BWXEq + HhsfHd8Q/4kTwqnlTK70X7l35QTXk7uH+5LnxivnjfFd+GX8kQSXhLKE0USXxF2JY0mu + SRVJ4wJPQbXgdbJf8oHkqZSQlKMpM6nRqc1phLT4tNNCJWGKsCtdMz0nvS/DNKMwQ7rK + adXuVROiQNGRTChzWWa7mI7+TPVIjCSbJYNZC7Nqst5nR2WfylHMEeb05JrkbssdyfPJ + +341ZjV3dWe+dv6G/ME17msOrYXWrlzbuU53XcG64fW+649tIG1I2fDLRsuNZRvfbore + 1FGgUbC+YGiz7+bGQrlCUeG9Lc5bDmzFbBVs7d1ms61q25ciXtH1YsviiuJPJdyS699Z + fVf53cz2hO29pfal+3fgdgh33N3puvNYmWJZXtnQruBdreXM8qLyt7tX7L5WYVtxYA9p + j2SPtDKosr1Kr2pH1afqpOqBGo+a5r3qe7ftndrH29e/321/0wGNA8UHPh4UHLx/yPdQ + a61BbcVh3OGsw8/rouq6v2d/X39E7Ujxkc9HhUelx8KPddU71Nc3qDeUNsKNksax43HH + b/3g9UN7E6vpUDOjufgEOCE58eLH+B/vngw82XmKfarpJ/2f9rbQWopaodbc1om2pDZp + e0x73+mA050dzh0tP5v/fPSM9pmas8pnS8+RzhWcmzmfd37yQsaF8YuJF4c6V3Q+urTk + 0p2usK7ey4GXr17xuXKp2737/FWXq2euOV07fZ19ve2G/Y3WHruell/sfmnpte9tvelw + s/2W462OvgV95/pd+y/e9rp95Y7/nRsDiwb67i6+e/9e3D3pfd790QepD14/zHo4/Wj9 + Y+zjoicKTyqeqj+t/dX412apvfTsoNdgz7OIZ4+GuEMv/5X5r0/DBc+pzytGtEbqR61H + z4z5jN16sfTF8MuMl9Pjhb8p/rb3ldGrn353+71nYsnE8GvR65k/St6ovjn61vZt52To + 5NN3ae+mp4req74/9oH9oftj9MeR6exP+E+Vn40/d3wJ/PJ4Jm1m5t/3hPP7CmVuZHN0 + cmVhbQplbmRvYmoKMTQgMCBvYmoKMjYxMgplbmRvYmoKOSAwIG9iagpbIC9JQ0NCYXNl + ZCAxMyAwIFIgXQplbmRvYmoKNCAwIG9iago8PCAvVHlwZSAvUGFnZXMgL01lZGlhQm94 + IFswIDAgNjEyIDc5Ml0gL0NvdW50IDEgL0tpZHMgWyAzIDAgUiBdID4+CmVuZG9iagox + NSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvT3V0bGluZXMgMiAwIFIgL1BhZ2VzIDQg + MCBSID4+CmVuZG9iagoyIDAgb2JqCjw8IC9MYXN0IDE2IDAgUiAvRmlyc3QgMTcgMCBS + ID4+CmVuZG9iagoxNyAwIG9iago8PCAvUGFyZW50IDE4IDAgUiAvQ291bnQgMCAvRGVz + dCBbIDMgMCBSIC9YWVogMCA3ODMgbnVsbCBdIC9UaXRsZSAoQ2FudmFzIDEpCj4+CmVu + ZG9iagoxOCAwIG9iago8PCA+PgplbmRvYmoKMTYgMCBvYmoKPDwgL1BhcmVudCAxOCAw + IFIgL0NvdW50IDAgL0Rlc3QgWyAzIDAgUiAvWFlaIDAgNzgzIG51bGwgXSAvVGl0bGUg + KENhbnZhcyAxKQo+PgplbmRvYmoKMTAgMCBvYmoKPDwgL1R5cGUgL0ZvbnQgL1N1YnR5 + cGUgL1RydWVUeXBlIC9CYXNlRm9udCAvWU5TU1ZWK0hlbHZldGljYU5ldWUtTGlnaHQg + L0ZvbnREZXNjcmlwdG9yCjE5IDAgUiAvRW5jb2RpbmcgL01hY1JvbWFuRW5jb2Rpbmcg + L0ZpcnN0Q2hhciA2NyAvTGFzdENoYXIgODAgL1dpZHRocyBbIDcwNAowIDAgMCAwIDAg + MCAwIDAgMCAwIDAgMCA2MzAgXSA+PgplbmRvYmoKMTkgMCBvYmoKPDwgL1R5cGUgL0Zv + bnREZXNjcmlwdG9yIC9Gb250TmFtZSAvWU5TU1ZWK0hlbHZldGljYU5ldWUtTGlnaHQg + L0ZsYWdzIDMyIC9Gb250QkJveApbLTQzMCAtMzM3IDE0MjAgMTE2M10gL0l0YWxpY0Fu + Z2xlIDAgL0FzY2VudCA5NjcgL0Rlc2NlbnQgLTIxMyAvQ2FwSGVpZ2h0Cjg1OSAvU3Rl + bVYgMCAvTGVhZGluZyAyOSAvWEhlaWdodCA2NDQgL01heFdpZHRoIDE0ODAgL0ZvbnRG + aWxlMiAyMCAwIFIgPj4KZW5kb2JqCjIwIDAgb2JqCjw8IC9MZW5ndGggMjEgMCBSIC9M + ZW5ndGgxIDIyNDQgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngBxVVdTFtl + GH6/c9rS8lNaxv8Z3TkrrUA76Fpg/Agro/wMCDJAck4iwwZaYAGHkyEzM/ZGklVCTEzM + TLzwysQrjzFZOm4gXvh7Y4xeaLg1WXZllpk441qf77Q2orhwscSvec73/n/v95zmPevX + rseomBIkUnh+NbpGxrKEsbXOb6zLhkrse+yn42uLqzn9NyJT5+LKjXhWt9wkYg+XYtGF + rE5/YG9fgiEX34q9fml1fTOrW7hevHJ1Pue3rEO3rkY3c+fTAXT5pehqLBtvvY3dv3b1 + FR6HZeVxDWvXYrl4pkJ/rfneuXe4N7vsaAmSSDepgF4FBMPuIgimTjLBy/3A4M7s/Fzp + s7+yGvEej7nT/2iY79/cNr2Qnkx/bN42vQXVlqtg5IhfZHxUZ/4yPZmRzdtGJZ7y1xJT + VO5LUaGPdpHFqNjHdtGKyIU9nH2J+qmVPFSFhDLfHplJpWHqoAaqNiy0h7Q5Gqce8lNt + LkikJXoeiQGqywbtkgWZThzE5IHXl6sjKSIfXOX4dVEbReg8NHvmAhUKJioVPgW+plJ4 + CZH+0RTZJtRPGNvRUizzZooidXdxS3Hu8hlU9MvywHJEZy9CEfwwNCmQRL88qIuewUnV + rclJOXlxISkPykvRBd3kMXY4YkmtRdZpSl3Gc1pV9LAm5cWYpnWhjonXQQrCkxoqXMlV + wG6YWh4jyOwflXXRO6FeUvVERNLDEU1SFHlA359Q9f2IpGgaoiz5TtExZyHbcwF6tjTB + b81WmUINlNCSSV4TmuBV9P1kUkriJobFraQY5Qy4KY8RPQMpFp5QuSvsViRucCtuBX1o + EdS2+Uen1AF0ovBOCp9MaVG+UcQWo70ig9KSp0Sp/TiUlh6LUke+00OUOtGzg1Nalqc0 + LOmUp9St6Il/EEr/yXCe8vARlCeylCeOoPzEIcrLn0x5Rf4i6LoS7VcYlFc9Jcqrj0N5 + zbEor813eohyCT3XcspP/o+U1/2NcgwU9pCCwj55hQ3MLD4N+RzFPMc08mLvJ/u/5qER + YDyyU/gk7dDvGHFBpH8Gk4j5XIShZG7BaGgJnPUoTsXjVJxsN51gifQme9vKfrGmcxOY + kTfzQPhA+JEaaXWXZKSKGIKyo09CDzJVAl6gHRgEZoA4sAFsAe8CHwJ3gM+Bktk+M/0A + 4WdAmMXMlshhlJQcfMZ6DNniCJxloWbRfdouVJS7hFCwVzjntkNvFtpae6G7BGEyUtD2 + XDQ0/cZ0UxMe5xfHAgURc33XRHAo3nfqVF986Jmh7kb2oGOm29WzuDUyshXvaRi5ciEw + 1Vvfrr3c1b2mtVf5+/hHCtwKGua1FV+JWymqadkFrRjPfNw7oANlB6DsAM3/hO/AfUCY + BQU2CDVAI9AJXAQ0YBm4AdwC3gM+Au4CXwEls9mqRd/iLRTiKBsO40fZwAB/t1wmMHDC + GQpWVpRb3MwZgtiLqzcL7u37Q6o6lP4u8igw3uFydYwH2AqrHx4ZGWaXH7ezoNQ2FgiM + tUn8XsbKvM/f/RFLhM1LITwZleX+Rxb+CdTGp6ZmZnzDsZWN2PryfHQ8dj12Zmx5cWn9 + T34W9U0KZW5kc3RyZWFtCmVuZG9iagoyMSAwIG9iagoxMTg2CmVuZG9iagoyMiAwIG9i + agooTWFjIE9TIFggMTAuOC40IFF1YXJ0eiBQREZDb250ZXh0KQplbmRvYmoKMjMgMCBv + YmoKKEQ6MjAxMzA4MjkxOTU4MjZaMDAnMDAnKQplbmRvYmoKMSAwIG9iago8PCAvUHJv + ZHVjZXIgMjIgMCBSIC9DcmVhdGlvbkRhdGUgMjMgMCBSIC9Nb2REYXRlIDIzIDAgUiA+ + PgplbmRvYmoKeHJlZgowIDI0CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwNzEyNSAw + MDAwMCBuIAowMDAwMDA1MDEyIDAwMDAwIG4gCjAwMDAwMDA2NjkgMDAwMDAgbiAKMDAw + MDAwNDg2MyAwMDAwMCBuIAowMDAwMDAwMDIyIDAwMDAwIG4gCjAwMDAwMDA2NTAgMDAw + MDAgbiAKMDAwMDAwMDc3MyAwMDAwMCBuIAowMDAwMDAyMDU1IDAwMDAwIG4gCjAwMDAw + MDQ4MjcgMDAwMDAgbiAKMDAwMDAwNTI3NCAwMDAwMCBuIAowMDAwMDAwODg0IDAwMDAw + IG4gCjAwMDAwMDIwMzQgMDAwMDAgbiAKMDAwMDAwMjA5MSAwMDAwMCBuIAowMDAwMDA0 + ODA2IDAwMDAwIG4gCjAwMDAwMDQ5NDYgMDAwMDAgbiAKMDAwMDAwNTE3OCAwMDAwMCBu + IAowMDAwMDA1MDYwIDAwMDAwIG4gCjAwMDAwMDUxNTYgMDAwMDAgbiAKMDAwMDAwNTQ4 + NyAwMDAwMCBuIAowMDAwMDA1NzM0IDAwMDAwIG4gCjAwMDAwMDcwMTAgMDAwMDAgbiAK + MDAwMDAwNzAzMSAwMDAwMCBuIAowMDAwMDA3MDgzIDAwMDAwIG4gCnRyYWlsZXIKPDwg + L1NpemUgMjQgL1Jvb3QgMTUgMCBSIC9JbmZvIDEgMCBSIC9JRCBbIDxmZGRiMmE0NjQ3 + MmU0OGZmNWU4MjA5Mjk0Njg2N2Y0Yz4KPGZkZGIyYTQ2NDcyZTQ4ZmY1ZTgyMDkyOTQ2 + ODY3ZjRjPiBdID4+CnN0YXJ0eHJlZgo3MjAwCiUlRU9GCjEgMCBvYmoKPDwvQXV0aG9y + IChmYWJpbykvQ3JlYXRpb25EYXRlIChEOjIwMTMwODI2MTQwOTAwWikvQ3JlYXRvciAo + T21uaUdyYWZmbGUgUHJvZmVzc2lvbmFsIDUuMi4zKS9Nb2REYXRlIChEOjIwMTMwODI3 + MTMyOTAwWikvUHJvZHVjZXIgMjIgMCBSIC9UaXRsZSAob3JpZ2luYWxzLmdyYWZmbGUp + Pj4KZW5kb2JqCnhyZWYKMSAxCjAwMDAwMDc4MzcgMDAwMDAgbiAKdHJhaWxlcgo8PC9J + RCBbPGZkZGIyYTQ2NDcyZTQ4ZmY1ZTgyMDkyOTQ2ODY3ZjRjPiA8ZmRkYjJhNDY0NzJl + NDhmZjVlODIwOTI5NDY4NjdmNGM+XSAvSW5mbyAxIDAgUiAvUHJldiA3MjAwIC9Sb290 + IDE1IDAgUiAvU2l6ZSAyND4+CnN0YXJ0eHJlZgo4MDE3CiUlRU9GCg== + + QuickLookThumbnail + + TU0AKgAAAJaAP+BP8AQWDQeEQmFQuGQaBv9xN1xABhuB4AB/goFAAXhMCgAUiMOAACAM + Bwx4u51gBXs1zyQAycUB4FxiBsBgNAAOgCR8MBQGgAPgiCPl/zIQyOFr5eM0APgQBYAC + 4Bv4APYBgGbQJoNBsgAGhSageFPJ8P0ACoSB+GNZuN0AOZzPcAPIB2QZB8EgCAgAAA8B + AAADAAAAAQANAAABAQADAAAAAQAEAAABAgADAAAABAAAAVABAwADAAAAAQAFAAABBgAD + AAAAAQACAAABEQAEAAAAAQAAAAgBEgADAAAAAQABAAABFQADAAAAAQAEAAABFgADAAAA + AQAEAAABFwAEAAAAAQAAAI0BHAADAAAAAQABAAABPQADAAAAAQACAAABUgADAAAAAQAB + AAABUwADAAAABAAAAViHcwAHAAAatAAAAWAAAAAAAAgACAAIAAgAAQABAAEAAQAAGrRh + cHBsAhAAAG1udHJSR0IgWFlaIAfdAAcABQATADIAOmFjc3BBUFBMAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD21gABAAAAANMtYXBwbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWRlc2MAAAFQAAAAYmRzY20AAAG0AAADuGNw + cnQAAAVsAAAAJHd0cHQAAAWQAAAAFHJYWVoAAAWkAAAAFGdYWVoAAAW4AAAAFGJYWVoA + AAXMAAAAFHJUUkMAAAXgAAAIDGFhcmcAAA3sAAAAIHZjZ3QAAA4MAAAGEm5kaW4AABQg + AAAGPmNoYWQAABpgAAAALG1tb2QAABqMAAAAKGJUUkMAAAXgAAAIDGdUUkMAAAXgAAAI + DGFhYmcAAA3sAAAAIGFhZ2cAAA3sAAAAIGRlc2MAAAAAAAAACERpc3BsYXkAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtbHVjAAAAAAAAAB4AAAAMc2tTSwAAABYAAAF4 + Y2FFUwAAABgAAAGOaGVJTAAAABYAAAGmcHRCUgAAABgAAAG8aXRJVAAAABQAAAHUaHVI + VQAAABQAAAHodWtVQQAAABwAAAH8a29LUgAAAAwAAAIYbmJOTwAAABIAAAIkY3NDWgAA + ABYAAAI2emhUVwAAAAwAAAJMZGVERQAAABAAAAJYcm9STwAAABIAAAJoc3ZTRQAAABAA + AAJ6emhDTgAAAAwAAAJMamFKUAAAAA4AAAKKYXIAAAAAABQAAAKYZWxHUgAAACIAAAKs + cHRQVAAAABYAAALObmxOTAAAABYAAALkZnJGUgAAABYAAAL6ZXNFUwAAABIAAAJodGhU + SAAAAAwAAAMQdHJUUgAAABQAAAMcZmlGSQAAABAAAAMwaHJIUgAAABQAAANAcGxQTAAA + ABIAAANUcnVSVQAAACQAAANmZW5VUwAAABIAAAOKZGFESwAAABwAAAOcAEYAYQByAGUA + YgBuAOkAIABMAEMARABMAEMARAAgAGUAbgAgAGMAbwBsAG8AciAPAEwAQwBEACAF5gXR + BeIF1QXgBdkATABDAEQAIABDAG8AbABvAHIAaQBkAG8ATABDAEQAIABjAG8AbABvAHIA + aQBTAHoA7QBuAGUAcwAgAEwAQwBEBBoEPgQ7BEwEPgRABD4EMgQ4BDkAIABMAEMARM7s + t+wAIABMAEMARABGAGEAcgBnAGUALQBMAEMARABCAGEAcgBlAHYAbgD9ACAATABDAERf + aYJyACAATABDAEQARgBhAHIAYgAtAEwAQwBEAEwAQwBEACAAYwBvAGwAbwByAEYA5ABy + AGcALQBMAEMARDCrMOkw/AAgAEwAQwBEIA8ATABDAEQAIAZFBkQGSAZGBikDiAOzA8cD + wQPJA7wDtwAgA78DuAPMA70DtwAgAEwAQwBEAEwAQwBEACAAYQAgAEMAbwByAGUAcwBL + AGwAZQB1AHIAZQBuAC0ATABDAEQATABDAEQAIABjAG8AdQBsAGUAdQByAEwAQwBEACAO + Kg41AFIAZQBuAGsAbABpACAATABDAEQAVgDkAHIAaQAtAEwAQwBEAEwAQwBEACAAdQAg + AGIAbwBqAGkASwBvAGwAbwByACAATABDAEQEJgQyBDUEQgQ9BD4EOQAgBBYEGgAtBDQE + OARBBD8EOwQ1BDkAQwBvAGwAbwByACAATABDAEQATABDAEQALQBmAGEAcgB2AGUAcwBr + AOYAcgBtdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUsIEluYy4sIDIwMTMAWFlaIAAAAAAA + APNSAAEAAAABFs9YWVogAAAAAAAAansAADcfAAABw1hZWiAAAAAAAABl1gAAut0AAAgK + WFlaIAAAAAAAACaFAAAOBAAAyWBjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgA + LQAyADYAOwBAAEUASgBPAFQAWQBeAGMAaABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8AowCo + AK0AsgC3ALwAwQDGAMsA0ADVANsA4ADlAOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIB + OAE+AUUBTAFSAVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGpAbEBuQHBAckB0QHZAeEB6QHy + AfoCAwIMAhQCHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2AsECywLVAuAC + 6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQT + BCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcF + hgWWBaYFtQXFBdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcr + Bz0HTwdhB3QHhgeZB6wHvwfSB+UH+AgLCB8IMghGCFoIbgiCCJYIqgi+CNII5wj7CRAJ + JQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9ClQKagqBCpgKrgrFCtwK8wsLCyILOQtR + C2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0NDSYNQA1aDXQNjg2pDcMN + 3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBDEGEQfhCb + ELkQ1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QT + xRPlFAYUJxRJFGoUixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcd + F0EXZReJF64X0hf3GBsYQBhlGIoYrxjVGPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa + 7BsUGzsbYxuKG7Ib2hwCHCocUhx7HKMczBz1HR4dRx1wHZkdwx3sHhYeQB5qHpQevh7p + HxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1IaEhziH7IiciVSKCIq8i3SMKIzgj + ZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtyboJxgnSSd6J6sn3CgN + KD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizXLQwt + QS12Last4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKb + MtQzDTNGM38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4 + jDjIOQU5Qjl/Obw5+To2OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6g + PuA/IT9hP6I/4kAjQGRApkDnQSlBakGsQe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJF + VUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1JY0mpSfBKN0p9SsRLDEtTS5pL4kwq + THJMuk0CTUpNk03cTiVObk63TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIxUnxSx1MTU19T + qlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtF + W5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Nj + l2PrZEBklGTpZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/ + bFdsr20IbWBtuW4SbmtuxG8eb3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1 + KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnnekZ6pXsEe2N7wnwhfIF84X1BfaF+AX5i + fsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VHhauGDoZyhteHO4efiASI + aYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/kaiSEZJ6 + kuOTTZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPed + ZJ3SnkCerp8dn4uf+qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhS + qMSpN6mpqhyqj6sCq3Wr6axcrNCtRK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660 + JbSctRO1irYBtnm28Ldot+C4WbjRuUq5wro7urW7LrunvCG8m70VvY++Cr6Evv+/er/1 + wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dBx7/IPci8yTrJuco4yrfLNsu2zDXM + tc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV1tjXXNfg2GTY6Nls + 2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN5pbn + H+ep6DLovOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC + 9VD13vZt9vv3ivgZ+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23//3BhcmEAAAAAAAMA + AAACZmYAAPKnAAANWQAAE9AAAAoOdmNndAAAAAAAAAAAAAMBAAACAAAAAwANACAAPABi + AJMAzgESAVgBqAICAm0C4wNlA/IEiwUwBeAGnQdqCEQJJgoOCwIMAw0RDisPSRByEaES + 3RQeFWIWshgEGWAawBwfHYke8SBdIcsjOiSlJhQngCjnKk4rry0OLmMvszD7MjszczSf + NcQ24Tf5OQw6GjsqPDg9RD5OP1pAaEF2QoZDlkSoRbpGykfaSOhJ90sETBBNHE4nTzFQ + OlFCUklTTlRSVVVWV1dYWFlZWlpaW1pcWl1aXlhfVmBSYU1iRmM+ZDVlKmYfZxNoB2j7 + ae5q4WvUbMdtuW6rb51wjXF5cmFzQXQZdO51wXaXd294Snkjef1613uvfIh9YH43fw5/ + 5YC7gZKCaIM9hBOE6IW+hpOHaIg8iRaJ/YsEjD6NrY84kLKR/ZMflCuVLpYwlzGYM5k0 + mjWbNZw2nTqeRp9loJih2KMVpEGlXaZup3qohqmRqpurpayvrbmuw6/SsO2yIbNwtNC2 + KbdsuJi5trrPu+e8/r4WvzHAVsGSwujESMWbxs3H4cjwygbLJcxHzWTOc89s0FXRN9Ie + 0wrT+dTj1c3Wv9e62LzZv9rB277cr92R3mzfQuAZ4PLh0OK546jkm+WQ5oXneeht6WDq + Tesz7BLs6+3G7qTvhvBq8U7yNPMi9Cn1Xfa++DD5lPrf/Bn9WP6k//8AAAACAAsAGwAy + AFIAegCsAOYBJwFvAcACGwKCAvIDcAP4BIgFIQXFBnQHLgfzCMIJnQp9C2QMWA1TDlgP + YhB1EYsSqBPQFPgWJxddGJMZzxsNHEwdjB7OIA0hUSKRI9AlCyZFJ3YopinRKvQsEC0o + LjQvOTA5MTMyKTMeNBA1ATX0NuU31zjIObk6qTuaPIs9ez5qP1tATkFCQjdDLkQkRRpG + EEcGR/tI8EnlSthLzEy+TbBOok+TUIRRc1JhU1BUPlUrVhhXBVfxWNxZyFqzW59ciV1z + Xl1fRWAsYRNh+GLdY8FkpGWIZmtnTWgvaRFp82rVa7ZsmG15blhvNnAScOlxv3KTc2R0 + NXUFddZ2pXd1eEV5FHnjerJ7gHxOfRx96n66f42AaoFSgkSDPYQ6hTaGMIcriCWJH4oZ + ixKMC40Ejf2O9o/xkPKR+ZMHlBiVKJY3l0SYUJlbmmebcpx9nYeekp+coKehuKLSo/ml + KaZcp4uotanaqv6sIa1FrmivjbC3se6zNbSFtdG3Dbg9uWm6mbvLvP++Nb9swKTB3MMR + xEDFacaLx6rIyMnpyw3MM81azoLPrNDX0gbTNNRf1YTWpNfE2ObaCtsv3FTded6e38fg + 9+Ix43Lks+Xy5yzoZ+mj6t/sG+1Y7pzv8PF28y31EvcK+QL66/y7/mn//wAAAAIACgAX + ACwASABrAJcAygEGAUcBkQHjAj0CoQMNA4UEBwSSBSYFwAZkBxIHyQiICU4KHwr5C9QM + uA2lDpkPkhCPEZISmhOlFLYVyBbeF/kZEhotG0YcYR18HpcfriDCIdQi5SPtJPIl8Cbo + J9gowimlKoMrWiwvLQEt0i6kL3cwSjEgMfcyzjOmNH41VTYsNwI32DitOYI6VjsqO/08 + 0D2jPnc/SkAfQPVBzEKlQ35EVkUuRgVG3Ee0SIpJYEo2SwtL4Ey1TYlOXU8xUAVQ2VGs + UoBTU1QlVPZVyFaaV2tYO1kLWdtaq1t6XEhdFl3kXrFff2BLYRdh42KuY3pkRmURZdtm + pmdxaDtpBGnOapdrYGwpbPFtuG6Ab0ZwCXDMcYxySnMIc8V0gnU+dft2t3dzeC546Xmk + el97GnvUfI59SH4Cfr5/foBIgR2B+4Lgg8iEsYWbhoSHbYhWiT6KJosNi/WM3I3DjqqP + lJCBkXeScpNwlHGVcZZxl3GYcJlummmbYJxTnUqeTJ9doHmhnaLFo+6lFaY3p0yoUqlR + qlure6y0rfuvR7CTsd+zK7R2tcG3C7hTuZi627whvWy+vcATwWrCw8QbxXTGzsgmyX3K + 1MwqzYLO49BW0dbTY9T11ojYGtmt20Dc1t6A4E3iU+SQ5vfplOzF8VT32f//AABuZGlu + AAAAAAAABjYAAKVGAABWlAAAUzkAAKWOAAAlqAAADTwAAFANAABUOQACGZkAAb1wAAFc + KAADAQAAAgAAAB4AQQBiAIMApADFAOYBBgEnAUkBawGNAa8B0QH0AhcCOwJfAoMCqALO + AvMDGgNBA2gDkAO4A+IECwQ2BGEEjQS5BOcFFQVFBXUFpgXYBgwGQQZ3Bq4G6AciB18H + ngfeCCIIaAiwCP0JTAmgCfYKUAqtCw0LbgvSDDgMoQ0LDXYN4w5RDsEPMg+kEBkQkBEJ + EYUSAhKCEwUTiRQQFJkVJRWzFkQW2BdvGAgYpBlDGeQahxstG9Ucfx0sHdwejh9EH/0g + uiF6Ij4jBSPPJJwlayY9JxIn6ijEKaIqgitlLEwtOS4uLy8wPjFVMmwzgzSaNbU20zf2 + OR06Rzt2PKk93z8ZQFdBmULfRChFdUbGSBpJZ0qVS5dMdk1ATf9OwU+MUGpRaVKEU7BU + 5FYaV1NYjlnMWw1cUV2WXthgD2EzYkdjVWRkZX9mq2foaTFqgWvTbShugG/acTdylnP4 + dVd2rHfteRd6MHtCfFl9f365gAmBaILNhDaFoocPiH6J54tBjISNtY7gkA6RTpKwlDSV + xZdOmM2aSJvGnVGe+KDIor6ksqaXqHqqaaxbrkGwFbHhs621f7deuVq7fL23v/nCOcRt + xorImcqkzK/OvtDR0ufVCNc92Y3b9N5a4LXjC+Vh57rqCOwn7gDvnfEe8pj0HPW29235 + Nfr8/Lb+X///AAAAJABLAHEAlgC8AOIBCAEuAVQBegGiAckB8QIZAkICawKVAsAC6wMW + A0MDcAOdA8wD+wQrBFwEjgTBBPUFKgVhBZgF0QYLBkcGhQbEBwYHSgePB9gIJAhzCMUJ + HAl3CdYKOQqgCwoLdwvnDFgMzQ1DDbwOOA62DzYPuRA/EMYRTxHZEmUS8hOCFBQUqBU/ + FdkWdRcUF7YYWhkCGawaWRsJG7wccx0sHegepx9pIC4g9iHBIo8jXyQzJQol5CbCJ6Qo + iSlzKmArUSxFLTwuNy81MDYxOjJCM000WzVuNoY3pjjPOf47NDxvPa4+8kA5QYRC00Qn + RX9G20g7SZ1K/UxQTZNOyk/8US5SZFOeVNpWGldcWKFZ6ls1XINd018lYHNhvGL+ZD1l + fWbBaAlpVmqla/dtTG6jb/xxWXK4dBl1fHbaeDB5e3q9e/p9On6Bf86BIoJ5g9OFMIaO + h+6JToqqi/uNQI58j7WQ9ZJEk6KVCZZyl9iZPpqjnAidb57WoD2hpqMUpImmBqeOqR6q + tKxNreOveLELsp+0NLXKt2C49bqIvBy9tb9XwQLCtcRpxhzHzsmAyzTM6s6j0FvSEdO/ + 1WPXANib2jnb3d2H3zLg3+KM5Dvl7Oee6U7q9eyI7fnvSfCF8bHyzvPo9P/2GPcz+FT5 + fPqq++r9OP6V//8AAAApAFQAfwCpANMA/gEpAVQBgAGsAdgCBgI0AmICkQLBAvIDIwNW + A4kDvQPzBCkEYASZBNMFDwVMBYsFywYNBlIGmQbiBy0HfQfPCCYIgQjhCUcJsgokCpsL + FguVDBYMmg0fDaYOMA68D0wP3xB1EQ8RrBJNEvETmRREFPIVoxZWFwsXwRh6GTYZ9hq5 + G38cSR0XHegevR+VIHIhUSI0IxokBCTyJeMm2SfSKM8p0CrVK94s6y38LxIwLDFKMmwz + kzS+Ne03IDhXOZI60jwVPV0+qj/7QVBCqkQJRW1G10hKScZLTEzYTmlQAFGbUzxU4laO + WD9Z9VuxXXJfNmDyYptkK2WtZypopmoma6ltL265cEdx2HNtdQZ2onhBed97eH0Gfo6A + FIGZgyKErYY7h82JY4sBjKuOXJAGkZuTHpSVlgeXd5jnmlqb051YnvSgqaJlpAeliqb2 + qFiptasUrHSt1q85sJ6yBbNutNm2R7e5uTC6qrwkvZu/DsB9werDV8TExjLHockSyoTL + 981szuTQXdHZ01fU19ZU18nZMtqS2+vdPt6O393hLeJ+49DlI+Z4583pI+p268Ds/u4x + 70rwWfFZ8k7zPPQj9Qf14va694D4Pfjw+YH6Evqg+wn7cfvZ/EL8mfzo/Tf9hf3U/iP+ + af6t/vD/NP94/7v//wAAc2YzMgAAAAAAAQxCAAAF3v//8yYAAAeSAAD9kf//+6L///2j + AAAD3AAAwGxtbW9kAAAAAAAABhAAAJzFAAAAAMZ644AAAAAAAAAAAAAAAAAAAAAA + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 1 + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + + name + Canvas 1 + + + Frame + {{320, -1}, {693, 774}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{0, 0}, {558, 619}} + Zoom + 1 + ZoomValues + + + Canvas 1 + 1 + 1 + + + + saveQuickLookFiles + YES + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..75bce0e --- /dev/null +++ b/pom.xml @@ -0,0 +1,270 @@ + + 4.0.0 + + maven-parent + org.gcube.tools + 1.0.0 + + + + org.gcube.core + common-smartgears + 2.1.0-SNAPSHOT + SmartGears + + + distro + 7.0.42 + 1.17.1 + UTF-8 + + + + scm:svn:http://svn.d4science.research-infrastructures.eu/gcube/trunk/Common/${project.artifactId} + scm:svn:https://svn.d4science.research-infrastructures.eu/gcube/trunk/Common/${project.artifactId} + http://svn.d4science.research-infrastructures.eu/gcube/trunk/Common/${project.artifactId} + + + + + + org.gcube.common + authorization-client + [2.0.0-SNAPSHOT,3.0.0-SNAPSHOT) + + + + org.gcube.common + common-authorization + [2.0.0-SNAPSHOT,3.0.0-SNAPSHOT) + + + + org.gcube.data.publishing + document-store-lib + [1.0.0-SNAPSHOT,2.0.0-SNAPSHOT) + + + + org.gcube.accounting + accounting-lib + [2.0.0-SNAPSHOT,3.0.0-SNAPSHOT) + + + + org.slf4j + slf4j-api + 1.7.5 + + + + org.gcube.resources + registry-publisher + [1.0.0-SNAPSHOT, 2.0.0-SNAPSHOT) + + + + org.gcube.resources + common-gcore-resources + [1.0.0-SNAPSHOT, 2.0.0-SNAPSHOT) + + + + org.gcube.core + common-validator + [1.0.0-SNAPSHOT,2.0.0-SNAPSHOT) + + + + org.gcube.core + common-scope + [1.0.0-SNAPSHOT, 2.0.0-SNAPSHOT) + + + + org.gcube.core + common-events + [1.0.0-SNAPSHOT,2.0.0-SNAPSHOT) + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + + + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-depchain + 2.0.0-beta-2 + pom + test + + + + com.sun.jersey + jersey-client + ${jersey.version} + test + + + + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + test + + + + org.apache.tomcat.embed + tomcat-embed-logging-log4j + ${tomcat.version} + test + + + + org.apache.tomcat.embed + tomcat-embed-jasper + ${tomcat.version} + test + + + + junit + junit + 4.10 + test + + + + ch.qos.logback + logback-classic + 1.0.13 + runtime + + + + org.mockito + mockito-core + 1.9.0 + test + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2 + + + ${distroDirectory}/descriptor.xml + + + + + servicearchive + install + + single + + + + + + + + maven-jar-plugin + 2.3.2 + + + default-jar + package + + jar + + + + **/probe/**/* + + + + + + + + + org.apache.maven.plugins + maven-war-plugin + 2.4 + + false + probe + + WEB-INF/classes/org/gcube/smartgears/probe/**/* + false + + + + probe-war + package + + war + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.15 + + + false + + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + + + copy-configuration + + copy-resources + + validate + + src/main/resources/META-INF + true + UTF-8 + + + ${distroDirectory} + + smartgears-config.xml + + true + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/Bootstrap.java b/src/main/java/org/gcube/smartgears/Bootstrap.java new file mode 100644 index 0000000..1428436 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/Bootstrap.java @@ -0,0 +1,149 @@ +package org.gcube.smartgears; + +import java.util.Set; + +import javax.servlet.ServletContainerInitializer; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.managers.ApplicationManager; +import org.gcube.smartgears.managers.ContainerManager; +import org.gcube.smartgears.provider.ProviderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Bootstraps management of all deployed applications which require it. + * + * @author Fabio Simeoni + * + */ +public class Bootstrap implements ServletContainerInitializer { + + private static Logger log = LoggerFactory.getLogger(Bootstrap.class); + + private static boolean smartgearsHasStarted = false; + + private static boolean containerHasFailed = false; + + private static ContainerManager manager; + + private static ContainerContext context; + + public Bootstrap() { + + if (smartgearsHasStarted) + return; + + smartgearsHasStarted = true; + + initialiseContainer(); + + //this can fail the app: managed resources need a working container + startContainerIfItHasntAlreadyFailed(); + + } + + @Override + public void onStartup(Set> c, ServletContext application) throws ServletException { + + ApplicationManager appManager = new ApplicationManager(); + + //act only on resources + if (isResource(application)) { + + try { + + log.info("starting management of application @ {}", application.getContextPath()); + + ApplicationContext app = appManager.start(context, application); + + manager.manage(app); + + context.configuration().app(app.configuration()); + + } catch (Throwable t) { + + appManager.stop(); + + throw new ServletException("cannot manage application @ " + application.getContextPath() + + " (see cause)", t); + + } + } + } + + // helpers + private void initialiseContainer() { + + try { + + log.trace("smartgears is starting"); + + + /* Get the ContainerContext. Look at DefaultProvider */ + context = ProviderFactory.provider().containerContext(); + + /* Validate the configuration retrieved by ContainerContext + * using gcube facilities annotation based + * ( i.e org.gcube.common.validator.annotations) + */ + context.configuration().validate(); + + } catch (RuntimeException e) { + + containerHasFailed = true; + + log.error("cannot start smartgears", e); + + //we let the container continue + + } + } + + private void startContainerIfItHasntAlreadyFailed() { + + if (containerHasFailed) + throw new IllegalStateException("container is not managed due to previous failure"); + + + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + + + // we initialise the container in the same classloader as this + // lib, lest container bind its resources to the current webapp + try { + + // TODO Ask why is needed? + Thread.currentThread().setContextClassLoader(ContainerManager.class.getClassLoader()); + + manager = ContainerManager.instance; + + context = manager.start(context); + + } catch (RuntimeException e) { + + containerHasFailed = true; + + throw new IllegalStateException("cannot manage container", e); + + } + + finally {//restore the classloader of the current application + Thread.currentThread().setContextClassLoader(contextCL); + } + + + } + + private boolean isResource(ServletContext application) { + + //with care: smartgears may have already failed at this stage but we want to recognise + //apps that would have been managed otherwise and give specific errors for those + return (!containerHasFailed && context.configuration().app(application.getContextPath())!=null) + || + application.getResourceAsStream(Constants.configuration_file_path) != null; + } +} diff --git a/src/main/java/org/gcube/smartgears/Constants.java b/src/main/java/org/gcube/smartgears/Constants.java new file mode 100644 index 0000000..684d3de --- /dev/null +++ b/src/main/java/org/gcube/smartgears/Constants.java @@ -0,0 +1,222 @@ +package org.gcube.smartgears; + +import org.gcube.smartgears.extensions.resource.RemoteResource; +import org.gcube.smartgears.handlers.application.lifecycle.ProfileManager; +import org.gcube.smartgears.handlers.application.request.RequestValidator; +import org.gcube.smartgears.handlers.container.lifecycle.AccountingManager; + +/** + * Library-wide constants. + * + * + * @author Fabio Simeoni + * + */ +public class Constants { + + /** + * The environment variable that points to the container configuration directory. + */ + public static final String ghn_home_env = "GHN_HOME"; + + /** + * The system property that points to the container configuration directory. + */ + public static final String ghn_home_property = "ghn.home"; + + + /** + * The container configuration file path, relative to the container configuration directory. + */ + public static final String container_configuraton_file_path = "container.xml"; + + + /** + * The path of the application profile file, relative to the container configuration directory. + */ + public static final String container_profile_file_path = "ghn.xml"; + + public static final String container_profile_file_path_copy = "ghn.xml.copy"; + + /** + * The container lifecycle configuration resource path. + */ + public static final String container_handlers_file_path = "/META-INF/container-handlers.xml"; + + /** + * The library configuration resource path. + */ + public static final String library_configuration_file_path = "/META-INF/smartgears-config.xml"; + + /** + * The name of the context property that contains the node profile. + */ + public static final String container_profile_property = "ghn-profile"; + + + /** + * The default value of for the container publication frequency. + */ + public static final long default_container_publication_frequency_in_seconds = 60; + + + + + /** + * The application configuration resource path. + */ + public static final String configuration_file_path = "/WEB-INF/gcube-app.xml"; + + /** + * The application lifecycle configuration resource path. + */ + public static final String handlers_file_path = "/WEB-INF/gcube-handlers.xml"; + + /** + * The default application lifecycle configuration resource path. + */ + public static final String default_handlers_file_path = "/META-INF/default-handlers.xml"; + + + /** + * The wildcard exclude directive. + */ + public static final String EXCLUDE_ALL = "*"; + + + /** + * The mapping root of all extensions. + */ + public static final String root_mapping = "/gcube/resource"; + + /** + * The application extensions configuration resource path. + */ + public static final String extensions_file_path = "/WEB-INF/gcube-extensions.xml"; + + /** + * The default application extensions configuration resource path. + */ + public static final String default_extensions_file_path = "/META-INF/default-extensions.xml"; + + /** + * The application frontpage resource path. + */ + public static final String frontpage_file_path = "/META-INF/frontpage.html"; + + /** + * The configuration name of {@link ProfileManager}s. + */ + public static final String profile_management = "profile-management"; + + /** + * The configuration name of {@link RequestValidator}s. + */ + public static final String request_validation = "request-validation"; + + + /** + * The configuration name of {@link AccountingManager}s. + */ + public static final String accounting_management = "accounting-management"; + + /** + * The configuration name of {@link RequestAccounting}s. + */ + public static final String request_accounting = "request-accounting"; + + + /** + * The configuration name of {@link RemoteResource}s. + */ + public static final String remote_management = "remote-management"; + + + + + /** + * The path of the application profile file, relative to the service configuration directory. + */ + public static final String profile_file_path = "endpoint.xml"; + + /** + * The name of the context property that contains the endpoint profile. + */ + public static final String profile_property = "endpoint-profile"; + + + /** + * The name of the attribute in the servlet context that contains the context of an application. + */ + public static final String context_attribute ="gcube-application-context"; + + /** + * The name of the HTTP header that contains the scope of requests + */ + public static final String scope_header="gcube-scope"; + + /** + * The name of the HTTP header that contains the authorization token of requests + */ + public static final String token_header="gcube-token"; + + /** + * The event for token registration for app. + */ + public static final String token_registered = "token-registered"; + + /** + * The event for token removal for app. + */ + public static final String token_removed = "token-removed"; + + /** + * The name of the HTTP header for standard HTTP basic authorization + */ + public static final String authorization_header ="Authorization"; + + /** + * The name of the HTTP header that contains the called method of the current request + */ + public static final String called_method_header="gcube-method"; + + /** + * The name of the Content-Type HTTP header + */ + public static final String content_type="Content-Type"; + + /** + * The name of the Accept HTTP header + */ + public static final String accept="Accept"; + + /** + * The name of the Allow HTTP header + */ + public static final String allow="Allow"; + + + /** + * The name of the XML media type. + */ + public static final String plain_text="text/plain"; + + /** + * The name of the XML media type. + */ + public static final String application_xml="application/xml"; + + + /** + * The name of the XHTML media type. + */ + public static final String application_xhtml="application/xhtml+xml"; + + /** + * The name of the Json media type. + */ + public static final String application_json="application/json"; + + public static final int application_republish_frequency_in_minutes = 20; + +} diff --git a/src/main/java/org/gcube/smartgears/configuration/Mode.java b/src/main/java/org/gcube/smartgears/configuration/Mode.java new file mode 100644 index 0000000..c02b97a --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/Mode.java @@ -0,0 +1,12 @@ +package org.gcube.smartgears.configuration; + +/** + * The management mode the container or its applications. + * + * @author Fabio Simeoni + * + */ +public enum Mode { + online, + offline +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/configuration/application/ApplicationConfiguration.java b/src/main/java/org/gcube/smartgears/configuration/application/ApplicationConfiguration.java new file mode 100644 index 0000000..b8323c4 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/application/ApplicationConfiguration.java @@ -0,0 +1,169 @@ +package org.gcube.smartgears.configuration.application; + +import java.util.Set; + +import org.gcube.smartgears.configuration.Mode; +import org.gcube.smartgears.persistence.Persistence; + +/** + * The configuration of the application. + * + * @author Fabio Simeoni + * + */ +public interface ApplicationConfiguration { + + + /** + * Returns the management mode of the application. + * @return the management mode + */ + Mode mode(); + + /** + * Returns true if the application is secure (accessible only in https). + * @return secure or not + */ + boolean secure(); + + /** + * Returns true if the application is behind a proxy. + * @return secure or not + */ + ApplicationConfiguration proxied(boolean proxied); + + /** + * Returns the context path of the application + * @return the context path + */ + String context(); + + boolean proxied(); + + /** + * Sets the context path of the application + * @param context the context path + * @return this configuration + */ + ApplicationConfiguration context(String context); + + /** + * Sets the management mode of this application. + * @param the management mode + * @return this configuration + */ + ApplicationConfiguration mode(Mode mode); + + + ApplicationConfiguration secure(boolean value); + + /** + * Returns the name of the application. + * @return the name + */ + String name(); + + + /** + * Sets the name of the application. + * @param name the name + * @return this configuration + */ + ApplicationConfiguration name(String name); + + /** + * Returns the class of the application + * @return the class + */ + String serviceClass(); + + /** + * Sets the class of the application. + * @param serviceClass the class + * @return this configuration + */ + ApplicationConfiguration serviceClass(String serviceClass); + + /** + * Returns the version of the application. + * @return the version + */ + String version(); + + /** + * Sets the version of the application. + * @param version the version + * @return this configuration + */ + ApplicationConfiguration version(String version); + + /** + * Returns the description of the application. + * @return the description + */ + String description(); + + /** + * Sets the description of the application. + * @param description the description + * @return this configuration + */ + ApplicationConfiguration description(String description); + + + /** + * Returns the tokens in which the application operates when it first starts. + * @return the tokens + */ + Set startTokens(); + + /** + * Sets the tokens in which the application operates when it first starts. + * @param scopes the scopes + * @return this configuration + */ + ApplicationConfiguration startTokens(Set tokens); + + + /** + * Returns the persistence manager of the application. + * @return the manager + */ + Persistence persistence(); + + + /** + * Returns a set of request paths that should not be subjected to request management. + * @return the set of exclude paths. + */ + Set excludes(); + + + /** + * Sets the persistence manager of the application. + * @param manager the manager + * @return this configuration + */ + ApplicationConfiguration persistence(Persistence manager); + + + /** + * Validates this configuration. + * + * @throws IllegalStateException if the configuration is not valid + */ + void validate(); + + + /** + * Merges this configuration with another configuration + * @param config the other configuration + */ + void merge(ApplicationConfiguration config); + + ApplicationConfiguration excludes(Exclude ... excludes); + + + + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/configuration/application/ApplicationConfigurationBinder.java b/src/main/java/org/gcube/smartgears/configuration/application/ApplicationConfigurationBinder.java new file mode 100644 index 0000000..9a81583 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/application/ApplicationConfigurationBinder.java @@ -0,0 +1,149 @@ +package org.gcube.smartgears.configuration.application; + +import static org.gcube.smartgears.utils.Utils.*; + +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.ServiceLoader; +import java.util.Set; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; + +import org.gcube.smartgears.extensions.ApplicationExtension; +import org.gcube.smartgears.handlers.application.ApplicationHandler; + +/** + * Binds {@link ApplicationConfiguration}s to and from XML serialisations. + * + * @author Fabio Simeoni + * + */ +public class ApplicationConfigurationBinder { + + /** + * Returns the application configuration from its XML serialisation. + * + * @param stream the serialisation + * @return the configuration + * @throws RuntimeException if the serialisation is invalid + */ + public ApplicationConfiguration bind(InputStream stream) { + + try { + + JAXBContext ctx = JAXBContext.newInstance(DefaultApplicationConfiguration.class); + + return (ApplicationConfiguration) ctx.createUnmarshaller().unmarshal(stream); + + } catch (JAXBException e) { + + throw new RuntimeException("invalid service configuration", e); + + } + finally { + closeSafely(stream); + } + } + + /** + * Returns the handlers of the application from their XML serialisation. + * + * @param stream the serialisation + * @return the handlers + * @throws RuntimeException if the serialisation is invalid + */ + public ApplicationHandlers bindHandlers(InputStream stream) { + + //collects handler classes + Set> classes = scanForHandlers(); + + try { + + JAXBContext ctx = JAXBContext.newInstance(classes.toArray(new Class[0])); + + return (ApplicationHandlers) ctx.createUnmarshaller().unmarshal(stream); + + } catch (JAXBException e) { + + throw unchecked(e); + + } + finally { + closeSafely(stream); + } + } + + /** + * Returns the extensions of the application from their XML serialisation. + * + * @param stream the serialisation + * @return the extensions + * @throws RuntimeException if the serialisation is invalid + */ + public ApplicationExtensions bindExtensions(InputStream stream) { + + //collects handler classes + Set> classes = scanForExtensions(); + + try { + + JAXBContext ctx = JAXBContext.newInstance(classes.toArray(new Class[0])); + + return (ApplicationExtensions) ctx.createUnmarshaller().unmarshal(stream); + + } catch (JAXBException e) { + + throw unchecked(e); + + } + finally { + closeSafely(stream); + } + } + + + + private Set> scanForHandlers() throws RuntimeException { + + @SuppressWarnings("all") + ServiceLoader handlerLoader = (ServiceLoader) ServiceLoader.load(ApplicationHandler.class); + + Set> scanned = new HashSet>(); + + for (ApplicationHandler handler : handlerLoader) { + Class handlerClass = handler.getClass(); + if (handlerClass.isInterface() || handlerClass.getModifiers() == Modifier.ABSTRACT) + continue; + else + scanned.add(handlerClass); + } + + //add top-level configuration + scanned.add(ApplicationHandlers.class); + + return scanned; + } + + private Set> scanForExtensions() throws RuntimeException { + + @SuppressWarnings("all") + ServiceLoader handlerLoader = (ServiceLoader) ServiceLoader.load(ApplicationExtension.class); + + Set> scanned = new HashSet>(); + + for (ApplicationExtension handler : handlerLoader) { + Class handlerClass = handler.getClass(); + if (handlerClass.isInterface() || handlerClass.getModifiers() == Modifier.ABSTRACT) + continue; + else + scanned.add(handlerClass); + } + + //add top-level configuration + scanned.add(ApplicationExtensions.class); + + return scanned; + } +} diff --git a/src/main/java/org/gcube/smartgears/configuration/application/ApplicationExtensions.java b/src/main/java/org/gcube/smartgears/configuration/application/ApplicationExtensions.java new file mode 100644 index 0000000..53496de --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/application/ApplicationExtensions.java @@ -0,0 +1,75 @@ +package org.gcube.smartgears.configuration.application; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.validator.ValidationError; +import org.gcube.common.validator.Validator; +import org.gcube.common.validator.ValidatorFactory; +import org.gcube.common.validator.annotations.IsValid; +import org.gcube.smartgears.extensions.ApplicationExtension; +import org.w3c.dom.Element; + +/** + * The {@link ApplicationExtension}s that manage the application. + * + * @author Fabio Simeoni + * + */ +@XmlRootElement(name="extensions") +public class ApplicationExtensions { + + @XmlAnyElement(lax=true) @IsValid + List extensions = new ArrayList(); + + public ApplicationExtensions() {} + + /** + * Returns the extensions for the application. + * @return the extensions + */ + public List extensions() { + return extensions; + } + + /** + * Sets the extensions for the application. + * @param extensions the extensions + * @return this configuration + */ + public ApplicationExtensions set(ApplicationExtension ... extensions) { + this.extensions = Arrays.asList(extensions); + return this; + } + + @Override + public String toString() { + return extensions.toString(); + } + + public void validate() { + + List msgs = new ArrayList(); + + Validator validator = ValidatorFactory.validator(); + + for (ValidationError error : validator.validate(this)) + msgs.add(error.toString()); + + if (!msgs.isEmpty()) + throw new IllegalStateException("invalid configuration: "+msgs); + + } + + //since we use @AnyElement, after deserialisation, we check there are no DOM elements + void afterUnmarshal(Unmarshaller u, Object parent) { + for (Object o : extensions) + if (o instanceof Element) + throw new RuntimeException("invalid extensions detected: "+Element.class.cast(o).getLocalName()); + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/configuration/application/ApplicationHandlers.java b/src/main/java/org/gcube/smartgears/configuration/application/ApplicationHandlers.java new file mode 100644 index 0000000..3834e38 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/application/ApplicationHandlers.java @@ -0,0 +1,141 @@ +package org.gcube.smartgears.configuration.application; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.validator.ValidationError; +import org.gcube.common.validator.Validator; +import org.gcube.common.validator.ValidatorFactory; +import org.gcube.common.validator.annotations.IsValid; +import org.gcube.smartgears.handlers.application.ApplicationHandler; +import org.gcube.smartgears.handlers.application.ApplicationLifecycleHandler; +import org.gcube.smartgears.handlers.application.RequestHandler; +import org.w3c.dom.Element; + +/** + * The {@link ApplicationHandler}s that manage the application. + * + * @author Fabio Simeoni + * + */ +@XmlRootElement(name="handlers") +public class ApplicationHandlers { + + + @XmlElement(name="lifecycle") @IsValid + private LifecycleHandlers lifecycleHandlers = new LifecycleHandlers(); + + @XmlElement(name="request") @IsValid + private RequestHandlers requestHandlers = new RequestHandlers(); + + public ApplicationHandlers() {} + + /** + * Returns the {@link ApplicationLifecycleHandler}s for the service. + * @return the lifecycle handlers + */ + public List lifecycleHandlers() { + return lifecycleHandlers.values; + } + + /** + * Sets the {@link ApplicationLifecycleHandler}s for the service. + * @param handlers the lifecycle handlers + * @return this configuration + */ + public ApplicationHandlers set(ApplicationLifecycleHandler ... handlers) { + this.lifecycleHandlers = new LifecycleHandlers(Arrays.asList(handlers)); + return this; + } + + /** + * Returns the {@link RequestHandler}s for the service. + * @return the lifetime handlers + */ + public List requestHandlers() { + return requestHandlers.values; + } + + /** + * Sets the {@link RequestHandler}s for the service. + * @param handlers the request handlers + * @return this configuration + */ + public ApplicationHandlers set(RequestHandler ... handlers) { + this.requestHandlers = new RequestHandlers(Arrays.asList(handlers)); + return this; + } + + public void validate() { + + List msgs = new ArrayList(); + + Validator validator = ValidatorFactory.validator(); + + for (ValidationError error : validator.validate(this)) + msgs.add(error.toString()); + + if (!msgs.isEmpty()) + throw new IllegalStateException("invalid configuration: "+msgs); + + } + + //////////////// HELPER BINDING CLASSES + + //used internally to introduce level of nesting in JAXB whilst preserving arbitrary extension + + private static class LifecycleHandlers { + + @SuppressWarnings("all") + LifecycleHandlers() { //needed for deserialisation + } + + LifecycleHandlers(List handlers) { + this.values=handlers; + } + + @XmlAnyElement(lax=true) + List values = new ArrayList(); + + + //since we use @AnyElement, after deserialisation, we check there are no DOM elements + @SuppressWarnings("unused") + void afterUnmarshal(Unmarshaller u, Object parent) { + for (Object o : values) + if (o instanceof Element) + throw new RuntimeException("invalid handler detected in configuration: "+Element.class.cast(o).getLocalName()); + } + + } + + //used internally to introduce level of nesting in JAXB whilst preserving arbitrary extension + private static class RequestHandlers { + + @SuppressWarnings("all") + RequestHandlers() { //needed for deserialisation + } + + RequestHandlers(List handlers) { + this.values=handlers; + } + + @XmlAnyElement(lax=true) + List values = new ArrayList(); + + //since we use @AnyElement, after deserialisation, we check there are no DOM elements + @SuppressWarnings("unused") + void afterUnmarshal(Unmarshaller u, Object parent) { + for (Object o : values) + if (o instanceof Element) + throw new RuntimeException("invalid handler detected in configuration: "+Element.class.cast(o).getLocalName()); + } + } + + + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/configuration/application/BridgedApplicationConfiguration.java b/src/main/java/org/gcube/smartgears/configuration/application/BridgedApplicationConfiguration.java new file mode 100644 index 0000000..f1182b9 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/application/BridgedApplicationConfiguration.java @@ -0,0 +1,162 @@ +package org.gcube.smartgears.configuration.application; + +import static org.gcube.smartgears.configuration.Mode.offline; + +import java.io.File; +import java.util.Set; + +import org.gcube.smartgears.configuration.Mode; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.persistence.DefaultPersistence; +import org.gcube.smartgears.persistence.Persistence; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author Fabio Simeoni + * @author Luca Frosini (ISTI - CNR) http://www.lucafrosini.com/ + */ +public class BridgedApplicationConfiguration implements ApplicationConfiguration { + + private static Logger log = LoggerFactory.getLogger(ApplicationConfiguration.class); + + private final ContainerConfiguration container; + private final ApplicationConfiguration application; + + + public BridgedApplicationConfiguration(ContainerConfiguration container, ApplicationConfiguration config) { + + this.container=container; + this.application=config; + + if (application.persistence()==null) { + + String location = container.persistence().location()+"/"+application.name(); + File dir = new File(location); + if (!dir.exists()) + dir.mkdirs(); + + application.persistence(new DefaultPersistence(location)); + + log.trace("setting persistence location for {} @ {}",application.name(), dir.getAbsolutePath()); + } + + if (container.proxyAddress()==null) + application.proxied(false); + + } + + public ApplicationConfiguration inner() { + return application; + } + + public Mode mode() { + return container.mode()==offline?offline:application.mode(); + } + + @Override + public String context() { + return application.context(); + } + + @Override + public ApplicationConfiguration context(String context) { + return application.context(context); + } + + public String name() { + return application.name(); + } + + public ApplicationConfiguration name(String name) { + return application.name(name); + } + + public String serviceClass() { + return application.serviceClass(); + } + + public ApplicationConfiguration serviceClass(String group) { + return application.serviceClass(group); + } + + public String version() { + return application.version(); + } + + public ApplicationConfiguration version(String version) { + return application.version(version); + } + + public String description() { + return application.description(); + } + + public ApplicationConfiguration description(String description) { + return application.description(description); + } + + public Persistence persistence() { + return application.persistence(); + } + + public ApplicationConfiguration persistence(Persistence manager) { + return application.persistence(manager); + } + + public ApplicationConfiguration mode(Mode mode) { + return application.mode(mode); + } + + public void validate() { + + application.validate(); + + } + + @Override + public Set excludes() { + return application.excludes(); + } + + @Override + public void merge(ApplicationConfiguration config) { + application.merge(config); + } + + @Override + public boolean secure() { + return application.secure(); + } + + @Override + public ApplicationConfiguration secure(boolean value) { + return application.secure(value); + } + + @Override + public Set startTokens() { + return application.startTokens(); + } + + @Override + public ApplicationConfiguration startTokens(Set tokens) { + return application.startTokens(tokens); + } + + @Override + public ApplicationConfiguration proxied(boolean proxied) { + return application.proxied(proxied); + } + + @Override + public boolean proxied() { + return application.proxied(); + } + + @Override + public ApplicationConfiguration excludes(Exclude ... excludes) { + return application.excludes(excludes); + } +} diff --git a/src/main/java/org/gcube/smartgears/configuration/application/DefaultApplicationConfiguration.java b/src/main/java/org/gcube/smartgears/configuration/application/DefaultApplicationConfiguration.java new file mode 100644 index 0000000..3c2e8fb --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/application/DefaultApplicationConfiguration.java @@ -0,0 +1,312 @@ +package org.gcube.smartgears.configuration.application; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; + +import org.gcube.common.validator.ValidationError; +import org.gcube.common.validator.Validator; +import org.gcube.common.validator.ValidatorFactory; +import org.gcube.common.validator.annotations.IsValid; +import org.gcube.common.validator.annotations.NotNull; +import org.gcube.smartgears.configuration.Mode; +import org.gcube.smartgears.persistence.DefaultPersistence; +import org.gcube.smartgears.persistence.Persistence; + +/** + * The configuration of a managed app. + *

+ * Includes the list of its client services. + * + * @author Fabio Simeoni + * @author Luca Frosini (ISTI - CNR) http://www.lucafrosini.com/ + * + */ +@XmlRootElement(name="application") +public class DefaultApplicationConfiguration implements ApplicationConfiguration { + + + @XmlAttribute + private Mode mode = Mode.online; + + @XmlAttribute(name="isSecure") + private boolean secure = false; + + @XmlAttribute(name="context") + String context; + + @XmlAttribute(name="proxied") + private boolean proxied = true; + + @XmlElement(name="name" , required=true) + @NotNull + String name; + + @XmlElement(name="group", required=true) + @NotNull + String group; + + @XmlElement(name="version", required=true) + @NotNull + String version; + + @XmlTransient + Set tokens = new HashSet(); + + @XmlElement(name="description") + String description=""; + + @XmlElementRef + Set excludes= new LinkedHashSet(); + + @XmlElementRef(type=DefaultPersistence.class) + @NotNull @IsValid + private Persistence persistenceManager; + + @Override + public Set excludes() { + return excludes; + } + + public DefaultApplicationConfiguration() {} + + @Override + public Mode mode() { + return mode; + } + + @Override + public boolean secure() { + return secure; + } + + @Override + public String name() { + return name; + } + + @Override + public String context() { + return context; + } + + @Override + public ApplicationConfiguration context(String context) { + this.context=context; + return this; + } + + @Override + public ApplicationConfiguration excludes(Exclude ... excludes) { + this.excludes=new HashSet(Arrays.asList(excludes)); + return this; + } + + @Override + public ApplicationConfiguration name(String name) { + this.name=name; + return this; + } + + @Override + public String serviceClass() { + return group; + } + + @Override + public boolean proxied() { + return proxied; + } + + @Override + public ApplicationConfiguration proxied(boolean proxied) { + this.proxied = proxied; + return this; + } + + @Override + public ApplicationConfiguration serviceClass(String group) { + this.group=group; + return this; + } + + @Override + public String version() { + return version; + } + + @Override + public ApplicationConfiguration version(String version) { + this.version=version; + return this; + } + + @Override + public Set startTokens() { + return tokens; + } + + @Override + public ApplicationConfiguration startTokens(Set tokens) { + this.tokens.addAll(tokens); + return this; + } + + @Override + public String description() { + return description; + } + + @Override + public ApplicationConfiguration description(String description) { + this.description=description; + return this; + } + + + + @Override + public Persistence persistence() { + return persistenceManager; + } + + @Override + public ApplicationConfiguration persistence(Persistence manager) { + this.persistenceManager=manager; + return this; + } + + @Override + public ApplicationConfiguration secure(boolean value) { + this.secure=value; + return this; + } + + @Override + public ApplicationConfiguration mode(Mode mode) { + this.mode=mode; + return this; + } + + @Override + public void validate() { + + List msgs = new ArrayList(); + + Validator validator = ValidatorFactory.validator(); + + for (ValidationError error : validator.validate(this)) + msgs.add(error.toString()); + + if (!msgs.isEmpty()) + throw new IllegalStateException("invalid configuration: "+msgs); + + } + + + @Override + public void merge(ApplicationConfiguration config) { + + mode(config.mode()); + + if (config.persistence()!=null) + persistence(config.persistence()); + + //scopes.addAll(config.startScopes()); + + } + + + + @Override + public String toString() { + return "DefaultApplicationConfiguration [mode=" + mode + ", secure=" + + secure + ", context=" + context + ", proxied=" + proxied + + ", name=" + name + ", group=" + group + ", version=" + + version + ", tokens=" + tokens + ", description=" + + description + ", excludes=" + excludes + + ", persistenceManager=" + persistenceManager + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((context == null) ? 0 : context.hashCode()); + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((excludes == null) ? 0 : excludes.hashCode()); + result = prime * result + ((group == null) ? 0 : group.hashCode()); + result = prime * result + ((mode == null) ? 0 : mode.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((persistenceManager == null) ? 0 : persistenceManager.hashCode()); + result = prime * result + ((tokens == null) ? 0 : tokens.hashCode()); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + DefaultApplicationConfiguration other = (DefaultApplicationConfiguration) obj; + if (context == null) { + if (other.context != null) + return false; + } else if (!context.equals(other.context)) + return false; + if (description == null) { + if (other.description != null) + return false; + } else if (!description.equals(other.description)) + return false; + if (excludes == null) { + if (other.excludes != null) + return false; + } else if (!excludes.equals(other.excludes)) + return false; + if (group == null) { + if (other.group != null) + return false; + } else if (!group.equals(other.group)) + return false; + if (mode != other.mode) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (persistenceManager == null) { + if (other.persistenceManager != null) + return false; + } else if (!persistenceManager.equals(other.persistenceManager)) + return false; + if (tokens == null) { + if (other.tokens != null) + return false; + } else if (!tokens.equals(other.tokens)) + return false; + if (version == null) { + if (other.version != null) + return false; + } else if (!version.equals(other.version)) + return false; + if (secure!=other.secure) + return false; + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/configuration/application/Exclude.java b/src/main/java/org/gcube/smartgears/configuration/application/Exclude.java new file mode 100644 index 0000000..579758b --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/application/Exclude.java @@ -0,0 +1,79 @@ +package org.gcube.smartgears.configuration.application; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlValue; + +@XmlRootElement(name="exclude") +@XmlAccessorType(XmlAccessType.FIELD) +public class Exclude { + + @XmlAttribute(name="handlers") + private List handlers = new ArrayList(); + + @XmlValue + private String path; + + public List getHandlers() { + return handlers; + } + + public String getPath() { + return path; + } + + protected Exclude() {} + + public Exclude(String path) { + super(); + this.path = path; + } + + public Exclude(List handlers, String path) { + super(); + this.handlers = handlers; + this.path = path; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((path == null) ? 0 : path.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Exclude other = (Exclude) obj; + if (handlers == null) { + if (other.handlers != null) + return false; + } else if (!handlers.equals(other.handlers)) + return false; + if (path == null) { + if (other.path != null) + return false; + } else if (!path.equals(other.path)) + return false; + return true; + } + + @Override + public String toString() { + return "Exclude [handlers=" + handlers + ", path=" + path + "]"; + } + +} diff --git a/src/main/java/org/gcube/smartgears/configuration/container/ContainerConfiguration.java b/src/main/java/org/gcube/smartgears/configuration/container/ContainerConfiguration.java new file mode 100644 index 0000000..55102bf --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/container/ContainerConfiguration.java @@ -0,0 +1,501 @@ +package org.gcube.smartgears.configuration.container; + +import static org.gcube.smartgears.Constants.default_container_publication_frequency_in_seconds; +import static org.gcube.smartgears.utils.Utils.notNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; + +import org.gcube.common.validator.ValidationError; +import org.gcube.common.validator.Validator; +import org.gcube.common.validator.ValidatorFactory; +import org.gcube.common.validator.annotations.IsValid; +import org.gcube.common.validator.annotations.NotEmpty; +import org.gcube.common.validator.annotations.NotNull; +import org.gcube.smartgears.configuration.Mode; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.DefaultApplicationConfiguration; +import org.gcube.smartgears.persistence.DefaultPersistence; +import org.gcube.smartgears.persistence.Persistence; + +/** + * The configuration of the container. + * + * @author Fabio Simeoni + * @author Luca Frosini (ISTI - CNR) http://www.lucafrosini.com/ + */ +@XmlRootElement(name="container") +public class ContainerConfiguration { + + + @XmlAttribute + private Mode mode = Mode.online; + + @XmlElement + @NotNull + String hostname; + + @XmlElement + @NotNull + Integer port; + + @XmlElementRef + @IsValid + ProxyAddress proxyAddress; + + @XmlElement(name ="authentication-endpoint") + String authenticationEnpoint = null; + + @XmlElement(name ="secure-port") + Integer securePort; + + @XmlElement + @NotNull + String infrastructure; + + @XmlElement + @NotNull @IsValid + Site site; + + @XmlElement(name="token") + @NotNull @NotEmpty + List tokens = new ArrayList(); + + @XmlTransient + Set allowedContext = new HashSet(); + + @XmlElementRef(type=DefaultApplicationConfiguration.class) + List apps = new ArrayList(); + + @XmlElement(name="property") + @IsValid + List properties = new ArrayList(); + + @XmlElement(name="publication-frequency") + long publicationFrequency = default_container_publication_frequency_in_seconds; + + @XmlElementRef(type=DefaultPersistence.class) + @IsValid + private Persistence persistenceManager; + + /** + * Returns the management mode for the container. + * @return the management mode + */ + public Mode mode() { + return mode; + } + + /** + * Sets the management mode for the container. + * @param mode the management mode + * @return this configuration + */ + public ContainerConfiguration mode(Mode mode) { + this.mode=mode; + return this; + } + + /** + * Returns the application configurations included in this configuration. + * @return the application configurations + */ + public List apps() { + return apps; + } + + /** + * Returns the configuration of an application with a given context path. + * @param context the context path + * @return the application configuration + */ + public ApplicationConfiguration app(String context) { + + for (ApplicationConfiguration app : apps) + if (context.equals(app.context())) + return app; + + return null; + } + + /** + * Adds the configuration of an application to this configuration. + * @param app the application configuration + * @return this configuration + */ + public synchronized ContainerConfiguration app(ApplicationConfiguration app) { + int indexToRemove =-1; + int index =0; + for (ApplicationConfiguration application : apps){ + if (app.context().equals(application.context())) + indexToRemove = index; + index++; + } + if(indexToRemove!=-1) + apps.remove(indexToRemove); + apps.add(app); + return this; + } + + /** + * Returns the geographical site of the container. + * @return the site + */ + public Site site() { + return site; + } + + /** + * Sets the geographical site of the container. + * @param site the site + * @return this configuration + */ + public ContainerConfiguration site(Site site) { + this.site=site; + return this; + } + + /** + * Returns the infrastructure in which the container is running. + * @return the infrastructure + */ + public String infrastructure() { + return infrastructure; + } + + /** + * Sets the infrastructure in which the container is running. + * @param infrastructure the infrastructure + * @return this configuration + */ + public ContainerConfiguration infrastructure(String infrastructure) { + this.infrastructure=infrastructure; + return this; + } + + /** + * Returns the host name of the container. + * @return the host name; + */ + public String hostname() { + return hostname; + } + + /** + * Sets the host name of the container. + * @param name the host name + * @return this configuration + */ + public ContainerConfiguration hostname(String name) { + this.hostname=name; + return this; + } + + /** + * Returns the port at which the container is listening for requests. + * @return the port + */ + public int port() { + return port; + } + + + /** + * Returns the port at which the container is listening for requests. + * @return the port + */ + public Integer securePort() { + return securePort; + } + + + public String authenticationEnpoint() { + return authenticationEnpoint; + } + + public ContainerConfiguration authenticationEnpoint(String endpoint) { + this.authenticationEnpoint = endpoint; + return this; + } + + /** + * Sets the port at which the container is listening for requests. + * @param port the port + * @return this configuration + */ + public ContainerConfiguration port(int port) { + this.port=port; + return this; + } + + public ContainerConfiguration securePort(int port) { + this.securePort=port; + return this; + } + + /** + * Returns the VOs in which the container initially operates. + * @return the VOs + */ + public List startTokens() { + return tokens; + } + + /** + * Sets the VOs in which the container initially operates. + * @param vos the VOs + * @return this configuration + */ + public ContainerConfiguration startTokens(List tokens) { + + notNull("start Tokens",tokens); + + this.tokens = tokens; + + return this; + } + + public ProxyAddress proxyAddress() { + return proxyAddress; + } + + public ContainerConfiguration setProxyaddress(ProxyAddress proxyaddress) { + this.proxyAddress = proxyaddress; + return this; + } + + /** + * Returns the persistence manager of the container. + * @return the manager + */ + public Persistence persistence() { + return persistenceManager; + } + + /** + * Sets the persistence manager of the container. + * @param manager the manager + * @return this configuration + */ + public ContainerConfiguration persistence(Persistence manager) { + this.persistenceManager=manager; + return this; + } + + /** + * Returns the configuration properties of the container. + * @return the properties + */ + public Map properties() { + Map map = new HashMap(); + for (Property prop : properties) + map.put(prop.name, prop.value); + return map; + } + + /** + * Adds a configuration property to the container. + * @param the name of the property + * @param the value of the property + * @return this configuration + */ + public ContainerConfiguration property(String name, String value) { + properties.add(new Property(name, value)); + return this; + } + + /** + * Returns the publication frequency for the container's profile. + * @return the frquency; + */ + public long publicationFrequency() { + return publicationFrequency; + } + + /** + * Sets the publication frequency for the container's profile. + * @param frequency the frequency + * @return this configuration + */ + public ContainerConfiguration publicationFrequency(long frequency) { + this.publicationFrequency=frequency; + return this; + } + + + public Set allowedContexts() { + return allowedContext; + } + + public void allowedContexts(Set allowedContexts) { + this.allowedContext = allowedContexts; + } + + /** + * Validates this configuration + * + * @throws IllegalStateException if the configuration is invalid + */ + public void validate() { + + List msgs = new ArrayList(); + + Validator validator = ValidatorFactory.validator(); + + for (ValidationError error : validator.validate(this)) + msgs.add(error.toString()); + + if (!msgs.isEmpty()) + throw new IllegalStateException("invalid configuration: "+msgs); + + } + + + + static class Property { + + @XmlAttribute @NotNull + String name; + + @XmlAttribute @NotNull + String value; + + Property() {} + + Property(String key, String value) { + this.name=key; + this.value=value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Property other = (Property) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + + + + } + + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((apps == null) ? 0 : apps.hashCode()); + result = prime * result + ((hostname == null) ? 0 : hostname.hashCode()); + result = prime * result + ((infrastructure == null) ? 0 : infrastructure.hashCode()); + result = prime * result + ((mode == null) ? 0 : mode.hashCode()); + result = prime * result + ((persistenceManager == null) ? 0 : persistenceManager.hashCode()); + result = prime * result + ((port == null) ? 0 : port.hashCode()); + result = prime * result + ((properties == null) ? 0 : properties.hashCode()); + result = prime * result + (int) (publicationFrequency ^ (publicationFrequency >>> 32)); + result = prime * result + ((site == null) ? 0 : site.hashCode()); + result = prime * result + ((tokens == null) ? 0 : tokens.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ContainerConfiguration other = (ContainerConfiguration) obj; + if (apps == null) { + if (other.apps != null) + return false; + } else if (!apps.equals(other.apps)) + return false; + if (hostname == null) { + if (other.hostname != null) + return false; + } else if (!hostname.equals(other.hostname)) + return false; + if (infrastructure == null) { + if (other.infrastructure != null) + return false; + } else if (!infrastructure.equals(other.infrastructure)) + return false; + if (mode != other.mode) + return false; + if (persistenceManager == null) { + if (other.persistenceManager != null) + return false; + } else if (!persistenceManager.equals(other.persistenceManager)) + return false; + if (port == null) { + if (other.port != null) + return false; + } else if (!port.equals(other.port)) + return false; + if (securePort == null) { + if (other.securePort != null) + return false; + } else if (!securePort.equals(other.securePort)) + return false; + if (properties == null) { + if (other.properties != null) + return false; + } else if (!properties.equals(other.properties)) + return false; + if (publicationFrequency != other.publicationFrequency) + return false; + if (site == null) { + if (other.site != null) + return false; + } else if (!site.equals(other.site)) + return false; + if (tokens == null) { + if (other.tokens != null) + return false; + } else if (!tokens.equals(other.tokens)) + return false; + + return true; + } + + + + + + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/configuration/container/ContainerConfigurationBinder.java b/src/main/java/org/gcube/smartgears/configuration/container/ContainerConfigurationBinder.java new file mode 100644 index 0000000..7d112a0 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/container/ContainerConfigurationBinder.java @@ -0,0 +1,101 @@ +package org.gcube.smartgears.configuration.container; + +import static org.gcube.smartgears.utils.Utils.*; + +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.ServiceLoader; +import java.util.Set; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; + +import org.gcube.smartgears.handlers.container.ContainerHandler; +import org.gcube.smartgears.utils.Utils; + +/** + * Binds {@link ContainerConfiguration}s to and from XML serialisations. + * + * @author Fabio Simeoni + * + */ +public class ContainerConfigurationBinder { + + /** + * Returns a {@link ContainerConfiguration} from its XML serialisation. + * + * @param stream the serialisation + * @return the configuration + * @throws RuntimeException if the serialisation is invalid + */ + public ContainerConfiguration bind(InputStream stream) { + + try { + + JAXBContext ctx = JAXBContext.newInstance(ContainerConfiguration.class); + + ContainerConfiguration config = (ContainerConfiguration) ctx.createUnmarshaller().unmarshal(stream); + + return config; + + } catch (JAXBException e) { + + throw new RuntimeException("invalid container configuration", e); + + } + finally { + + Utils.closeSafely(stream); + } + } + + /** + * Returns the handlers of the container from their XML serialisation. + * + * @param stream the serialisation + * @return the handlers + * @throws RuntimeException if the serialisation is invalid + */ + public ContainerHandlers bindHandlers(InputStream stream) { + + //collects handler classes + Set> classes = scanForConfigurationElements(); + + try { + + JAXBContext ctx = JAXBContext.newInstance(classes.toArray(new Class[0])); + + return (ContainerHandlers) ctx.createUnmarshaller().unmarshal(stream); + + } catch (JAXBException e) { + + throw unchecked(e); + + } + } + + + + private Set> scanForConfigurationElements() throws RuntimeException { + + @SuppressWarnings("all") + ServiceLoader handlerLoader = (ServiceLoader) ServiceLoader.load(ContainerHandler.class); + + Set> scanned = new HashSet>(); + + for (ContainerHandler handler : handlerLoader) { + Class handlerClass = handler.getClass(); + if (handlerClass.isInterface() || handlerClass.getModifiers() == Modifier.ABSTRACT) + continue; + else + scanned.add(handlerClass); + } + + //add top-level configuration + scanned.add(ContainerHandlers.class); + + return scanned; + } + +} diff --git a/src/main/java/org/gcube/smartgears/configuration/container/ContainerHandlers.java b/src/main/java/org/gcube/smartgears/configuration/container/ContainerHandlers.java new file mode 100644 index 0000000..77a30fd --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/container/ContainerHandlers.java @@ -0,0 +1,44 @@ +package org.gcube.smartgears.configuration.container; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.smartgears.handlers.container.ContainerHandler; + +/** + * The {@link ContainerHandler}s that manage the application. + * + * @author Fabio Simeoni + * + */ +@XmlRootElement(name="handlers") +public class ContainerHandlers { + + @XmlAnyElement(lax=true) + List handlers = new ArrayList(); + + public ContainerHandlers() {} + + /** + * Returns the {@link ContainerHandler}s for the service. + * @return the lifecycle handlers + */ + public List get() { + return handlers; + } + + /** + * Sets the {@link ContainerHandler}s for the service. + * @param handlers the lifecycle handlers + * @return this configuration + */ + public ContainerHandlers set(ContainerHandler ... handlers) { + this.handlers = Arrays.asList(handlers); + return this; + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/configuration/container/ProxyAddress.java b/src/main/java/org/gcube/smartgears/configuration/container/ProxyAddress.java new file mode 100644 index 0000000..8cfde55 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/container/ProxyAddress.java @@ -0,0 +1,52 @@ +package org.gcube.smartgears.configuration.container; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.validator.annotations.NotNull; + +@XmlRootElement(name="proxy") +public class ProxyAddress { + + @XmlAttribute + boolean secure = false; + + @XmlElement + @NotNull + String hostname; + + @XmlElement + @NotNull + int port; + + public String hostname() { + return hostname; + } + + public ProxyAddress hostname(String hostname) { + this.hostname = hostname; + return this; + } + + public int port() { + return port; + } + + public ProxyAddress port(int port) { + this.port = port; + return this; + } + + public boolean secure() { + return secure; + } + + public ProxyAddress secure(boolean secure) { + this.secure = secure; + return this; + } + + + +} diff --git a/src/main/java/org/gcube/smartgears/configuration/container/Site.java b/src/main/java/org/gcube/smartgears/configuration/container/Site.java new file mode 100644 index 0000000..cc41f4f --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/container/Site.java @@ -0,0 +1,152 @@ +package org.gcube.smartgears.configuration.container; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.validator.annotations.NotNull; + +/** + * The geographical site of the container. + * + * @author Fabio Simeoni + * + */ +@XmlRootElement(name="site") +public class Site { + + @XmlElement + @NotNull + String country; + + @XmlElement + @NotNull + String location; + + @XmlElement + @NotNull + String latitude; + + @XmlElement + @NotNull + String longitude; + + /** + * Returns the country. + * @return the country + */ + public String country() { + return country; + } + + /** + * Sets the country. + * @param the country + * @return this configuration + */ + public Site country(String country) { + this.country=country; + return this; + } + + + /** + * Returns the latitude. + * @return the latitude + */ + public String latitude() { + return latitude; + } + + /** + * Sets the latitude. + * @param the latitude + * @return this configuration + */ + public Site latitude(String latitude) { + this.latitude=latitude; + return this; + } + + + /** + * Returns the longitude. + * @return the longitude + */ + public String longitude() { + return longitude; + } + + /** + * Sets the longitude. + * @param the longitude + * @return this configuration + */ + public Site longitude(String longitude) { + this.longitude=longitude; + return this; + } + + /** + * Returns the location. + * @return the location + */ + public String location() { + return location; + } + + /** + * Sets the location. + * @param the location + * @return this location + */ + public Site location(String location) { + this.location=location; + return this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((country == null) ? 0 : country.hashCode()); + result = prime * result + ((latitude == null) ? 0 : latitude.hashCode()); + result = prime * result + ((location == null) ? 0 : location.hashCode()); + result = prime * result + ((longitude == null) ? 0 : longitude.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Site other = (Site) obj; + if (country == null) { + if (other.country != null) + return false; + } else if (!country.equals(other.country)) + return false; + if (latitude == null) { + if (other.latitude != null) + return false; + } else if (!latitude.equals(other.latitude)) + return false; + if (location == null) { + if (other.location != null) + return false; + } else if (!location.equals(other.location)) + return false; + if (longitude == null) { + if (other.longitude != null) + return false; + } else if (!longitude.equals(other.longitude)) + return false; + return true; + } + + + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/configuration/library/SmartGearsConfiguration.java b/src/main/java/org/gcube/smartgears/configuration/library/SmartGearsConfiguration.java new file mode 100644 index 0000000..1541837 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/library/SmartGearsConfiguration.java @@ -0,0 +1,79 @@ +package org.gcube.smartgears.configuration.library; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.validator.ValidationError; +import org.gcube.common.validator.Validator; +import org.gcube.common.validator.ValidatorFactory; +import org.gcube.common.validator.annotations.NotEmpty; + +@XmlRootElement(name="smartgears") +public class SmartGearsConfiguration { + + @XmlAttribute @NotEmpty + private String version; + + public SmartGearsConfiguration(){ + } + + public String version() { + return version; + } + + public SmartGearsConfiguration version(String version) { + this.version=version; + return this; + } + + + /** + * Validates this configuration + * + * @throws IllegalStateException if the configuration is invalid + */ + public void validate() { + + List msgs = new ArrayList(); + + Validator validator = ValidatorFactory.validator(); + + for (ValidationError error : validator.validate(this)) + msgs.add(error.toString()); + + if (!msgs.isEmpty()) + throw new IllegalStateException("invalid configuration: "+msgs); + + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SmartGearsConfiguration other = (SmartGearsConfiguration) obj; + if (version == null) { + if (other.version != null) + return false; + } else if (!version.equals(other.version)) + return false; + return true; + } + + + +} diff --git a/src/main/java/org/gcube/smartgears/configuration/library/SmartGearsConfigurationBinder.java b/src/main/java/org/gcube/smartgears/configuration/library/SmartGearsConfigurationBinder.java new file mode 100644 index 0000000..11a9b42 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/configuration/library/SmartGearsConfigurationBinder.java @@ -0,0 +1,47 @@ +package org.gcube.smartgears.configuration.library; + +import java.io.InputStream; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; + +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.utils.Utils; + +/** + * Binds {@link ContainerConfiguration}s to and from XML serialisations. + * + * @author Fabio Simeoni + * + */ +public class SmartGearsConfigurationBinder { + + /** + * Returns a {@link ContainerConfiguration} from its XML serialisation. + * + * @param stream the serialisation + * @return the configuration + * @throws RuntimeException if the serialisation is invalid + */ + public SmartGearsConfiguration bind(InputStream stream) { + + try { + + JAXBContext ctx = JAXBContext.newInstance(SmartGearsConfiguration.class); + + SmartGearsConfiguration config = (SmartGearsConfiguration) ctx.createUnmarshaller().unmarshal(stream); + + return config; + + } catch (JAXBException e) { + + throw new RuntimeException("invalid library configuration", e); + + } + finally { + + Utils.closeSafely(stream); + } + } + +} diff --git a/src/main/java/org/gcube/smartgears/context/Properties.java b/src/main/java/org/gcube/smartgears/context/Properties.java new file mode 100644 index 0000000..d682f55 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/context/Properties.java @@ -0,0 +1,171 @@ +package org.gcube.smartgears.context; + +import static org.gcube.smartgears.utils.Utils.*; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * A collection of uniquely named {@link Property}s. + * + * @author Fabio Simeoni + * + */ +public class Properties implements Iterable { + + private final Map properties = new HashMap(); + private Properties parent; + + /** + * Creates an instance that delegates to another for unknown properties. + * @param parent the parent instance + */ + public Properties(Properties parent) { + this.parent=parent; + } + + /** + * Creates an instance. + */ + public Properties() {} + + @Override + public Iterator iterator() { + return properties.values().iterator(); + } + + /** + * Adds one or more properties to this collection. + * + * @param properties the properties + */ + public synchronized void add(Property ... properties) { + + notNull("properties",properties); + + for (Property property : properties) + this.properties.put(property.name(),property); + } + + /** + * Returns true if this collection contains a given property. + * + * @param name the name of the property + * @return true if this collection contains a property with the given name + */ + public synchronized boolean contains(String name) { + + notNull("property name",name); + + return this.properties.containsKey(name) || + parent==null? + false: + this.parent.contains(name); + } + + /** + * Removes a given property. + * + * @param name the name of the property + * + * @throws IllegalStateException if a property with the given name does not exist in this collection + */ + public void delete(String name) { + + notNull("property name",name); + + if (this.properties.remove(name) == null) + if (parent==null) + throw new IllegalStateException("unknown property " + name); + else + parent.delete(name); + } + + /** + * Returns a given property in this collection. + * + * @param name the name of the property + * @return the property + * + * @throws IllegalStateException if a property with a given name does not exist in this collection + */ + public synchronized Property lookup(String name) { + + notNull("property name",name); + + Property property = this.properties.get(name); + + if (property == null) + if (parent==null) + throw new IllegalStateException("unknown property " + name); + else + property = parent.lookup(name); + + return property; + + } + + /** + * Returns true if this collection has no properties. + * + * @return true if this collection has no properties + */ + public synchronized boolean isEmpty() { + return properties.isEmpty() || parent==null? true: parent.isEmpty(); + } + + + @Override + public String toString() { + final int maxLen = 100; + return "board=" + (properties != null ? toString(properties.entrySet(), maxLen) : null); + } + + private String toString(Collection collection, int maxLen) { + StringBuilder builder = new StringBuilder(); + builder.append("["); + int i = 0; + for (Iterator iterator = collection.iterator(); iterator.hasNext() && i < maxLen; i++) { + if (i > 0) + builder.append(", "); + builder.append(iterator.next()); + } + builder.append("]"); + return builder.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((parent == null) ? 0 : parent.hashCode()); + result = prime * result + ((properties == null) ? 0 : properties.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Properties other = (Properties) obj; + if (parent == null) { + if (other.parent != null) + return false; + } else if (!parent.equals(other.parent)) + return false; + if (properties == null) { + if (other.properties != null) + return false; + } else if (!properties.equals(other.properties)) + return false; + return true; + } + + +} diff --git a/src/main/java/org/gcube/smartgears/context/Property.java b/src/main/java/org/gcube/smartgears/context/Property.java new file mode 100644 index 0000000..be92170 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/context/Property.java @@ -0,0 +1,155 @@ +package org.gcube.smartgears.context; + +import static org.gcube.smartgears.utils.Utils.*; + + +/** + * A named property with a value and a description. + * + * @author Fabio Simeoni + * + */ +public class Property { + + private final String name; + private final String description; + private final Object value; + private boolean display =true; + + /** + * Creates an instance with a given name and value. + * @param name the name + * @param value the value + */ + public Property(String name, Object value) { + this(name,value,null); + } + + /** + * Creates an instance with a given name, value, and description. + * @param name the name + * @param value the value + * @param description the description + */ + public Property(String name, Object value, String description) { + + notNull("property name",name); + notNull("property value",value); + + this.name=name; + this.description=description; + this.value=value; + } + + /** + * Returns the name of this property. + * @return the name + */ + public String name() { + return name; + } + + /** + * Sets whether the property is intended for display + * @param display true if the property is intended for display + */ + public void display(boolean display) { + this.display = display; + } + + /** + * Returns true if the property is intended for display + * @return true if the property is intended for display + */ + public boolean isDisplay() { + return display; + } + + /** + * Returns the value of this property. + * + * @return the value + */ + public Object value() { + return value; + } + + /** + * Returns the value of this property under a given type. + * + * @return the value + * + * @throws IllegalStateException if the value cannot be returned under the given type. + */ + public S value(Class type) { + + if (is(type)) + return type.cast(value()); + + throw new IllegalStateException("property value "+value()+" of type "+value().getClass()+" cannot be typed as "+type.getCanonicalName()); + + } + + /** + * Returns true if the value of this property has a given type. + * @param type the type + * @return true if the value of this property has a given type + */ + public boolean is(Class type) { + return type.isInstance(value()); + } + + /** + * Returns the description of this property. + * + * @return the description + */ + public String description() { + return description; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Property other = (Property) obj; + if (description == null) { + if (other.description != null) + return false; + } else if (!description.equals(other.description)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + + @Override + public String toString() { + return name+"="+value; + } + + + +} diff --git a/src/main/java/org/gcube/smartgears/context/application/ApplicationContext.java b/src/main/java/org/gcube/smartgears/context/application/ApplicationContext.java new file mode 100644 index 0000000..033cb34 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/context/application/ApplicationContext.java @@ -0,0 +1,82 @@ +package org.gcube.smartgears.context.application; + +import javax.servlet.ServletContext; + +import org.gcube.common.events.Hub; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.context.Properties; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; +import org.gcube.smartgears.persistence.Persistence; + +/** + * The management context of an application. + * + * @author Fabio Simeoni + * + */ +public interface ApplicationContext { + + + String id(); + + /** + * Returns the name of the application. + * + * @return the name + */ + String name(); + + /** + * Returns the configuration of the application. + * + * @return the configuration + */ + ApplicationConfiguration configuration(); + + + T profile(Class type); + + /** + * Returns the lifecycle of the application. + * + * @return the lifecycle + */ + ApplicationLifecycle lifecycle(); + + /** + * Returns the event hub of the application + * + * @return the hub + */ + Hub events(); + + /** + * Returns the persistence manager of the application. + * + * @return the manager + */ + Persistence persistence(); + + /** + * Returns the servlet context of the application. + * + * @return the context + */ + ServletContext application(); + + /** + * Returns the management context of the container. + * + * @return the context + */ + ContainerContext container(); + + /** + * Returns the properties of the application + * + * @return the properties + */ + Properties properties(); + +} diff --git a/src/main/java/org/gcube/smartgears/context/application/DefaultApplicationContext.java b/src/main/java/org/gcube/smartgears/context/application/DefaultApplicationContext.java new file mode 100644 index 0000000..96f2b3a --- /dev/null +++ b/src/main/java/org/gcube/smartgears/context/application/DefaultApplicationContext.java @@ -0,0 +1,113 @@ +package org.gcube.smartgears.context.application; + +import static org.gcube.smartgears.Constants.profile_property; + +import javax.servlet.ServletContext; + +import org.gcube.common.events.Hub; +import org.gcube.common.resources.gcore.GCoreEndpoint; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.context.Properties; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; +import org.gcube.smartgears.persistence.Persistence; + +/** + * Default {@link ApplicationContext} implementation. + * + * @author Fabio Simeoni + * + */ +public class DefaultApplicationContext implements ApplicationContext { + + private final ContainerContext container; + private final ServletContext sctx; + private final ApplicationConfiguration configuration; + private final ApplicationLifecycle lifecycle; + private final Properties properties; + private final Hub hub; + private final String id; + + /** + * Crates an intance with mandatory parameters + * @param container the container context + * @param sctx the servlet context + * @param configuration the configuration + * @param hub the event hub + * @param lifecycle the lifecycle + * @param properties the properties + */ + public DefaultApplicationContext(String id,ContainerContext container,ServletContext sctx,ApplicationConfiguration configuration, Hub hub, ApplicationLifecycle lifecycle, Properties properties) { + this.id = id; + this.container=container; + this.sctx = sctx; + this.configuration=configuration; + this.hub=hub; + this.lifecycle = lifecycle; + this.properties=properties; + } + + /** + * Creates an instance by copying the configuration of another. + * @param context the other instance + */ + public DefaultApplicationContext(ApplicationContext context) { + this(context.id(), context.container(),context.application(),context.configuration(),context.events(), context.lifecycle(), new Properties(context.properties())); + } + + @Override + public ServletContext application() { + return sctx; + } + + @Override + public ContainerContext container() { + return container; + } + + @Override + @SuppressWarnings("all") + public T profile(Class type) { + + if (type==GCoreEndpoint.class) + return (T) properties().lookup(profile_property).value(GCoreEndpoint.class); + + throw new IllegalArgumentException("unsupported profile type: "+type); + } + + @Override + public String name() { //little shortcut for ease of logging + return configuration.name(); + } + + @Override + public ApplicationConfiguration configuration() { + return configuration; + } + + @Override + public ApplicationLifecycle lifecycle() { + return lifecycle; + } + + @Override + public Hub events() { + return hub; + } + + @Override + public Persistence persistence() { + return configuration.persistence(); + } + + @Override + public Properties properties() { + return properties; + } + + @Override + public String id() { + return id; + } + +} diff --git a/src/main/java/org/gcube/smartgears/context/container/ContainerContext.java b/src/main/java/org/gcube/smartgears/context/container/ContainerContext.java new file mode 100644 index 0000000..3c7ae53 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/context/container/ContainerContext.java @@ -0,0 +1,56 @@ +package org.gcube.smartgears.context.container; + +import org.gcube.common.events.Hub; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.context.Properties; +import org.gcube.smartgears.lifecycle.container.ContainerLifecycle; +import org.gcube.smartgears.persistence.Persistence; + +/** + * The management context of the container. + * + * @author Fabio Simeoni + * + */ +public interface ContainerContext { + + + /** + * Returns the configuration of the container. + * @return the configuration + */ + ContainerConfiguration configuration(); + + /** + * Returns the resource profile of a given type of the container. + * @return the profile + */ + T profile(Class type); + + /** + * Returns the lifecycle of the container + * @return the lifecycle + */ + ContainerLifecycle lifecycle(); + + /** + * Returns the event hub of the container + * @return the hub + */ + Hub events(); + + /** + * Returns the persistence manager of the container. + * @return the manager + */ + Persistence persistence(); + + /** + * Returns the properties of the container. + * @return the properties + */ + Properties properties(); + + String id(); + +} diff --git a/src/main/java/org/gcube/smartgears/context/container/DefaultContainerContext.java b/src/main/java/org/gcube/smartgears/context/container/DefaultContainerContext.java new file mode 100644 index 0000000..0a599f7 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/context/container/DefaultContainerContext.java @@ -0,0 +1,81 @@ +package org.gcube.smartgears.context.container; + +import static org.gcube.smartgears.Constants.*; +import org.gcube.common.events.Hub; +import org.gcube.common.resources.gcore.HostingNode; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.context.Properties; +import org.gcube.smartgears.lifecycle.container.ContainerLifecycle; +import org.gcube.smartgears.persistence.Persistence; + +/** + * Default {@link ContainerContext} implementation. + * + * @author Fabio Simeoni + * + */ +public class DefaultContainerContext implements ContainerContext { + + private final ContainerConfiguration configuration; + private final ContainerLifecycle lifecycle; + private final Properties properties; + private final Hub hub; + private final String id; + /** + * Creates an instance with mandatory parameters. + * @param configuration the configuration + * @param hub the event hub + * @param lifecycle the lifecycle + * @param properties the properties + */ + public DefaultContainerContext(String id,ContainerConfiguration configuration, Hub hub, ContainerLifecycle lifecycle, + Properties properties) { + this.id = id; + this.configuration=configuration; + this.hub=hub; + this.lifecycle = lifecycle; + this.properties=properties; + } + + @SuppressWarnings("all") + public T profile(Class type) { + + if (type==HostingNode.class) + return (T) properties().lookup(container_profile_property).value(HostingNode.class); + + throw new IllegalArgumentException("unsupported profile type: "+type); + }; + + @Override + public ContainerConfiguration configuration() { + return configuration; + } + + @Override + public ContainerLifecycle lifecycle() { + return lifecycle; + } + + @Override + public Hub events() { + return hub; + } + + @Override + public Persistence persistence() { + return configuration.persistence(); + } + + @Override + public Properties properties() { + return properties; + } + + @Override + public String id() { + return id; + } + + + +} diff --git a/src/main/java/org/gcube/smartgears/extensions/ApiMethodSignature.java b/src/main/java/org/gcube/smartgears/extensions/ApiMethodSignature.java new file mode 100644 index 0000000..2763618 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/ApiMethodSignature.java @@ -0,0 +1,37 @@ +package org.gcube.smartgears.extensions; + +import static java.util.Arrays.*; +import static org.gcube.common.events.impl.Utils.*; + +import java.util.HashSet; +import java.util.Set; + +import org.gcube.smartgears.extensions.HttpExtension.Method; + +public class ApiMethodSignature { + + Method method; + Set requestTypes = new HashSet(); + Set responseTypes = new HashSet(); + + public ApiMethodSignature(Method method) { + notNull("method",method); + this.method=method; + } + + public ApiMethodSignature accepts(String ... types) { + + notNull("request types",types); + this.requestTypes.addAll(asList(types)); + return this; + } + + public ApiMethodSignature produces(String ... types) { + + notNull("response types",types); + this.responseTypes.addAll(asList(types)); + return this; + } + + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/extensions/ApiResource.java b/src/main/java/org/gcube/smartgears/extensions/ApiResource.java new file mode 100644 index 0000000..6fab2bd --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/ApiResource.java @@ -0,0 +1,150 @@ +package org.gcube.smartgears.extensions; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.configuration.application.Exclude; + +/** + * A resource-specifc API handled by an {@link HttpController}. + * + * @author Fabio Simeoni + * + */ +public abstract class ApiResource extends HttpExtension { + + private static final long serialVersionUID = 1L; + + /** + * Returns a {@link ApiSignature} that declares the method and media types handled by an {@link ApiResource} for a given mapping. + * @param mapping the mapping + * @return the signature + */ + public static ApiSignature handles(String mapping) { + return new ApiSignature(mapping); + } + + /** + * Returns an {@link ApiMethodSignature} that declares the media types handled by an {@link ApiResource} for a given method. + * @param method the method + * @return the signature + */ + public static ApiMethodSignature method(Method method) { + return new ApiMethodSignature(method); + } + + private final ApiSignature signature; + + /** + * Creates an instance with a given signature. + * @param signature the signature + */ + public ApiResource(ApiSignature signature) { + + super("extension-resource", signature.mapping()); + + this.signature = signature; + } + + @Override + public Set excludes() { + return Collections.singleton(new Exclude(Constants.root_mapping+mapping())); + } + + /** + * Returns true if this resource supports a given method. + * @param method the method + * @return true if this resource supports the given method + */ + public boolean supports(Method method) { + + return signature.methods().contains(method); + } + + /** + * Returns true if this resource accepts a given media type for a given method. + * @param method the method + * @param type the media type + * @return true if this resource accepts the given media type for the given method + */ + public boolean accepts(Method method, String type) { + + Set requestTypes = signature.requestTypes().get(method); + + //if signature does not specify, we assume resource can work with anything or uses a default and is in charge + return requestTypes.isEmpty() || requestTypes.contains(type); + } + + /** + * Returns true if this resource produces a given media type for a given method. + * @param method the method + * @param type the media type + * @return true if this resource produces the given media type for the given method + */ + public boolean produces(Method method, String type) { + + if (type.contains("*")) + return true; + + Set contentTypes = signature.responseTypes().get(method); + return contentTypes.contains(type); + } + + /** + * Return the signature of this resource. + * @return the signature + */ + public ApiSignature signature() { + return signature; + } + + + //adapt to http servlet API delegating to standard implementation that throws a 405, resources ovverride in line with their signature + + @Override + public void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doHead(req, resp); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doGet(req, resp); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // TODO Auto-generated method stub + super.doPost(req, resp); + } + + @Override + public void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // TODO Auto-generated method stub + super.doPut(req, resp); + } + + @Override + public void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // TODO Auto-generated method stub + super.doDelete(req, resp); + } + + @Override + public void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // TODO Auto-generated method stub + super.doOptions(req, resp); + } + + @Override + public void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // TODO Auto-generated method stub + super.doTrace(req, resp); + } + +} diff --git a/src/main/java/org/gcube/smartgears/extensions/ApiSignature.java b/src/main/java/org/gcube/smartgears/extensions/ApiSignature.java new file mode 100644 index 0000000..7388e21 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/ApiSignature.java @@ -0,0 +1,51 @@ +package org.gcube.smartgears.extensions; + +import static org.gcube.common.events.impl.Utils.*; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.gcube.smartgears.extensions.HttpExtension.Method; + +public class ApiSignature { + + + private final String mapping; + private Set methods = new HashSet(); + private Map> requestTypes = new HashMap>(); + private Map> responseTypes = new HashMap>(); + + + public ApiSignature(String mapping) { + this.mapping=mapping; + } + + public ApiSignature with(ApiMethodSignature signature) { + + notNull("method signature",signature); + + this.methods.add(signature.method); + this.requestTypes.put(signature.method,signature.requestTypes); + this.responseTypes.put(signature.method,signature.responseTypes); + + return this; + } + + public String mapping() { + return mapping; + } + + public Set methods() { + return methods; + } + + public Map> requestTypes() { + return requestTypes; + } + + public Map> responseTypes() { + return responseTypes; + } +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/extensions/ApplicationExtension.java b/src/main/java/org/gcube/smartgears/extensions/ApplicationExtension.java new file mode 100644 index 0000000..c4216d6 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/ApplicationExtension.java @@ -0,0 +1,43 @@ +package org.gcube.smartgears.extensions; + +import java.util.Set; + +import javax.servlet.Servlet; + +import org.gcube.smartgears.configuration.application.Exclude; +import org.gcube.smartgears.context.application.ApplicationContext; + +/** + * A servlet that allows remote management of the application. + * + * @author Fabio Simeoni + * + */ +public interface ApplicationExtension extends Servlet { + + /** + * Initialises the extensions with the context of the application. + * @param context the application context + * @throws Exception if the extension cannot be initialised + */ + void init(ApplicationContext context) throws Exception; + + /** + * Returns the name of this extension. + * @return the name + */ + String name(); + + + /** + * Returns the mapping of this extension. + * @return the mapping + */ + String mapping(); + + /** + * Returns the set of request paths that should be excluded from request management. + * @return the set of request paths that should be excluded from request management + */ + Set excludes(); +} diff --git a/src/main/java/org/gcube/smartgears/extensions/HttpController.java b/src/main/java/org/gcube/smartgears/extensions/HttpController.java new file mode 100644 index 0000000..8a2d7a7 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/HttpController.java @@ -0,0 +1,245 @@ +package org.gcube.smartgears.extensions; + +import static org.gcube.smartgears.Constants.*; +import static org.gcube.smartgears.handlers.application.request.RequestError.*; +import static org.gcube.smartgears.utils.Utils.*; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.smartgears.configuration.application.Exclude; +import org.gcube.smartgears.context.application.ApplicationContext; + +/** + * An {@link HttpExtension} that dispatches to one or more {@link ApiResource}s, handling the generic, HTTP-aspects + * aspects of their client interactions + * + * @author Fabio Simeoni + * + */ +public class HttpController extends HttpExtension { + + private static final long serialVersionUID = 1L; + + private final Map resources = new HashMap(); + + + HttpController() { + } + + /** + * Creates an instance with a given name and a given mapping. + * + * @param name the name + * @param mapping the mapping + */ + public HttpController(String name, String mapping) { + super(name, mapping); + } + + /** + * Adds one ore more {@link ApiResource}s to this controller. + * + * @param resources the resources + */ + public void addResources(ApiResource... resources) { + + notNull("API resources", resources); + + for (ApiResource resource : resources) + this.resources.put(resource.mapping(), resource); + } + + @Override + public void init(ApplicationContext context) throws Exception { + super.init(context); + for (ApiResource resource : resources.values()) + resource.init(context); + + } + + @Override + public Set excludes() { + + Set resourceExcludes = new LinkedHashSet(); + + for (ApiResource resource : resources.values()) + resourceExcludes.addAll(resource.excludes()); + + return resourceExcludes; + } + + // final because we dispatch to http servlet method inside, resources can use init(Context) + @Override + public final void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException { + + HttpServletRequest request = HttpServletRequest.class.cast(req); + HttpServletResponse response = HttpServletResponse.class.cast(resp); + + ApiResource resource = resourceFor(request.getPathInfo()); + + checkMethod(resource, request, response); + + checkContentTyperHeader(resource, request, response); + checkAcceptHeader(resource, request, response); + + setContentTypeHeader(resource, request, response); + + dispatch(resource, request, response); + + + } + + // helpers + + private void setContentTypeHeader(ApiResource resource, HttpServletRequest request, HttpServletResponse response) { + + Method method = valueOf(request.getMethod()); + + Set responseTypes = resource.signature().responseTypes().get(method); + + if (responseTypes.isEmpty()) + //overridden by resources that e.g. create something and return more specific codes + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + else + if (responseTypes.size() == 1 && !response.containsHeader(content_type)) + response.addHeader(content_type, responseTypes.iterator().next()); + + + } + + + private ApiResource resourceFor(String path) { + + if (path==null) { + path = "/"; + } + else + // some tolerance for trailing slashes + if (path.length()>1 && path.endsWith("/")) + path = path.substring(0, path.length() - 1); + + ApiResource resource = resources.get(path); + + if (resource == null) + resource_notfound_error.fire(); + + return resource; + } + + + + private void checkMethod(ApiResource resource, HttpServletRequest request, HttpServletResponse response) { + + Method method = valueOf(request.getMethod()); + + if (!resource.supports(method)) { + response.addHeader(allow, toSingleString(resource.signature().methods())); + method_unsupported_error.fire("this resource does not support method " + method); + } + + } + + private void dispatch(ApiResource resource, HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + Method method = valueOf(request.getMethod()); + + // dispatches + switch (method) { + + case HEAD: + resource.doHead(request, response); + break; + case GET: + resource.doGet(request, response); + break; + case POST: + resource.doPost(request, response); + break; + case PUT: + resource.doPut(request, response); + break; + case DELETE: + resource.doDelete(request, response); + break; + case OPTIONS: + resource.doOptions(request, response); + break; + case TRACE: + resource.doTrace(request, response); + } + } + + private void checkContentTyperHeader(ApiResource resource, HttpServletRequest request, HttpServletResponse response) { + + Method method = valueOf(request.getMethod()); + + // if request specifies a media-type, we check it against signature + // if it doesn't we let it pass and let the resource apply default or complain. + String requestTypes = request.getHeader(content_type); + if (requestTypes != null) { + String type = null; + for (String value : valuesOf(requestTypes)) + if (resource.accepts(method, value)) { + type = value; + break; + } + if (type == null) + incoming_contenttype_unsupported_error.fire("this resource does not accept " + requestTypes); + } + } + + private void checkAcceptHeader(ApiResource resource, HttpServletRequest request, HttpServletResponse response) { + + Method method = valueOf(request.getMethod()); + + // check match on outgoing media type, if any + String responseType = request.getHeader(accept); + if (responseType != null) { + String type = null; + for (String value : valuesOf(responseType)) + if (resource.produces(method, value)) { + type = value; + break; + } + if (type == null) + outgoing_contenttype_unsupported_error.fire("this resource cannot produce " + responseType); + } + + } + + private String toSingleString(Collection values) { + StringBuilder builder = new StringBuilder(); + + for (Object value : values) + builder.append(value).append(","); + + String concat = builder.toString(); + return concat.substring(0, concat.length() - 1); + + } + + private String[] valuesOf(String header) { + return header.split(","); + } + + private Method valueOf(String method) { + + try { + return Method.valueOf(method); + } catch (Exception e) { + throw method_unsupported_error.toException("unsupported method " + method); + } + } +} diff --git a/src/main/java/org/gcube/smartgears/extensions/HttpExtension.java b/src/main/java/org/gcube/smartgears/extensions/HttpExtension.java new file mode 100644 index 0000000..c97a9e7 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/HttpExtension.java @@ -0,0 +1,88 @@ +package org.gcube.smartgears.extensions; + +import static org.gcube.common.events.impl.Utils.*; + +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServlet; +import javax.xml.bind.annotation.XmlAttribute; + +import org.gcube.common.validator.annotations.NotEmpty; +import org.gcube.smartgears.configuration.application.Exclude; +import org.gcube.smartgears.context.application.ApplicationContext; + +/** + * An {@link ApplicationExtension} that implements the {@link HttpServlet} interface + * + * @author Fabio Simeoni + * + */ +public abstract class HttpExtension extends HttpServlet implements ApplicationExtension { + + private static final long serialVersionUID = 1L; + + /** + * Enumeration of HTTP methods. + * + */ + public static enum Method { + GET, PUT, POST, HEAD, DELETE, OPTIONS, TRACE + } + + @XmlAttribute @NotEmpty + private String name; + + @XmlAttribute @NotEmpty + private String mapping; + + private ApplicationContext context; + + protected HttpExtension() {} + + public HttpExtension(String name, String mapping) { + + valid("extension name",name); + valid("extension mapping",name); + + name(name); + mapping(mapping); + } + + //extensions use init(context) instead + public final void init() throws javax.servlet.ServletException { + }; + + @Override + public void init(ApplicationContext context) throws Exception { + this.context=context; + } + + @Override + public Set excludes() { + return new HashSet(); //all managed by default + } + + protected ApplicationContext context() { + return context; + + } + + @Override + public String name() { + return name; + } + + public void name(String name) { + this.name = name; + } + + @Override + public String mapping() { + return mapping; + } + + public void mapping(String mapping) { + this.mapping = mapping; + } +} diff --git a/src/main/java/org/gcube/smartgears/extensions/RequestExceptionBarrier.java b/src/main/java/org/gcube/smartgears/extensions/RequestExceptionBarrier.java new file mode 100644 index 0000000..b419b7b --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/RequestExceptionBarrier.java @@ -0,0 +1,49 @@ +package org.gcube.smartgears.extensions; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.smartgears.handlers.application.request.RequestException; +import org.gcube.smartgears.utils.Utils; + +/** + * A {@link Filter} that maps {@link RequestException}s onto error responses. + * + * + * @author Fabio Simeoni + * + */ +public class RequestExceptionBarrier implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException { + + try { + + + chain.doFilter(request, response); + + + } catch (Throwable t) { + + Utils.handleError(HttpServletRequest.class.cast(request), HttpServletResponse.class.cast(response), t); + } + } + + @Override + public void destroy() { + } + +} diff --git a/src/main/java/org/gcube/smartgears/extensions/resource/ConfigurationResource.java b/src/main/java/org/gcube/smartgears/extensions/resource/ConfigurationResource.java new file mode 100644 index 0000000..0e3c939 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/resource/ConfigurationResource.java @@ -0,0 +1,44 @@ +package org.gcube.smartgears.extensions.resource; + +import static org.gcube.smartgears.Constants.application_xml; +import static org.gcube.smartgears.extensions.HttpExtension.Method.GET; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.common.resources.gcore.Resources; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.BridgedApplicationConfiguration; +import org.gcube.smartgears.extensions.ApiResource; +import org.gcube.smartgears.extensions.ApiSignature; + +/** + * An {@link ApiResource} of {@link RemoteResource} at {@link #mapping}. + * + * @author Fabio Simeoni + * + */ +public class ConfigurationResource extends ApiResource { + + private static final long serialVersionUID = 1L; + + public static final String mapping = "/configuration"; + + private static final ApiSignature signature = handles(mapping).with(method(GET).produces(application_xml)); + + ConfigurationResource() { + super(signature); + } + + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + ApplicationConfiguration config = BridgedApplicationConfiguration.class.cast(context().configuration()).inner(); + Resources.marshal(config,resp.getWriter()); + } + +} diff --git a/src/main/java/org/gcube/smartgears/extensions/resource/FrontPageResource.java b/src/main/java/org/gcube/smartgears/extensions/resource/FrontPageResource.java new file mode 100644 index 0000000..a587d0c --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/resource/FrontPageResource.java @@ -0,0 +1,152 @@ +package org.gcube.smartgears.extensions.resource; + +import static org.gcube.smartgears.Constants.application_xhtml; +import static org.gcube.smartgears.Constants.frontpage_file_path; +import static org.gcube.smartgears.extensions.HttpExtension.Method.GET; +import static org.gcube.smartgears.handlers.application.request.RequestError.application_error; +import static org.gcube.smartgears.provider.ProviderFactory.provider; +import static org.gcube.smartgears.utils.Utils.closeSafely; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.common.resources.gcore.GCoreEndpoint; +import org.gcube.common.scope.impl.ScopeBean; +import org.gcube.smartgears.extensions.ApiResource; +import org.gcube.smartgears.extensions.ApiSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An {@link ApiResource} of {@link RemoteResource} at {@link #mapping}. + * + * @author Fabio Simeoni + * + */ +public class FrontPageResource extends ApiResource { + + // the variable replacement pattern + private static Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}"); + + // log on behalf of extension + private static final Logger log = LoggerFactory.getLogger(RemoteResource.class); + + private static final long serialVersionUID = 1L; + + public static final String mapping = "/"; + + private static final ApiSignature signature = handles(mapping).with(method(GET).produces(application_xhtml)); + + FrontPageResource() { + super(signature); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + InputStream page = getClass().getResourceAsStream(frontpage_file_path); + + if (page == null) { + log.error("invalid distribution: missing {}", frontpage_file_path); + application_error.fire("invalid distribution: missing " + frontpage_file_path); + } + + Map values = values(); + + BufferedReader reader = null; + try { + + String line = null; + reader = new BufferedReader(new InputStreamReader(page)); + while ((line = reader.readLine()) != null) + resp.getWriter().write(interpolate(line, values)); + + } catch (Exception e) { + application_error.fire("could not read " + frontpage_file_path, e); + } finally { + closeSafely(reader); + } + + } + + private Map values() { + + Map values = new HashMap(); + + values.put("profile_link", ProfileResource.mapping.substring(1,ProfileResource.mapping.length())); + values.put("config_link", ConfigurationResource.mapping.substring(1,ConfigurationResource.mapping.length())); + + values.put("name", context().name()); + values.put("version", context().configuration().version()); + + String infrastructure = context().container().configuration().infrastructure(); + StringBuilder voValue = new StringBuilder(); + + Collection scopes = context().profile(GCoreEndpoint.class).scopes().asCollection(); + Set vos = new HashSet(); + + //pre-process + for (String scope : scopes) { + ScopeBean bean = new ScopeBean(scope); + switch (bean.type()) { + case INFRASTRUCTURE: + infrastructure = bean.name(); + break; + case VO: + vos.add(bean.name()); + break; + case VRE: + vos.add(bean.enclosingScope().name()); + infrastructure=bean.enclosingScope().enclosingScope().name(); + } + } + + //build vo value + int i = 0; + int max = vos.size()-1; + for (String vo : vos) { + String voPrefix = i == 0 ? "" : (i==max?" and ":", "); + voValue.append(voPrefix+"" + vo + ""); + i++; + } + + values.put("infra", infrastructure); + values.put("vos", voValue.toString()); + + values.put("status", context().lifecycle().state().toString()); + + values.put("smartgears-version", provider().smartgearsConfiguration().version()); + + return values; + } + + public static String interpolate(String text, Map replacements) { + + Matcher matcher = pattern.matcher(text); + StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + String replacement = replacements.get(matcher.group(1)); + if (replacement != null) { + matcher.appendReplacement(buffer, ""); // safer in case replacements include some of the variable + // characters + buffer.append(replacement); + } + } + matcher.appendTail(buffer); + return buffer.toString(); + } + +} diff --git a/src/main/java/org/gcube/smartgears/extensions/resource/LifecycleResource.java b/src/main/java/org/gcube/smartgears/extensions/resource/LifecycleResource.java new file mode 100644 index 0000000..072ca7f --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/resource/LifecycleResource.java @@ -0,0 +1,102 @@ +package org.gcube.smartgears.extensions.resource; + +import static org.gcube.smartgears.Constants.application_xml; +import static org.gcube.smartgears.extensions.HttpExtension.Method.GET; +import static org.gcube.smartgears.extensions.HttpExtension.Method.POST; +import static org.gcube.smartgears.handlers.application.request.RequestError.illegal_state_error; +import static org.gcube.smartgears.handlers.application.request.RequestError.invalid_request_error; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlValue; + +import org.gcube.common.resources.gcore.Resources; +import org.gcube.smartgears.extensions.ApiResource; +import org.gcube.smartgears.extensions.ApiSignature; +import org.gcube.smartgears.lifecycle.application.ApplicationState; + +/** + * An {@link ApiResource} of {@link RemoteResource} at {@link #mapping}. + * + * @author Fabio Simeoni + * + */ +public class LifecycleResource extends ApiResource { + + private static final long serialVersionUID = 1L; + + public static final String mapping = "/lifecyle"; + + private static final ApiSignature signature = handles(mapping).with(method(GET).produces(application_xml)).with( + method(POST).accepts(application_xml)); + + + LifecycleResource() { + super(signature); + } + + + @Override + public boolean supports(Method method) { + + return method == GET || method == POST; + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + try { + Resources.marshal(new State(context().lifecycle().state()), resp.getWriter()); + } + catch(Exception e) { + invalid_request_error.fire("cannot parse request body",e); + } + + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + State wrapper = Resources.unmarshal(State.class, req.getReader()); + String value = wrapper.value; + + if (value == null || value.isEmpty()) + invalid_request_error.fire("missing state in request body"); + + ApplicationState state = null; + + try { + state = ApplicationState.valueOf(value); + } + catch(Exception e) { + invalid_request_error.fire(value+" is an unkown resource state",e); + } + + try { + context().lifecycle().moveTo(state); + } + catch(Exception e) { + illegal_state_error.fire("invalid state transition for this resource"+value, e); + } + } + + // helper classes + + @XmlRootElement(name="state") + public static class State { + + @XmlValue + public String value; + + State() { + } + + public State(ApplicationState state) { + this.value=state.name(); + } + } +} diff --git a/src/main/java/org/gcube/smartgears/extensions/resource/ProfileResource.java b/src/main/java/org/gcube/smartgears/extensions/resource/ProfileResource.java new file mode 100644 index 0000000..9e76f66 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/resource/ProfileResource.java @@ -0,0 +1,42 @@ +package org.gcube.smartgears.extensions.resource; + +import static org.gcube.smartgears.Constants.application_xml; +import static org.gcube.smartgears.extensions.HttpExtension.Method.GET; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.common.resources.gcore.GCoreEndpoint; +import org.gcube.common.resources.gcore.Resources; +import org.gcube.smartgears.extensions.ApiResource; +import org.gcube.smartgears.extensions.ApiSignature; + +/** + * An {@link ApiResource} of {@link RemoteResource} at {@link #mapping}. + * + * @author Fabio Simeoni + * + */ +public class ProfileResource extends ApiResource { + + private static final long serialVersionUID = 1L; + + public static final String mapping = "/profile"; + + private static final ApiSignature signature = handles(mapping).with(method(GET).produces(application_xml)); + + ProfileResource() { + super(signature); + } + + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + Resources.marshal(context().profile(GCoreEndpoint.class),resp.getWriter()); + } + +} diff --git a/src/main/java/org/gcube/smartgears/extensions/resource/RemoteResource.java b/src/main/java/org/gcube/smartgears/extensions/resource/RemoteResource.java new file mode 100644 index 0000000..1acbf96 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/extensions/resource/RemoteResource.java @@ -0,0 +1,37 @@ +package org.gcube.smartgears.extensions.resource; + +import static org.gcube.smartgears.Constants.*; + +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.extensions.ApiResource; +import org.gcube.smartgears.extensions.HttpController; + +/** + * An {@link HttpController} for remote management of the application. + * + * @author Fabio Simeoni + * + */ +@XmlRootElement(name = remote_management) +public class RemoteResource extends HttpController { + + private static final String default_mapping = Constants.root_mapping+"/*"; + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance with its {@link ApiResource}s. + */ + public RemoteResource() { + super(remote_management, default_mapping); + addResources(new FrontPageResource(), new ConfigurationResource(), new ProfileResource(), + new LifecycleResource()); + } + + @Override + public String toString() { + return remote_management; + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/AbstractHandler.java b/src/main/java/org/gcube/smartgears/handlers/AbstractHandler.java new file mode 100644 index 0000000..ce128bc --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/AbstractHandler.java @@ -0,0 +1,20 @@ +package org.gcube.smartgears.handlers; + +/** + * Partial implementation of {@link Handler}. + * + * @author Fabio Simeoni + * + * + */ +public abstract class AbstractHandler { + + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + //so far, largely a placeholder for future cross-handler behaviour + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/Event.java b/src/main/java/org/gcube/smartgears/handlers/Event.java new file mode 100644 index 0000000..aa6e316 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/Event.java @@ -0,0 +1,35 @@ +package org.gcube.smartgears.handlers; + + +/** + * An event related to a given context. + * + * @author Fabio Simeoni + * + * @param the context type + */ +public abstract class Event { + + private final C context; + + /** + * Creates an instance with the context of the container. + * @param context the context + */ + public Event(C context) { + this.context=context; + } + + /** + * Returns the context of the container. + * @return the context + */ + public C context() { + return context; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/Handler.java b/src/main/java/org/gcube/smartgears/handlers/Handler.java new file mode 100644 index 0000000..b7c58d4 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/Handler.java @@ -0,0 +1,22 @@ +package org.gcube.smartgears.handlers; + + + +/** + * Handles events. + * + * @author Fabio Simeoni + * + * @param the event type + */ +public interface Handler { + + + /** + * Processes a given event. + * + * @param e the event + */ + void onEvent(E event); + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/Pipeline.java b/src/main/java/org/gcube/smartgears/handlers/Pipeline.java new file mode 100644 index 0000000..14723c6 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/Pipeline.java @@ -0,0 +1,78 @@ +package org.gcube.smartgears.handlers; + +import java.util.ArrayList; +import java.util.List; + +/** + * An ordered list {@link Handler}s that handle related events. + * + * @author Fabio Simeoni + * + * @param the type of events + * @param the type of handlers + * + * + */ +public abstract class Pipeline> { + + //private static Logger log = LoggerFactory.getLogger(Pipeline.class); + + private final List handlers; + private int cursor = 0; + + /** + * Creates an instance with a given list of handlers. + *

+ * Modification to the input list do not affect the pipeline. + * + * @param handlers the handlers + */ + public Pipeline(List handlers) { + this.handlers = new ArrayList(handlers); // defensive copy + } + + /** + * Returns the handlers in this pipeline. + *

+ * Modification to the input list do not affect the pipeline. + * + * @return the handlers + */ + public List handlers() { + return new ArrayList(handlers); // defensive copy + } + + /** + * Forwards an event through the pipeline. + * + * @param event the event + */ + public void forward(E event) { + + if (cursor >= handlers.size()) { + reset(); + return; + } + + + H handler = handlers.get(cursor); + + cursor++; + + //log.trace("forwarding {} to {}", event, handler); + + try { + handler.onEvent(event); + } + catch(RuntimeException e) { + reset(); + throw e; + } + + forward(event); //make sure it's the last thing we do, or it will keep acting as recursion retracts + } + + private void reset() { + cursor=0; + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/ProfileEvents.java b/src/main/java/org/gcube/smartgears/handlers/ProfileEvents.java new file mode 100644 index 0000000..caf3565 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/ProfileEvents.java @@ -0,0 +1,24 @@ +package org.gcube.smartgears.handlers; + +/** + * Profile lifetime events for container and application. + * + * @author Fabio Simeoni + * + */ +public class ProfileEvents { + + /** + * The event that signals the publication of the profile. + */ + public static final String published ="published"; + + /** + * The event that signals a change to the profile. + */ + public static final String changed ="changed"; + + public static final String addToContext = "addToContext"; + + public static final String removeFromContext = "removeFromContext"; +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/ApplicationEvent.java b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationEvent.java new file mode 100644 index 0000000..0547dd8 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationEvent.java @@ -0,0 +1,24 @@ +package org.gcube.smartgears.handlers.application; + +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.Event; + +/** + * An {@link Event} that occurs in the lifetime of the application. + + * @author Fabio Simeoni + * + * @param the type of handler that handles these events + * + */ +public abstract class ApplicationEvent> extends Event { + + /** + * Creates an instance with a given application context. + * @param context the context + */ + ApplicationEvent(ApplicationContext context) { + super(context); + } + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/ApplicationHandler.java b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationHandler.java new file mode 100644 index 0000000..73bc5b8 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationHandler.java @@ -0,0 +1,16 @@ +package org.gcube.smartgears.handlers.application; + +import org.gcube.smartgears.handlers.Handler; + +/** + * A {@link Handler} of {@link ApplicationEvent}s. + * + * @author Fabio Simeoni + * + * @param the self type of the handler. + * + * @see ApplicationEvent + */ +public interface ApplicationHandler> extends Handler> { + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/ApplicationLifecycleEvent.java b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationLifecycleEvent.java new file mode 100644 index 0000000..aebe98c --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationLifecycleEvent.java @@ -0,0 +1,49 @@ +package org.gcube.smartgears.handlers.application; + +import org.gcube.smartgears.context.application.ApplicationContext; + +/** + * An {@link ApplicationEvent} that occurs when the application starts or stops. + + * @author Fabio Simeoni + * + */ +public abstract class ApplicationLifecycleEvent extends ApplicationEvent { + + /** + * An {@link ApplicationEvent} that occurs when the application starts. + */ + public static class Start extends ApplicationLifecycleEvent { + + /** + * Creates an instance for a given {@link ApplicationContext}. + * @param context the context + */ + public Start(ApplicationContext context) { + super(context); + } + } + + /** + * An {@link ApplicationEvent} that occurs when the application stops. + */ + public static class Stop extends ApplicationLifecycleEvent { + + /** + * Creates an instance for a given {@link ApplicationContext}. + * @param context the context + */ + public Stop(ApplicationContext context) { + super(context); + } + } + + /** + * Creates an instance for a given {@link ApplicationContext}. + * @param context the context + */ + ApplicationLifecycleEvent(ApplicationContext context) { + super(context); + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/handlers/application/ApplicationLifecycleHandler.java b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationLifecycleHandler.java new file mode 100644 index 0000000..4db4b49 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationLifecycleHandler.java @@ -0,0 +1,47 @@ +package org.gcube.smartgears.handlers.application; + +import java.util.logging.Handler; + +import org.gcube.smartgears.handlers.AbstractHandler; + +/** + * A {@link Handler} of {@link ApplicationLifecycleEvent}s. + *

+ * The handler participates in a {@link ApplicationPipeline} of other handlers registered for notification of the same events. + * After processing the event, it may or may not propagate the event to the handlers further down in the {@link ApplicationPipeline} + * {@link ApplicationPipeline#forward(ApplicationEvent)}. + * @author Fabio Simeoni + * + * @see ApplicationLifecycleEvent + * @see ApplicationPipeline + */ +public abstract class ApplicationLifecycleHandler extends AbstractHandler implements ApplicationHandler { + + + /** + * Invoked when the container starts a managed app. + * @param pipeline the pipeline in which this handler is registered + * @param e the event + */ + public void onStart(ApplicationLifecycleEvent.Start e) { + + } + + /** + * Invoked when the container stops a managed app. + * @param pipeline the pipeline in which this handler is registered + * @param e the stop event + */ + public void onStop(ApplicationLifecycleEvent.Stop e) { + } + + @Override + public void onEvent(ApplicationEvent e) { + + if (e instanceof ApplicationLifecycleEvent.Start) + onStart(ApplicationLifecycleEvent.Start.class.cast(e)); + else + if (e instanceof ApplicationLifecycleEvent.Stop) + onStop(ApplicationLifecycleEvent.Stop.class.cast(e)); + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/ApplicationPipeline.java b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationPipeline.java new file mode 100644 index 0000000..220583d --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/ApplicationPipeline.java @@ -0,0 +1,42 @@ +package org.gcube.smartgears.handlers.application; + +import java.util.Collections; +import java.util.List; + +import org.gcube.smartgears.handlers.Pipeline; + +/** + * A {@link Pipeline} of {@link ApplicationHandler}s. + * + * @author Fabio Simeoni + * + * @param the type of the handlers + * + * + */ +public class ApplicationPipeline> extends Pipeline,H> { + + /** + * Creates an instance with a given list of handlers. + *

+ * Modification to the input list do not affect the pipeline. + * + * @param handlers the handlers + */ + public ApplicationPipeline(List handlers) { + super(handlers); + } + + /** + * Returns a pipeline with the same handlers as this pipeline but in reverse order. + * @return the reversed pipeline + */ + public ApplicationPipeline reverse() { + + List handlers = handlers(); // it's a copy, we're not changing this pipeline + + Collections.reverse(handlers); + + return new ApplicationPipeline(handlers); + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/RequestEvent.java b/src/main/java/org/gcube/smartgears/handlers/application/RequestEvent.java new file mode 100644 index 0000000..d219d02 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/RequestEvent.java @@ -0,0 +1,72 @@ +package org.gcube.smartgears.handlers.application; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.smartgears.context.application.ApplicationContext; + +/** + * An {@link ApplicationEvent} that occurs when an application receives a request. + * + * @author Fabio Simeoni + * + */ +public class RequestEvent extends ApplicationEvent { + + private final String servlet; + private final HttpServletRequest request; + private final HttpServletResponse response; + + + /** + * Creates an instance with the application context, the client request, and the name of the target servlet. + * + * @param context the context of the application + * @param servlet the name of the target servlet + * @param request the client request + */ + public RequestEvent(String servlet,ApplicationContext context, HttpServletRequest request, HttpServletResponse response) { + super(context); + this.request = request; + this.response=response; + this.servlet = servlet; + } + + /** + * Returns the name of the target servlet. + * + * @return the name of the servlet. + */ + public String servlet() { + return servlet; + } + + public String uri() { + String query = request().getQueryString(); + return query==null?request().getRequestURI():request().getRequestURI()+"?"+query; + } + + /** + * Returns the client request. + * + * @return the request + */ + public HttpServletRequest request() { + return request; + } + + /** + * Returns the response. + * + * @return the response + */ + public HttpServletResponse response() { + return response; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[req=" + request().getRemoteHost() + ",resp=" + + response.toString().substring(0, 12) + "]"; + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/RequestHandler.java b/src/main/java/org/gcube/smartgears/handlers/application/RequestHandler.java new file mode 100644 index 0000000..07f83e2 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/RequestHandler.java @@ -0,0 +1,68 @@ +package org.gcube.smartgears.handlers.application; + +import java.util.logging.Handler; + +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.AbstractHandler; + +/** + * A {@link Handler} of {@link RequestEvent}s and {@link ResponseEvent}s. + *

+ * The handler participates in a {@link ApplicationPipeline} of other handlers registered for notification of the same events. + * After processing the event, it may or may not propagate the event to the handlers further down in the + * {@link ApplicationPipeline} {@link ApplicationPipeline#forward(ApplicationEvent)}. + * + * @author Fabio Simeoni + * + * @see RequestEvent + * @see ResponseEvent + * @see ApplicationPipeline + */ +public abstract class RequestHandler extends AbstractHandler implements ApplicationHandler { + + abstract public String getName(); + + /** + * Initialises the handler. + * + * @param ctx the servlet context of the managed app. + */ + public void start(ApplicationContext ctx) { + } + + /** + * Invoked when the container receives a request for a servlet of a managed app. + * + * @param pipeline the pipeline in which this handler is registered + * @param e the event + */ + public void handleRequest(RequestEvent e) { + } + + /** + * Invoked when a servlet of a managed app has produced a response to a request. + * + * @param pipeline the pipeline in which this handler is registered + * @param e the event + */ + public void handleResponse(ResponseEvent e) { + } + + @Override + public void onEvent(ApplicationEvent e) { + + // mind the order here, ResponseEvent servletExcludes = Arrays.asList("default","jsp"); + + private ApplicationContext context; + + public ProfileBuilder(ApplicationContext context) { + this.context = context; + } + + public void fill(GCoreEndpoint endpoint) { + + + ApplicationConfiguration configuration = context.configuration(); + ContainerConfiguration container = context.container().configuration(); + + + endpoint.profile() + .description(configuration.description()) + .serviceName(configuration.name()) + .serviceClass(configuration.serviceClass()) + .version(configuration.version()) + .serviceId(configuration.name() + configuration.serviceClass() + configuration.version()) + .ghnId(context.container().profile(HostingNode.class).id()); + + endpoint.profile().newDeploymentData() + .activationTime(Calendar.getInstance()) + .status((context.lifecycle().state().remoteForm())); + + endpoint.profile().endpoints().clear(); + + String baseAddress; + if (configuration.proxied()){ + String protocol = container.proxyAddress().secure()? "https://": "http://"; + int port = container.proxyAddress().port(); + + baseAddress=String.format("%s%s:%d%s", protocol , container.proxyAddress().hostname(), port,context.application().getContextPath()); + } else { + String protocol = configuration.secure()? "https://": "http://"; + int port = configuration.secure()?container.securePort(): container.port(); + + baseAddress=String.format("%s%s:%d%s", protocol , container.hostname(), port,context.application().getContextPath()); + } + + for (ServletRegistration servlet : context.application().getServletRegistrations().values()) + if (!servletExcludes.contains(servlet.getName())) + for (String mapping : servlet.getMappings()) { + String address = baseAddress+(mapping.endsWith("*")?mapping.substring(0,mapping.length()-2):mapping); + endpoint.profile().endpoints().add().nameAndAddress(servlet.getName(),URI.create(address)); + } + + + } + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/lifecycle/ProfileManager.java b/src/main/java/org/gcube/smartgears/handlers/application/lifecycle/ProfileManager.java new file mode 100644 index 0000000..a897070 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/lifecycle/ProfileManager.java @@ -0,0 +1,306 @@ +package org.gcube.smartgears.handlers.application.lifecycle; + +import static org.gcube.common.events.Observes.Kind.resilient; +import static org.gcube.smartgears.Constants.profile_management; +import static org.gcube.smartgears.Constants.profile_property; +import static org.gcube.smartgears.handlers.ProfileEvents.addToContext; +import static org.gcube.smartgears.handlers.ProfileEvents.changed; +import static org.gcube.smartgears.handlers.ProfileEvents.published; +import static org.gcube.smartgears.handlers.ProfileEvents.removeFromContext; +import static org.gcube.smartgears.lifecycle.application.ApplicationLifecycle.activation; +import static org.gcube.smartgears.lifecycle.application.ApplicationLifecycle.failure; +import static org.gcube.smartgears.lifecycle.application.ApplicationLifecycle.stop; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.failed; + +import java.util.Collections; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.events.Observes; +import org.gcube.common.events.Observes.Kind; +import org.gcube.common.resources.gcore.GCoreEndpoint; +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.context.Property; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.application.ApplicationLifecycleEvent; +import org.gcube.smartgears.handlers.application.ApplicationLifecycleHandler; +import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; +import org.gcube.smartgears.lifecycle.application.ApplicationState; +import org.gcube.smartgears.lifecycle.container.ContainerLifecycle; +import org.gcube.smartgears.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * Manages the resource profile of the application. + *

+ * + * The manager: + * + *

    + *
  • creates the profile when the application starts for the first time; + *
  • loads the profile when the application restarts; + *
  • publishes the profile when the application becomes active, and at any + * lifecycle change thereafter; + *
  • stores the profile locally after each publication; + *
+ * + * @author Fabio Simeoni + * @see ProfileBuilder + * @see ProfilePublisher + */ +@XmlRootElement(name = profile_management) +public class ProfileManager extends ApplicationLifecycleHandler { + + Logger log = LoggerFactory.getLogger(ProfileManager.class); + + private ApplicationContext context; + private ProfileBuilder builder; + private ProfilePublisher publisher; + + private ScheduledFuture periodicUpdates; + + @Override + public void onStart(ApplicationLifecycleEvent.Start e) { + + context = e.context(); + builder = new ProfileBuilder(context); + + activated(); + + schedulePeriodicUpdates(); + // note we don't fire profile events, but wait for the final startup + // outcome which + // will result in a state change. only then we publish and store the + // profile + // this avoids the redundancy and performance penalty of storing and + // publishing multiple + // times in rapid succession (which would be correct). Revise if proves + // problematic in corner + // cases. + + } + + + private void activated(){ + GCoreEndpoint profile = loadOrCreateProfile(); + + share(profile); + + publisher = new ProfilePublisher(context); + + registerObservers(); + } + + // helpers + private void registerObservers() { + + context.events().subscribe(new Object() { + + @Observes({ activation, stop, failure }) + void onChanged(ApplicationLifecycle lc) { + + GCoreEndpoint profile = context.profile(GCoreEndpoint.class); + + profile.profile().deploymentData().status(lc.state().remoteForm()); + + log.debug("moving app {} to {}",context.name(), lc.state().remoteForm()); + + // since we do not know the observers, they will deal with + // failures and their consequences + // any that comes back will be logged in this event thread + context.events().fire(profile, changed); + } + + @Observes(value = published) + void shareAfterPublish(GCoreEndpoint profile) { + + share(profile); // publish may produce a new profile instance + + } + + @Observes(value = changed, kind = Kind.safe) + void publishAfterChange(GCoreEndpoint profile) { + + boolean firstPublication = profile.scopes().isEmpty(); + + //if we've failed before first publication do not try to publish + //(we may well have failed there) + try { + + if (firstPublication) { + if (context.lifecycle().state()!= failed) + publishFirstTime(profile); + } + else{ + log.debug("update app {} profile",context.name()); + publisher.update(); // if successful, triggers share. + } + } + catch (Exception e) { + + log.error("cannot publish "+context.name()+" (see details)", e); + + // since we've failed no published event is fired and profile + // will not be stored. + // we do it manually to ensure we leave some local trace of the + // changed profile. + //TODO: CHECK --- store(profile); + } + + + + } + + @Observes(value = addToContext) + void addTo(String token) { + try { + log.trace("publishing application with new token"); + publisher.addTo(Collections.singleton(token)); + publisher.update(); + }catch (Exception e) { + + log.error("cannot add token {} (see details)",token, e); + + // since we've failed no published event is fired and profile + // will not be stored. + // we do it manually to ensure we leave some local trace of the + // changed profile. + //TODO: CHECK --- store(profile); + } + + } + + @Observes(value = removeFromContext) + void removeFrom(String token) { + try { + log.trace("unpublishing application with token"); + publisher.removeFrom(Collections.singleton(token)); + publisher.update(); + }catch (Exception e) { + + log.error("cannot remove token {} (see details)",token, e); + + // since we've failed no published event is fired and profile + // will not be stored. + // we do it manually to ensure we leave some local trace of the + // changed profile. + //TODO: CHECK --- store(profile); + } + + } + }); + } + + + private void share(GCoreEndpoint profile) { + + log.trace("sharing profile for {}", context.name()); + + context.properties().add(new Property(profile_property, profile)); + } + + private void publishFirstTime(GCoreEndpoint profile) { + + try { + + publisher.addToAll(); + + } catch (Exception e) { + log.warn("publishing failed",e); + } + } + + private GCoreEndpoint loadOrCreateProfile() { + + return create(); + } + + private GCoreEndpoint create() { + + log.info("creating profile for {}", context.name()); + + try { + + GCoreEndpoint profile = new GCoreEndpoint(); + profile.setId(context.id()); + + builder.fill(profile); + + return profile; + + } catch (RuntimeException e) { + + // this is a critical startup failure: it will fail the application + throw new RuntimeException("cannot create profile for " + context.name(), e); + + } + + } + + private void schedulePeriodicUpdates() { + + // register to cancel updates + context.events().subscribe( + + new Object() { + + // we register it in response to lifecycle events so that we can stop and resume along with application + @Observes(value = { activation }, kind = resilient) + synchronized void restartPeriodicUpdates(ApplicationLifecycle lc) { + + //already running + if (periodicUpdates!=null) + return; + + if (lc.state()==ApplicationState.active) + log.info("scheduling periodic updates of application {} profile", context.name()); + + else + log.info("resuming periodic updates of application {} profile", context.name()); + + final Runnable updateTask = new Runnable() { + public void run() { + GCoreEndpoint profile = context.profile(GCoreEndpoint.class); + + //if handling of event generates failures these will be reported + //for resilience we do not fail the application + log.trace("firing change event on application {} profile", context.name()); + context.events().fire(profile,changed); + } + }; + + periodicUpdates = Utils.scheduledServicePool.scheduleAtFixedRate(updateTask, Constants.application_republish_frequency_in_minutes, Constants.application_republish_frequency_in_minutes , TimeUnit.MINUTES); + + } + + @Observes(value = { stop, failure }, kind = resilient) + synchronized void cancelPeriodicUpdates(ContainerLifecycle ignore) { + + if (periodicUpdates != null){ + log.trace("stopping periodic updates of application {} profile", context.name()); + + try { + periodicUpdates.cancel(true); + periodicUpdates=null; + } + catch(Exception e) { + log.warn("could not stop periodic updates of application {} profile", context.name(),e); + } + } + } + + }); + + } + + + @Override + public String toString() { + return profile_management; + } + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/lifecycle/ProfilePublisher.java b/src/main/java/org/gcube/smartgears/handlers/application/lifecycle/ProfilePublisher.java new file mode 100644 index 0000000..6c58cbd --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/lifecycle/ProfilePublisher.java @@ -0,0 +1,214 @@ +package org.gcube.smartgears.handlers.application.lifecycle; + +import static org.gcube.smartgears.handlers.ProfileEvents.published; +import static org.gcube.smartgears.utils.Utils.notEmpty; +import static org.gcube.smartgears.utils.Utils.rethrowUnchecked; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.gcube.common.authorization.client.proxy.AuthorizationProxy; +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; +import org.gcube.common.resources.gcore.GCoreEndpoint; +import org.gcube.informationsystem.publisher.ScopedPublisher; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.provider.ProviderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Publishes the current resource profile of the application. + *

+ *Distinguishes publication in new scopes ({@link #addTo(List)} from publication updates in existing scopes ({@link #update()}. + * + * + * @author Fabio Simeoni + * + */ +public class ProfilePublisher { + + private static final Logger log = LoggerFactory.getLogger(ProfilePublisher.class); + + //the underlying IS publisher + private final ScopedPublisher publisher; + + private final ApplicationContext context; + + private AuthorizationProxy authProxy ; + + /** + * Creates an instance for a given application. + * @param context the context of the application + */ + public ProfilePublisher(ApplicationContext context) { + this.context = context; + this.publisher=ProviderFactory.provider().publisherFor(context); + this.authProxy = ProviderFactory.provider().authorizationProxy(); + } + + /** + * Adds for the first time the current resource profile of the application in one or more scopes. + * @param scopes the scopes + */ + public void addTo(Collection tokens) { + + notEmpty("tokens",tokens); + + GCoreEndpoint profile = context.profile(GCoreEndpoint.class); + + /* TODO: reintroduce it when scope will be removed + //TODO: remove when move to new IS + Collection retainedContexts = new ArrayList(context.container().configuration().allowedContexts()); + retainedContexts.removeAll(profile.scopes().asCollection()); + profile.scopes().asCollection().addAll(retainedContexts); + + + String previousToken = SecurityTokenProvider.instance.get(); + try { + for (String token: tokens){ + log.info("creating profile with token {}", token); + SecurityTokenProvider.instance.set(token); + profile = publisher.create(profile); + SecurityTokenProvider.instance.reset(); + } + }catch (Exception e) { + rethrowUnchecked(e); + } finally{ + SecurityTokenProvider.instance.set(previousToken); + } + */ + + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + + String previousToken = SecurityTokenProvider.instance.get(); + try{//This classloader set is needed for the jaxb context + if (previousToken==null) + SecurityTokenProvider.instance.set((String)tokens.toArray()[0]); + Thread.currentThread().setContextClassLoader(ProfilePublisher.class.getClassLoader()); + profile = publisher.create(profile, resolveScopesFromTokens(tokens)); + + } catch (Exception e) { + rethrowUnchecked(e); + } finally{ + SecurityTokenProvider.instance.set(previousToken); + Thread.currentThread().setContextClassLoader(contextCL); + } + + sharePublished(profile); + } + + public void addToAll() { + this.addTo(context.configuration().startTokens()); + } + + + public void update() { + + + GCoreEndpoint profile = context.profile(GCoreEndpoint.class); + + /* TODO: reintroduce it when scope will be removed + String previousToken = SecurityTokenProvider.instance.get(); + try { + + for (String token: context.configuration().startTokens()){ + SecurityTokenProvider.instance.set(token); + profile = publisher.update(profile); + SecurityTokenProvider.instance.reset(); + } + + } + catch (Exception e) { + rethrowUnchecked(e); + } finally{ + SecurityTokenProvider.instance.set(previousToken); + } + */ + + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + + String previousToken = SecurityTokenProvider.instance.get(); + try{//This classloader set is needed for the jaxb context + if (previousToken==null) + SecurityTokenProvider.instance.set((String)context.configuration().startTokens().toArray()[0]); + + Thread.currentThread().setContextClassLoader(ProfilePublisher.class.getClassLoader()); + profile = publisher.update(profile); + + } catch (Exception e) { + rethrowUnchecked(e); + } finally{ + SecurityTokenProvider.instance.set(previousToken); + Thread.currentThread().setContextClassLoader(contextCL); + } + + sharePublished(profile); + } + + + /** + * Removes the application from one or more scopes. + * @param scopes the scopes + */ + public void removeFrom(Collection tokens) { + + GCoreEndpoint profile = context.profile(GCoreEndpoint.class); + + /* TODO: reintroduce it when scope will be removed + String previousToken = SecurityTokenProvider.instance.get(); + try { + + for (String token: tokens){ + SecurityTokenProvider.instance.set(token); + profile = publisher.remove(profile); + SecurityTokenProvider.instance.reset(); + } + + } + catch (Exception e) { + + rethrowUnchecked(e); + + } finally{ + SecurityTokenProvider.instance.set(previousToken); + } + */ + + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + + String previousToken = SecurityTokenProvider.instance.get(); + try{//This classloader set is needed for the jaxb context + if (previousToken==null) + SecurityTokenProvider.instance.set((String)tokens.toArray()[0]); + Thread.currentThread().setContextClassLoader(ProfilePublisher.class.getClassLoader()); + profile = publisher.remove(profile, resolveScopesFromTokens(tokens)); + + } catch (Exception e) { + rethrowUnchecked(e); + } finally{ + SecurityTokenProvider.instance.set(previousToken); + Thread.currentThread().setContextClassLoader(contextCL); + } + log.debug("after remove application profile contains scopes {}",profile.scopes().asCollection()); + sharePublished(profile); + } + + + + private void sharePublished(GCoreEndpoint profile) { + context.events().fire(profile,published); + } + + private List resolveScopesFromTokens(Collection tokens){ + List scopes = new ArrayList(tokens.size()); + for (String token: tokens) + try{ + scopes.add(this.authProxy.get(token).getContext()); + }catch (Exception e) { + log.warn("error retrieving token {} , it should never happen",token); + } + return scopes; + } + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/request/RequestAccounting.java b/src/main/java/org/gcube/smartgears/handlers/application/request/RequestAccounting.java new file mode 100644 index 0000000..c8138ff --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/request/RequestAccounting.java @@ -0,0 +1,95 @@ +package org.gcube.smartgears.handlers.application.request; + +import static org.gcube.smartgears.Constants.called_method_header; + +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.accounting.datamodel.UsageRecord.OperationResult; +import org.gcube.accounting.datamodel.usagerecords.ServiceUsageRecord; +import org.gcube.accounting.persistence.AccountingPersistence; +import org.gcube.accounting.persistence.AccountingPersistenceFactory; +import org.gcube.common.authorization.library.provider.AuthorizationProvider; +import org.gcube.common.authorization.library.provider.CalledMethodProvider; +import org.gcube.common.scope.api.ScopeProvider; +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.application.RequestEvent; +import org.gcube.smartgears.handlers.application.RequestHandler; +import org.gcube.smartgears.handlers.application.ResponseEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@XmlRootElement(name = Constants.request_accounting) +public class RequestAccounting extends RequestHandler { + + private static Logger log = LoggerFactory.getLogger(RequestAccounting.class); + + private static ThreadLocal startCallThreadLocal = new ThreadLocal(); + + + @Override + public String getName() { + return Constants.request_accounting; + } + + @Override + public void handleRequest(RequestEvent e) { + ApplicationContext context = e.context(); + String calledMethod = e.request().getHeader(called_method_header); + if (calledMethod==null){ + calledMethod = e.request().getRequestURI().substring(e.request().getContextPath().length()); + if (calledMethod.startsWith("/")) + calledMethod = calledMethod.replaceFirst("/",""); + } + CalledMethodProvider.instance.set(calledMethod); + String caller = AuthorizationProvider.instance.get()!=null? AuthorizationProvider.instance.get().getClient().getId(): "UNKNOWN"; + startCallThreadLocal.set(System.currentTimeMillis()); + log.info("REQUEST START ON {}:{}({}) CALLED FROM {}@{} IN SCOPE {} ", + context.configuration().name(),context.configuration().serviceClass(), CalledMethodProvider.instance.get(), + caller, e.request().getRemoteHost(), ScopeProvider.instance.get()); + } + + @Override + public void handleResponse(ResponseEvent e) { + ApplicationContext context = e.context(); + String caller = AuthorizationProvider.instance.get()!=null? AuthorizationProvider.instance.get().getClient().getId(): "UNKNOWN"; + String callerQualifier = AuthorizationProvider.instance.get()!=null? AuthorizationProvider.instance.get().getTokenQualifier(): "UNKNOWN"; + //retieves caller Ip when there is a proxy + String callerIp = e.request().getHeader("x-forwarded-for"); + if(callerIp==null) + callerIp=e.request().getRemoteHost(); + + generateAccounting(caller,callerQualifier,callerIp==null?"UNKNOWN":callerIp , context); + + log.info("REQUEST SERVED ON {}:{}({}) CALLED FROM {}@{} IN SCOPE {} FINISHED IN {} millis", + context.configuration().name(),context.configuration().serviceClass(), CalledMethodProvider.instance.get(), + caller, callerIp, ScopeProvider.instance.get(), System.currentTimeMillis()-startCallThreadLocal.get()); + startCallThreadLocal.remove(); + CalledMethodProvider.instance.reset(); + } + + void generateAccounting(String caller, String callerQualifier, String remoteHost, ApplicationContext context){ + AccountingPersistenceFactory.setFallbackLocation(context.container().persistence().location()); + AccountingPersistence persistence = AccountingPersistenceFactory.getPersistence(); + ServiceUsageRecord serviceUsageRecord = new ServiceUsageRecord(); + try{ + + serviceUsageRecord.setConsumerId(caller); + serviceUsageRecord.setCallerQualifier(callerQualifier); + serviceUsageRecord.setScope(ScopeProvider.instance.get()); + serviceUsageRecord.setServiceClass(context.configuration().serviceClass()); + serviceUsageRecord.setServiceName(context.configuration().name()); + + serviceUsageRecord.setHost(context.container().configuration().hostname()+":"+context.container().configuration().port()); + serviceUsageRecord.setCalledMethod(CalledMethodProvider.instance.get()); + serviceUsageRecord.setCallerHost(remoteHost); + serviceUsageRecord.setOperationResult(OperationResult.SUCCESS); + serviceUsageRecord.setDuration(System.currentTimeMillis()-startCallThreadLocal.get()); + persistence.account(serviceUsageRecord); + + }catch(Exception ex){ + log.warn("invalid record passed to accounting ",ex); + } + } + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/request/RequestError.java b/src/main/java/org/gcube/smartgears/handlers/application/request/RequestError.java new file mode 100644 index 0000000..cb194a9 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/request/RequestError.java @@ -0,0 +1,122 @@ +package org.gcube.smartgears.handlers.application.request; + +import javax.servlet.http.HttpServletResponse; + + +/** + * Known error types. + *

+ * Each type can throw a corresponding {@link RequestException}. + * + * @author Fabio Simeoni + * + */ +public enum RequestError { + + /** + * The error raised when requests are made to failed applications. + */ + application_failed_error(HttpServletResponse.SC_GONE,"this resource is not currently available"), + + /** + * The error raised when requests are made to stopped applications. + */ + application_unavailable_error(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "this resource is permanently available"), + + + + /** + * The error raised when requests are made to unknown resources. + */ + resource_notfound_error(HttpServletResponse.SC_NOT_FOUND, "no gCube resource at this URI"), + + + /** + * The error raised when requests require illegal resource state transitions. + */ + illegal_state_error(HttpServletResponse.SC_CONFLICT, "this resource cannot assume the required state"), + + + /** + * The error raised when requests are made with unsupported HTTP methods. + */ + method_unsupported_error(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "this resource does not support this method"), + + /** + * The error raised when requests are genrically invalid. + */ + invalid_request_error(HttpServletResponse.SC_BAD_REQUEST, "this resource cannot process this request because it is malformed"), + + request_not_authorized_error(HttpServletResponse.SC_UNAUTHORIZED, "this resource cannot process this request because it needs authorization"), + + internal_server_error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "unexpected error"), + + + /** + * The error raised when requests carry an unsupported media type. + */ + incoming_contenttype_unsupported_error(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "this resource cannot consume this media type"), + + + /** + * The error raised when requests request an unsupported media type. + */ + outgoing_contenttype_unsupported_error(HttpServletResponse.SC_NOT_ACCEPTABLE, "this resource cannot produce this media type"), + + + /** + * An error raised by managed applications. + */ + application_error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "this resource has incurred in a generic error"); + + + private final int code; + private final String msg; + + private RequestError(int code,String msg) { + this.code=code; + this.msg=msg; + } + + public int code() { + return code; + } + + public String message() { + return msg; + } + + public void fire() { + throw toException(); + } + + public void fire(String msg) { + throw toException(msg); + } + + public void fire(Throwable cause) { + throw toException(cause); + } + + public void fire(String msg,Throwable cause) { + throw toException(msg,cause); + } + + public RequestException toException() { + return new RequestException(this); + } + + public RequestException toException(String msg) { + return new RequestException(this, msg); + } + + public RequestException toException(Throwable cause) { + return new RequestException(this,cause); + } + + public RequestException toException(String msg,Throwable cause) { + return new RequestException(this,cause,msg); + } + + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/request/RequestException.java b/src/main/java/org/gcube/smartgears/handlers/application/request/RequestException.java new file mode 100644 index 0000000..79170b8 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/request/RequestException.java @@ -0,0 +1,66 @@ +package org.gcube.smartgears.handlers.application.request; + + +/** + * Thrown for the occurrence of an error during request processing. + * + * @author Fabio Simeoni + * + */ +public class RequestException extends RuntimeException { + + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final RequestError error; + + /** + * Creates an instance with an underlying error. + * @param error the error + */ + public RequestException(RequestError error) { + this(error, error.message()); + } + + /** + * Creates an instance with an underling error and a custom message. + * @param message the message + * @param error the error + */ + public RequestException(RequestError error,String message) { + super(message); + this.error=error; + } + + /** + * Creates an instance with an underlying error and an underlying cause + * @param error the error + * @param cause the cause; + */ + public RequestException(RequestError error,Throwable cause) { + this(error, cause, error.message()); + } + + /** + * Creates an instance with an underlying error, an underlying cause, and an underlying message. + * @param error the error + * @param cause the cause; + * @Param message the message; + */ + public RequestException(RequestError error,Throwable cause,String message) { + super(message,cause); + this.error=error; + } + + /** + * Returns the underlying error. + * @return the error + */ + public RequestError error() { + return error; + } + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/application/request/RequestValidator.java b/src/main/java/org/gcube/smartgears/handlers/application/request/RequestValidator.java new file mode 100644 index 0000000..41e2938 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/application/request/RequestValidator.java @@ -0,0 +1,190 @@ +package org.gcube.smartgears.handlers.application.request; + +import static org.gcube.common.authorization.client.Constants.authorizationService; +import static org.gcube.smartgears.Constants.request_validation; +import static org.gcube.smartgears.Constants.scope_header; +import static org.gcube.smartgears.Constants.token_header; +import static org.gcube.smartgears.handlers.application.request.RequestError.application_failed_error; +import static org.gcube.smartgears.handlers.application.request.RequestError.application_unavailable_error; +import static org.gcube.smartgears.handlers.application.request.RequestError.internal_server_error; +import static org.gcube.smartgears.handlers.application.request.RequestError.invalid_request_error; + +import java.io.IOException; + +import javax.xml.bind.DatatypeConverter; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.authorization.client.exceptions.ObjectNotFound; +import org.gcube.common.authorization.library.AuthorizationEntry; +import org.gcube.common.authorization.library.PolicyUtils; +import org.gcube.common.authorization.library.policies.Policy; +import org.gcube.common.authorization.library.provider.AuthorizationProvider; +import org.gcube.common.authorization.library.provider.ClientInfo; +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; +import org.gcube.common.authorization.library.provider.ServiceIdentifier; +import org.gcube.common.authorization.library.provider.UserInfo; +import org.gcube.common.authorization.library.utils.Caller; +import org.gcube.common.scope.api.ScopeProvider; +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.application.RequestEvent; +import org.gcube.smartgears.handlers.application.RequestHandler; +import org.gcube.smartgears.handlers.application.ResponseEvent; +import org.gcube.smartgears.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@XmlRootElement(name = Constants.request_validation) +public class RequestValidator extends RequestHandler { + + private static Logger log = LoggerFactory.getLogger(RequestValidator.class); + + private ApplicationContext context; + + @Override + public String getName() { + return Constants.request_validation; + } + + @Override + public void handleRequest(RequestEvent call) { + + log.trace("executing request validator ON REQUEST"); + + context = call.context(); + + validateAgainstLifecycle(call); + + if (!validateToken(call)){ + String scope = call.request().getParameter(scope_header)==null? call.request().getHeader(scope_header):call.request().getParameter(scope_header); + validateScope(scope); + log.info("received call to {} in context {}",call.uri(),scope); + } + + } + + @Override + public void handleResponse(ResponseEvent e){ + SecurityTokenProvider.instance.reset(); + AuthorizationProvider.instance.reset(); + ScopeProvider.instance.reset(); + log.debug("resetting all the Thread local for this call."); + } + + private void validateAgainstLifecycle(RequestEvent call) { + + switch(context.lifecycle().state()) { + + case stopped : + application_unavailable_error.fire(); break; + + case failed: + application_failed_error.fire(); break; + + default: + //nothing to do, but avoids warnings + } + + + } + + private void validateScope(String scope) { + + if (scope == null) { + log.warn("rejecting unscoped call to {}",context.name()); + invalid_request_error.fire("call is unscoped"); + } + + if (!context.container().configuration().allowedContexts().contains(scope)) { + log.warn("rejecting call to {} in invalid context {}, allowed context are {}",context.name(),scope,context.container().configuration().allowedContexts()); + invalid_request_error.fire(context.name()+" cannot be called in scope "+scope); + } + + ScopeProvider.instance.set(scope); + } + + private boolean validateToken(RequestEvent call){ + + + String token = call.request().getParameter(token_header)==null? call.request().getHeader(token_header):call.request().getParameter(token_header); + + String scope = call.request().getParameter(scope_header)==null? call.request().getHeader(scope_header):call.request().getParameter(scope_header); + + if (token == null && scope==null){ + log.warn("rejecting call to {}, authorization required",context.name(),token); + if (call.context().container().configuration().authenticationEnpoint()==null){ + if (call.request().getHeader(Constants.authorization_header)!=null){ + String basicAuthorization = call.request().getHeader(Constants.authorization_header); + String base64Credentials = basicAuthorization.substring("Basic".length()).trim(); + String credentials = new String(DatatypeConverter.parseBase64Binary(base64Credentials)); + // credentials = username:password + final String[] values = credentials.split(":",2); + token = values[1]; + ClientInfo info = retreiveAndSetInfo(token, call); + if (!(info instanceof UserInfo) || !info.getId().equals(values[0])) { + log.warn("rejecting call to {}, username {} not valid for token {}",context.name(),values[0],token); + RequestError.request_not_authorized_error.fire(context.name()+": username "+values[0]+" not valid for token "+token); + } + return true; + }else { + log.warn("rejecting call to {}, authorization required",context.name(),token); + RequestError.request_not_authorized_error.fire(context.name()+": authorization required"); + } + }else { + log.info("authorization enpoint found on configuration, redirecting the call"); + String recallLocation = String.format("http://%s:%d%s", call.context().container().configuration().hostname(), call.context().container().configuration().port(), call.uri()); + //call.response().setHeader("Allowed-Contexts", call.context().container().configuration().allowedContexts().toString()); + try { + call.response().sendRedirect(context.container().configuration().authenticationEnpoint()+"?Recall-Location="+recallLocation); + } catch (IOException e) { + log.error("errror redirecting call",e ); + } + return false; + } + + } + + log.trace("token is "+token); + + if (token!=null){ + retreiveAndSetInfo(token, call); + return true; + } + log.info("invalid token, checking the context"); + return false; + } + + @Override + public String toString() { + return request_validation; + } + + private ClientInfo retreiveAndSetInfo(String token, RequestEvent call){ + log.info("accessing validator with token {} ", token); + AuthorizationEntry authEntry = null; + try{ + authEntry = authorizationService().get(token); + }catch(ObjectNotFound onf){ + log.warn("rejecting call to {}, invalid token {}",context.name(),token); + invalid_request_error.fire(context.name()+" invalid token : "+token); + }catch(Exception e){ + log.error("error contacting authorization service",e); + internal_server_error.fire("error contacting authorization service"); + } + + ServiceIdentifier serviceIdentifier = Utils.getServiceInfo(call.context()).getServiceIdentifier(); + + for (Policy policy: authEntry.getPolicies()) + if (PolicyUtils.isPolicyValidForClient(policy.getServiceAccess(), serviceIdentifier)){ + log.error("rejecting call to {} : {} is not allowed to contact the service ",context.name(),authEntry.getClientInfo().getId()); + invalid_request_error.fire("rejecting call to "+context.name()+": "+authEntry.getClientInfo().getId()+" is not allowed to contact the service"); + } + + AuthorizationProvider.instance.set(new Caller(authEntry.getClientInfo(), authEntry.getQualifier())); + validateScope(authEntry.getContext()); + log.info("retrieved request authorization info {} in scope {} ", AuthorizationProvider.instance.get(), authEntry.getContext()); + SecurityTokenProvider.instance.set(token); + return authEntry.getClientInfo(); + } + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/container/ContainerHandler.java b/src/main/java/org/gcube/smartgears/handlers/container/ContainerHandler.java new file mode 100644 index 0000000..1704546 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/container/ContainerHandler.java @@ -0,0 +1,41 @@ +package org.gcube.smartgears.handlers.container; + + + +import org.gcube.smartgears.handlers.AbstractHandler; +import org.gcube.smartgears.handlers.Handler; + +/** + * A {@link Handler} of {@link ContainerLifecycleEvent}s. + * + * @author Fabio Simeoni + * + */ +public abstract class ContainerHandler extends AbstractHandler implements Handler { + + + /** + * Invoked when the container starts. + * @param e the event + */ + public void onStart(ContainerLifecycleEvent.Start e) { + + } + + /** + * Invoked when the container stops. + * @param e the stop event + */ + public void onStop(ContainerLifecycleEvent.Stop e) { + } + + @Override + public void onEvent(ContainerLifecycleEvent e) { + + if (e instanceof ContainerLifecycleEvent.Start) + onStart(ContainerLifecycleEvent.Start.class.cast(e)); + else + if (e instanceof ContainerLifecycleEvent.Stop) + onStop(ContainerLifecycleEvent.Stop.class.cast(e)); + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/container/ContainerLifecycleEvent.java b/src/main/java/org/gcube/smartgears/handlers/container/ContainerLifecycleEvent.java new file mode 100644 index 0000000..f848001 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/container/ContainerLifecycleEvent.java @@ -0,0 +1,50 @@ +package org.gcube.smartgears.handlers.container; + +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.handlers.Event; + +/** + * An {@link Event} that occurs in the lifetime of the container. + + * @author Fabio Simeoni + * + */ +public abstract class ContainerLifecycleEvent extends Event { + + /** + * An {@link ContainerLifecycleEvent} that occurs when the container starts. + */ + public static class Start extends ContainerLifecycleEvent { + + /** + * Creates an instance for a given {@link ContainerContext}. + * @param context the context + */ + public Start(ContainerContext context) { + super(context); + } + } + + /** + * An {@link ContainerLifecycleEvent} that occurs when the container stops. + */ + public static class Stop extends ContainerLifecycleEvent { + + /** + * Creates an instance for a given {@link ContainerContext}. + * @param context the context + */ + public Stop(ContainerContext context) { + super(context); + } + } + + /** + * Creates an instance with the context of the container. + * @param context the context + */ + ContainerLifecycleEvent(ContainerContext context) { + super(context); + } + +} diff --git a/src/main/java/org/gcube/smartgears/handlers/container/ContainerPipeline.java b/src/main/java/org/gcube/smartgears/handlers/container/ContainerPipeline.java new file mode 100644 index 0000000..0430b37 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/container/ContainerPipeline.java @@ -0,0 +1,40 @@ +package org.gcube.smartgears.handlers.container; + +import java.util.Collections; +import java.util.List; + +import org.gcube.smartgears.handlers.Pipeline; + +/** + * A {@link Pipeline} of {@link ContainerHandler}s. + * + * @author Fabio Simeoni + * + * + */ +public class ContainerPipeline extends Pipeline { + + /** + * Creates an instance with a given list of handlers. + *

+ * Modification to the input list do not affect the pipeline. + * + * @param handlers the handlers + */ + public ContainerPipeline(List handlers) { + super(handlers); + } + + /** + * Returns a pipeline with the same handlers as this pipeline but in reverse order. + * @return the reversed pipeline + */ + public ContainerPipeline reverse() { + + List handlers = handlers(); // it's a copy, we're not changing this pipeline + + Collections.reverse(handlers); + + return new ContainerPipeline(handlers); + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/AccountingManager.java b/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/AccountingManager.java new file mode 100644 index 0000000..cf33c86 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/AccountingManager.java @@ -0,0 +1,36 @@ +/** + * + */ +package org.gcube.smartgears.handlers.container.lifecycle; + +import static org.gcube.smartgears.Constants.accounting_management; + +import java.util.concurrent.TimeUnit; + +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.accounting.persistence.AccountingPersistenceFactory; +import org.gcube.smartgears.handlers.container.ContainerHandler; +import org.gcube.smartgears.handlers.container.ContainerLifecycleEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) http://www.lucafrosini.com/ + */ +@XmlRootElement(name = accounting_management) +public class AccountingManager extends ContainerHandler { + + private static Logger logger = LoggerFactory.getLogger(AccountingManager.class); + + @Override + public void onStop(ContainerLifecycleEvent.Stop e) { + logger.trace("Going to flush accounting data"); + AccountingPersistenceFactory.flushAll(1000, TimeUnit.MILLISECONDS); + } + + @Override + public String toString() { + return accounting_management; + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfileBuilder.java b/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfileBuilder.java new file mode 100644 index 0000000..d12080d --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfileBuilder.java @@ -0,0 +1,366 @@ +package org.gcube.smartgears.handlers.container.lifecycle; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.gcube.common.resources.gcore.HostingNode; +import org.gcube.common.resources.gcore.HostingNode.Profile.NodeDescription.GHNType; +import org.gcube.common.resources.gcore.HostingNode.Profile.NodeDescription.Processor; +import org.gcube.common.resources.gcore.HostingNode.Profile.NodeDescription.Variable; +import org.gcube.common.resources.gcore.utils.Group; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.configuration.library.SmartGearsConfiguration; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.provider.ProviderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Fabio Simeoni + * @author Luca Frosini (ISTI - CNR) http://www.lucafrosini.com/ + * + */ +public class ProfileBuilder { + + private static Logger log = LoggerFactory.getLogger(ProfileBuilder.class); + + private ContainerContext context; + + public ProfileBuilder(ContainerContext context) { + this.context = context; + } + + public HostingNode create() { + + HostingNode node = new HostingNode(); + + ContainerConfiguration cfg = context.configuration(); + + node.newProfile().infrastructure(cfg.infrastructure()); + + addSiteTo(node); + + String ip = null; + try { + ip = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.warn("unable to detect the IP address of the host"); + } + + node.profile().newDescription().activationTime(Calendar.getInstance()).name(cfg.hostname() + ":" + cfg.port()); + + node.profile().description().networkAdapters().add().mtu(0).name("local-adapter").ipAddress(ip).inboundIP("") + .outboundIP(""); + + node.profile().description().newOperatingSystem().name(System.getProperty("os.name")) + .version(System.getProperty("os.version")).release(""); + + node.profile().description().newArchitecture().platformType(System.getProperty("os.arch")).smpSize(0) + .smtSize(0); + + ArrayList> info = cpuInfo(); + + Group processors = node.profile().description().processors(); + + for (HashMap map : info) + + processors.add().bogomips(new BigDecimal(map.get("bogomips"))) + .clockSpeedMhz(new BigDecimal(map.get("cpu_MHz"))).family(map.get("cpu_family")) + .modelName(map.get("model_name")).model(map.get("model")).vendor(map.get("vendor_id")) + .cacheL1(new Integer(map.get("cache_size"))).cacheL1D(0).cacheL1I(0).cacheL2(0); + + addVariablesTo(node); + + update(node,false); + + node.profile().description().type(GHNType.Static); + // String type = (String) context.getProperty(GHNContext.GHN_TYPE, false); + // if (type.compareToIgnoreCase(Type.DYNAMIC.toString()) == 0) description.setType(Description.Type.Dynamic); + // else if (type.compareToIgnoreCase(Type.STATIC.toString()) == 0) description.setType(Description.Type.Static); + // else if (type.compareToIgnoreCase(Type.SELFCLEANING.toString()) == 0) + // description.setType(Description.Type.Selfcleaning); + // + // file system + node.profile().description().localFileSystems().add().name("").type("").readOnly(false) + .root(cfg.persistence().location()); + + return node; + } + + @SuppressWarnings("all") + private ArrayList> cpuInfo() { + + ArrayList> map = new ArrayList>(); + + File file = new File("/proc/cpuinfo"); + + if (!file.exists()) { + log.warn("cannot acquire CPU info (no /proc/cpuinfo)"); + return map; + } + + BufferedReader input = null; + + try { + input = new BufferedReader(new FileReader(file)); + + String line = null; + + HashMap currentProcessor = null; + + while ((line = input.readLine()) != null) { + + if ((line.startsWith("processor"))) { // add the current processor to the map + + if (currentProcessor != null) + map.add((HashMap) currentProcessor.clone()); + + currentProcessor = new HashMap(); + } + + try { + if (line.contains("vendor_id")) + currentProcessor.put("vendor_id", line.split(":")[1].trim()); + } catch (Exception ex) { + } + try { + if (line.contains("cpu family")) + currentProcessor.put("cpu_family", line.split(":")[1].trim()); + } catch (Exception ex) { + } + try { + if ((line.contains("model\t")) || (line.contains("model\b"))) + currentProcessor.put("model", line.split(":")[1].trim()); + } catch (Exception ex) { + } + try { + if (line.contains("model name")) + currentProcessor.put("model_name", line.split(":")[1].trim()); + } catch (Exception ex) { + } + try { + if (line.contains("cpu MHz")) + currentProcessor.put("cpu_MHz", line.split(":")[1].trim()); + } catch (Exception ex) { + } + try { + if (line.contains("cache size")) + currentProcessor.put("cache_size", line.split(":")[1].trim().split(" ")[0]); + } catch (Exception ex) { + } + try { + if (line.contains("bogomips")) + currentProcessor.put("bogomips", line.split(":")[1].trim()); + } catch (Exception ex) { + } + } + + if (currentProcessor != null) + map.add(currentProcessor); + + } catch (Exception e) { + + log.warn("unable to acquire CPU info", e); + + } finally { + + if (input != null) + try { + input.close(); + } catch (IOException e) { + log.warn("unable to close stream", e); + } + } + return map; + } + + private long getFreeSpace() { + long free = 0; + try { + free = Files.getFileStore(Paths.get(context.configuration().persistence().location())).getUsableSpace()/1024; + } catch (IOException ioe) { + log.warn("unable to detect the free space on the disk", ioe); + } + return free; + } + + public void update(HostingNode node,boolean onLoad) { + + ContainerConfiguration cfg = context.configuration(); + + if (onLoad) { + + log.info("updating ghn profile"); + + node.profile().description().activationTime(Calendar.getInstance()).name(cfg.hostname() + ":" + cfg.port()); + + addVariablesTo(node); + + addSiteTo(node); + + } + + node.profile().description().status(context.lifecycle().state().remoteForm()); + + Map mem = memoryUsage(); + + node.profile().description().newMainMemory().ramAvailable(mem.get("MemoryAvailable")) + .ramSize(mem.get("MemoryTotalSize")).virtualAvailable(mem.get("VirtualAvailable")) + .virtualSize(mem.get("VirtualSize")); + + node.profile().description().localAvailableSpace(getFreeSpace()); + + node.profile().description().uptime(uptime()); + + node.profile().description().lastUpdate(Calendar.getInstance()); + + Map loads = loadStatistics(); + + node.profile().description().newLoad().lastMin(loads.get("1min") == null ? 0 : loads.get("1min")) + .last5Mins(loads.get("5mins") == null ? 0 : loads.get("5mins")) + .last15Mins(loads.get("15mins") == null ? 0 : loads.get("15mins")); + + } + + private void addSiteTo(HostingNode node) { + + ContainerConfiguration cfg = context.configuration(); + + node.profile().newSite().country(cfg.site().country()).location(cfg.site().location()) + .latitude(cfg.site().latitude()).longitude(cfg.site().longitude()).domain(domainIn(cfg.hostname())); + } + + private void addVariablesTo(HostingNode node) { + + ContainerConfiguration cfg = context.configuration(); + + Group variables = node.profile().description().environmentVariables(); + + // Cleaning variables to avoid duplicates + variables.removeAll(node.profile().description().environmentVariables()); + + Map map = new HashMap(); + map.putAll(cfg.properties()); + map.putAll(System.getenv()); + + for (Map.Entry entry : map.entrySet()) { + String varname = entry.getKey(); + if ((varname.compareToIgnoreCase("CLASSPATH") == 0) || (varname.compareToIgnoreCase("PATH") == 0) + || (varname.contains("SSH")) || (varname.contains("MAIL")) + || (varname.compareToIgnoreCase("LS_COLORS") == 0)) + continue; + variables.add().keyAndValue(entry.getKey(), entry.getValue()); + } + + /* The following code is useless can be removed + Map envvars = new HashMap(); + for (String varname : envvars.keySet()) { + + // a bit of filtering + if ((varname.compareToIgnoreCase("CLASSPATH") == 0) || (varname.compareToIgnoreCase("PATH") == 0) + || (varname.contains("SSH")) || (varname.contains("MAIL")) + || (varname.compareToIgnoreCase("LS_COLORS") == 0)) + continue; + + variables.add().keyAndValue(varname, envvars.get(varname)); + } + */ + + + variables.add().keyAndValue("Java", System.getProperty("java.version")); + + SmartGearsConfiguration config = ProviderFactory.provider().smartgearsConfiguration(); + variables.add().keyAndValue("SmartGears",config.version()); + + variables.add().keyAndValue("ghn-update-interval-in-secs", String.valueOf(cfg.publicationFrequency())); + + } + + public String uptime() { + String lines = "", linetemp = null; + try { + Process p = Runtime.getRuntime().exec("uptime"); + p.waitFor(); + BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); + while ((linetemp = input.readLine()) != null) + lines += linetemp; + input.close(); + p.destroy(); + lines = lines.split(",")[0].split("up")[1].trim(); + } catch (Exception e) { + log.warn("unable to detect the uptime of this machine", e); + lines = "unable to detect"; + } + return lines; + } + + public Map loadStatistics() { + + Map result = new HashMap(); + try { + File loadadv = new File("/proc/loadavg"); + if (loadadv.exists()) { + Reader reader = new FileReader(loadadv); + int c; + StringBuilder content = new StringBuilder(); + while ((c = reader.read()) != -1) + content.append((char) c); + reader.close(); + Pattern p = Pattern.compile("^(.*?)\\s{1}(.*?)\\s{1}(.*?)\\s{1}(.*)$"); + Matcher matcher = p.matcher(content.toString()); + if ((matcher.matches()) && (matcher.groupCount() > 3)) { + result.put("1min", new Double(matcher.group(1))); + result.put("5mins", new Double(matcher.group(2))); + result.put("15mins", new Double(matcher.group(3).split("\\s")[0])); + } + } + } catch (Exception ioe) { + log.warn("unable to detect the load values of this machine", ioe); + } + return result; + } + + @SuppressWarnings("all") + public Map memoryUsage() { + Map map = new HashMap(); + java.lang.management.OperatingSystemMXBean mxbean = java.lang.management.ManagementFactory + .getOperatingSystemMXBean(); + com.sun.management.OperatingSystemMXBean sunmxbean = (com.sun.management.OperatingSystemMXBean) mxbean; + long freeMemory = sunmxbean.getFreePhysicalMemorySize() / 1048576; // in MB + long availableMemory = sunmxbean.getTotalPhysicalMemorySize() / 1048576; // in MB + map.put("MemoryAvailable", freeMemory); + map.put("MemoryTotalSize", availableMemory); + long ramVirtualAvailable = Runtime.getRuntime().freeMemory() / 1048576; // in MB + long ramVirtualSize = Runtime.getRuntime().totalMemory() / 1048576; // in MB + map.put("VirtualAvailable", ramVirtualAvailable); + map.put("VirtualSize", ramVirtualSize); + return map; + } + + private String domainIn(String hostname) { + Pattern pattern = Pattern.compile("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})"); + java.util.regex.Matcher regexMatcher = pattern.matcher(hostname); + if (regexMatcher.matches()) //it's an IP address, nothing to trim + return hostname; + String[] tokens = hostname.split("\\."); + if (tokens.length < 2) + return hostname; + else + return tokens[tokens.length-2]+ "." + tokens[tokens.length-1]; + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfileManager.java b/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfileManager.java new file mode 100644 index 0000000..2bcfce1 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfileManager.java @@ -0,0 +1,289 @@ +package org.gcube.smartgears.handlers.container.lifecycle; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.gcube.common.events.Observes.Kind.critical; +import static org.gcube.common.events.Observes.Kind.resilient; +import static org.gcube.smartgears.Constants.container_profile_property; +import static org.gcube.smartgears.Constants.profile_management; +import static org.gcube.smartgears.handlers.ProfileEvents.addToContext; +import static org.gcube.smartgears.handlers.ProfileEvents.changed; +import static org.gcube.smartgears.handlers.ProfileEvents.published; +import static org.gcube.smartgears.handlers.ProfileEvents.removeFromContext; +import static org.gcube.smartgears.lifecycle.container.ContainerLifecycle.activation; +import static org.gcube.smartgears.lifecycle.container.ContainerLifecycle.failure; +import static org.gcube.smartgears.lifecycle.container.ContainerLifecycle.part_activation; +import static org.gcube.smartgears.lifecycle.container.ContainerLifecycle.shutdown; +import static org.gcube.smartgears.lifecycle.container.ContainerLifecycle.stop; +import static org.gcube.smartgears.lifecycle.container.ContainerState.active; + +import java.util.Collections; +import java.util.concurrent.ScheduledFuture; + +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.events.Observes; +import org.gcube.common.resources.gcore.HostingNode; +import org.gcube.smartgears.context.Property; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.handlers.container.ContainerHandler; +import org.gcube.smartgears.handlers.container.ContainerLifecycleEvent.Start; +import org.gcube.smartgears.lifecycle.container.ContainerLifecycle; +import org.gcube.smartgears.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * Manages the resource profile of the container. + *

+ * + * The manager: + * + *

    + *
  • creates the profile when the container starts for the first time; + *
  • loads the profile when the container restarts; + *
  • publishes the profile when the container becomes active, and at any lifecycle change thereafter; + *
  • stores the profile locally after each publication; + *
+ * + * @author Fabio Simeoni + * @see ProfileBuilder + */ +@XmlRootElement(name = profile_management) +public class ProfileManager extends ContainerHandler { + + private static Logger log = LoggerFactory.getLogger(ProfileManager.class); + + private ContainerContext context; + + private ProfileBuilder builder; + private ProfilePublisher publisher; + + + private ScheduledFuture periodicUpdates; + + @Override + public void onStart(Start e) { + + context = e.context(); + builder = new ProfileBuilder(context); + + activated(); + + // note we don't fire profile events, but wait for the final startup response which + // will result in a state change. only then we publish and store the profile + // this avoids the redundancy and performance penalty of storing and publishing multiple + // times in rapid succession (which would be correct). Revise if proves problematic in corner + // cases. + + } + + private void activated(){ + HostingNode profile = loadOrCreateProfile(); + + share(profile); + + publisher = new ProfilePublisher(context); + + registerObservers(); + + schedulePeriodicUpdates(); + } + + private void registerObservers() { + + context.events().subscribe(new Object() { + + @Observes({ activation, part_activation, shutdown, stop, failure }) + void onChanged(ContainerLifecycle lc) { + + HostingNode profile = context.profile(HostingNode.class); + + profile.profile().description().status(lc.state().remoteForm()); + + // since we do not know the observers, they will deal with failures and their consequences + // any that comes back will be logged in this event thread + context.events().fire(profile, changed); + + } + + + @Observes(value = published) + void shareAfterPublish(HostingNode profile) { + + share(profile); // publish may produce a new profile instance + + } + + @Observes(value = changed, kind = critical) + void publishAfterChange(HostingNode profile) { + log.info("Publish after profile Change event called"); + publish(profile); // if successful, triggers share and store. + + } + + @Observes(value = addToContext) + void addTo(String token) { + try { + log.trace("publishing container with new token"); + publisher.addTo(Collections.singleton(token)); + publisher.update(); + }catch (Exception e) { + + log.error("cannot add token {} (see details)",token, e); + + // since we've failed no published event is fired and profile + // will not be stored. + // we do it manually to ensure we leave some local trace of the + // changed profile. + //TODO: CHECK --- store(profile); + } + + } + + @Observes(value = removeFromContext) + void removeFrom(String token) { + try { + log.trace("unpublishing container with new token"); + publisher.removeFrom(Collections.singleton(token)); + publisher.update(); + }catch (Exception e) { + + log.error("cannot remove token {} (see details)",token, e); + + // since we've failed no published event is fired and profile + // will not be stored. + // we do it manually to ensure we leave some local trace of the + // changed profile. + //TODO: CHECK --- store(profile); + } + + } + }); + } + + private HostingNode loadOrCreateProfile() { + + return createProfile(); + + } + + private void share(HostingNode profile) { + + log.trace("sharing container profile"); + context.properties().add(new Property(container_profile_property, profile)); + } + + private HostingNode createProfile() { + + log.info("creating container profile"); + + try { + HostingNode node = builder.create(); + node.setId(context.id()); + return node; + } catch (Throwable e) { + + // this is a critical startup failure: it will fail the application + throw new RuntimeException("cannot create container profile", e); + + } + + } + + private void publish(HostingNode profile) { + + //ContainerConfiguration configuration = context.configuration(); + + // first-publication vs. routine publication: when we delete scopes let's make sure there is + // at least one left of it will be re-triggered + boolean firstPublication = profile.scopes().isEmpty(); + + try { + + if (firstPublication) + publisher.addToAll(); + else + publisher.update(); + + } catch (Exception e) { + + log.error("cannot publish container (see details)", e); + + // since we've failed no published event is fired and profile will not be stored. + // we do it manually to ensure we leave some local trace of the changed profile. + //store(profile); + + } + } + + private void schedulePeriodicUpdates() { + + // register to cancel updates + context.events().subscribe( + + new Object() { + + // we register it in response to lifecycle events so that we can stop and resume along with application + @Observes(value = { activation, part_activation }, kind = resilient) + synchronized void restartPeriodicUpdates(ContainerLifecycle lc) { + + //already running + if (periodicUpdates!=null) + return; + + if (lc.state()==active) + log.info("scheduling periodic updates of container profile"); + + else + log.info("resuming periodic updates of container profile"); + + final Runnable updateTask = new Runnable() { + public void run() { + HostingNode profile = context.profile(HostingNode.class); + + try { + builder.update(profile, false); + } + catch(Exception e) { + //we may fail in the update of the profile + log.error("cannot complete periodic update of container profile",e); + } + + //if handling of event generates failures these will be reported + //for resilience we do not fail the application + log.trace("firing change event on container profile"); + context.events().fire(profile,changed); + } + }; + + periodicUpdates = Utils.scheduledServicePool.scheduleAtFixedRate(updateTask, 3, context.configuration() + .publicationFrequency(), SECONDS); + + } + + @Observes(value = { stop, failure, shutdown }, kind = resilient) + synchronized void cancelPeriodicUpdates(ContainerLifecycle ignore) { + + if (periodicUpdates != null){ + log.trace("stopping periodic updates of container profile"); + + try { + periodicUpdates.cancel(true); + periodicUpdates=null; + } + catch(Exception e) { + log.warn("could not stop periodic updates of container profile",e); + } + } + } + + }); + + } + + @Override + public String toString() { + return profile_management; + } +} diff --git a/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfilePublisher.java b/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfilePublisher.java new file mode 100644 index 0000000..d3933ab --- /dev/null +++ b/src/main/java/org/gcube/smartgears/handlers/container/lifecycle/ProfilePublisher.java @@ -0,0 +1,233 @@ +package org.gcube.smartgears.handlers.container.lifecycle; + +import static org.gcube.smartgears.utils.Utils.notEmpty; +import static org.gcube.smartgears.utils.Utils.rethrowUnchecked; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.gcube.common.authorization.client.proxy.AuthorizationProxy; +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; +import org.gcube.common.resources.gcore.HostingNode; +import org.gcube.informationsystem.publisher.ScopedPublisher; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.handlers.ProfileEvents; +import org.gcube.smartgears.provider.ProviderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Publishes the resource profile of the container. + *

+ * Distinguishes publication in new scopes ({@link #addTo(List)} from publication updates in existing scopes ({@link #update(List)}. + * + * @author Fabio Simeoni + * + */ +public class ProfilePublisher { + + private static final Logger log = LoggerFactory.getLogger(ProfilePublisher.class); + + //the underlying IS publisher + private final ScopedPublisher publisher; + //private final AuthorizationProvider authorization; + private final ContainerContext context; + + private AuthorizationProxy authProxy ; + + /** + * Creates an instance for the container. + * @param context the context of the application + */ + public ProfilePublisher(ContainerContext context) { + this.context = context; + this.publisher=ProviderFactory.provider().publisherFor(context); + this.authProxy = ProviderFactory.provider().authorizationProxy(); + } + + /** + * Adds the current resource profile of the application in one or more + * scopes. The scopes are retrieved from tokens + * @param tokens the tokens + */ + public void addTo(Collection tokens) { + + notEmpty("tokens",tokens); + + log.info("publishing container with tokens {}", tokens); + + HostingNode profile = context.profile(HostingNode.class); + + /* TODO: reintroduce it when scope will be removed + //TODO: remove when move to new IS + Collection retainedContexts = new ArrayList(context.configuration().allowedContexts()); + retainedContexts.removeAll(profile.scopes().asCollection()); + profile.scopes().asCollection().addAll(retainedContexts); + + log.trace("profile scopes on create are {} ",profile.scopes().asCollection()); + + String previousToken = SecurityTokenProvider.instance.get(); + + try { + for (String token: tokens){ + log.info("creating profile with token {}", token); + SecurityTokenProvider.instance.set(token); + profile = publisher.create(profile); + SecurityTokenProvider.instance.reset(); + } + + update(); + + } catch (Exception e) { + log.warn("error adding scopes",e); + rethrowUnchecked(e); + + } finally{ + SecurityTokenProvider.instance.set(previousToken); + }*/ + + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + + String previousToken = SecurityTokenProvider.instance.get(); + try{//This classloader set is needed for the jaxb context + if (previousToken==null) + SecurityTokenProvider.instance.set((String)tokens.toArray()[0]); + Thread.currentThread().setContextClassLoader(ProfilePublisher.class.getClassLoader()); + profile = publisher.create(profile, resolveScopesFromTokens(tokens)); + } catch (Exception e) { + rethrowUnchecked(e); + } finally { + SecurityTokenProvider.instance.set(previousToken); + Thread.currentThread().setContextClassLoader(contextCL); + } + + sharePublished(profile); + + } + + /** + * Adds the current resource profile of the application in one or more scopes. + */ + public void addToAll() { + addTo(context.configuration().startTokens()); + } + + /** + * Updates the current resource profile of the application in its current scopes. + */ + public void update() { + + HostingNode profile = context.profile(HostingNode.class); + /* TODO: reintroduce it when scope will be removed + Collection tokens = context.configuration().startTokens(); + + log.info("updating container with tokens {}", tokens); + + String previousToken = SecurityTokenProvider.instance.get(); + + try { + for (String token: tokens){ + SecurityTokenProvider.instance.set(token); + profile = publisher.update(profile); + SecurityTokenProvider.instance.reset(); + } + sharePublished(profile); + + } + catch (Exception e) { + log.warn("error updating container",e); + rethrowUnchecked(e); + + } finally{ + SecurityTokenProvider.instance.set(previousToken); + }*/ + + + log.debug("[update] resource scopes are : {} ",profile.scopes().asCollection()); + + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + + String previousToken = SecurityTokenProvider.instance.get(); + try{//This classloader set is needed for the jaxb context + if (previousToken==null) + SecurityTokenProvider.instance.set((String)context.configuration().startTokens().toArray()[0]); + + Thread.currentThread().setContextClassLoader(ProfilePublisher.class.getClassLoader()); + profile = publisher.update(profile); + } catch (Exception e) { + rethrowUnchecked(e); + } finally { + SecurityTokenProvider.instance.set(previousToken); + Thread.currentThread().setContextClassLoader(contextCL); + } + + sharePublished(profile); + } + + /** + * Removes the container from one or more scopes. + * @param tokens the tokens + */ + public void removeFrom(Collection tokens) { + + HostingNode profile = context.profile(HostingNode.class); + + log.info("removing container with tokens {}", tokens); + + /* TODO: reintroduce it when scope will be removed + String previousToken = SecurityTokenProvider.instance.get(); + + try { + + for (String token: tokens){ + SecurityTokenProvider.instance.set(token); + profile = publisher.remove(profile); + SecurityTokenProvider.instance.reset(); + } + + update(); + + } + catch (Exception e) { + log.warn("error removing scopes",e); + rethrowUnchecked(e); + + } finally{ + SecurityTokenProvider.instance.set(previousToken); + } */ + + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + + String previousToken = SecurityTokenProvider.instance.get(); + try{//This classloader set is needed for the jaxb context + if (previousToken==null) + SecurityTokenProvider.instance.set((String)tokens.toArray()[0]); + Thread.currentThread().setContextClassLoader(ProfilePublisher.class.getClassLoader()); + profile = publisher.remove(profile, resolveScopesFromTokens(tokens)); + } catch (Exception e) { + rethrowUnchecked(e); + } finally { + SecurityTokenProvider.instance.set(previousToken); + Thread.currentThread().setContextClassLoader(contextCL); + } + + log.debug("after remove container profile contains scopes {}",profile.scopes().asCollection()); + sharePublished(profile); + } + + private void sharePublished(HostingNode profile) { + context.events().fire(profile,ProfileEvents.published); + } + + private List resolveScopesFromTokens(Collection tokens){ + List scopes = new ArrayList(tokens.size()); + for (String token: tokens) + try{ + scopes.add(this.authProxy.get(token).getContext()); + }catch (Exception e) { + log.warn("error retrieving token {} , it should never happen",token); + } + return scopes; + } +} diff --git a/src/main/java/org/gcube/smartgears/lifecycle/DefaultLifecycle.java b/src/main/java/org/gcube/smartgears/lifecycle/DefaultLifecycle.java new file mode 100644 index 0000000..2181547 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/lifecycle/DefaultLifecycle.java @@ -0,0 +1,63 @@ +package org.gcube.smartgears.lifecycle; + +import org.gcube.common.events.Hub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultLifecycle> implements Lifecycle { + + private static final Logger log = LoggerFactory.getLogger(Lifecycle.class); + + private final Hub hub; + private final String name; + private S currentState; + private S previousState; + + public DefaultLifecycle(Hub hub, String name, S startState){ + this.hub = hub; + this.currentState = startState; + this.name = name; + } + + @Override + public S state() { + return currentState; + } + + @Override + public void moveTo(S next) { + + if (next == currentState) + return; + + if (currentState.next().contains(next)){ + + this.previousState = this.currentState; + this.currentState = next; + + log.trace("{} transitioned from {} to {}",new Object[]{name,previousState,currentState}); + + hub.fire(this, currentState.event()); + + } + else throw new IllegalStateException("cannot transition "+name+" from " +currentState+" to "+next); + + } + + @Override + public boolean tryMoveTo(S next) { + try { + moveTo(next); + return true; + } + catch(IllegalStateException ignore) { + return false; + } + + } + + public S previous() { + return this.previousState; + } + +} diff --git a/src/main/java/org/gcube/smartgears/lifecycle/Lifecycle.java b/src/main/java/org/gcube/smartgears/lifecycle/Lifecycle.java new file mode 100644 index 0000000..7ec16bb --- /dev/null +++ b/src/main/java/org/gcube/smartgears/lifecycle/Lifecycle.java @@ -0,0 +1,39 @@ +package org.gcube.smartgears.lifecycle; + +/** + * The lifecycle of an application managed as a gCube service. + * + * @author Fabio Simeoni + * + */ +public interface Lifecycle> { + + /** + * Returns the state from which this lifecycle transitioned to its current state. + * @return the previous state + */ + S previous(); + + /** + * Returns the current state of this lifecycle. + * @return the current state. + */ + S state(); + + /** + * Transition this lifecycle to a given state. + * @param state the state + * + * @throws IllegalStateException if the transition is illegal for this lifecycle + */ + void moveTo(S state); + + + /** + * Attempts to transition this lifecycle to a given state, but does not fail if the transition is illegal for this lifecycle. + * @param state the state + * @return true if this lifecycle has transitioned to the given state + */ + boolean tryMoveTo(S state); + +} diff --git a/src/main/java/org/gcube/smartgears/lifecycle/State.java b/src/main/java/org/gcube/smartgears/lifecycle/State.java new file mode 100644 index 0000000..7bb1fde --- /dev/null +++ b/src/main/java/org/gcube/smartgears/lifecycle/State.java @@ -0,0 +1,24 @@ +package org.gcube.smartgears.lifecycle; + +import java.util.List; + +public interface State> { + + /** + * Returns the list of states to which services can transition to from this state. + * @return the states + */ + public abstract List next(); + + /** + * Returns the event corresponding to this state. + * @return the + */ + public String event(); + + /** + * Returns a serialisation of this state for exchange purposes. + * @return + */ + public String remoteForm(); +} diff --git a/src/main/java/org/gcube/smartgears/lifecycle/application/ApplicationLifecycle.java b/src/main/java/org/gcube/smartgears/lifecycle/application/ApplicationLifecycle.java new file mode 100644 index 0000000..556014e --- /dev/null +++ b/src/main/java/org/gcube/smartgears/lifecycle/application/ApplicationLifecycle.java @@ -0,0 +1,38 @@ +package org.gcube.smartgears.lifecycle.application; + +import org.gcube.common.events.Hub; +import org.gcube.smartgears.lifecycle.DefaultLifecycle; +import org.gcube.smartgears.lifecycle.Lifecycle; + +/** + * The {@link Lifecycle} of an application. + * + * @author Fabio Simeoni + * + */ +public class ApplicationLifecycle extends DefaultLifecycle { + + /** + * The event qualifier that correspond to the {@link ApplicationState#started} state of the service lifecycle. + */ + public static final String start = "start"; + + /** + * The event qualifier that correspond to the {@link ApplicationState#active} state of the service lifecycle. + */ + public static final String activation = "activation"; + + /** + * The event qualifier that correspond to the {@link ApplicationState#started#stopped} state of the service lifecycle. + */ + public static final String stop = "stop"; + + /** + * The event qualifier that correspond to the {@link ApplicationState#failed} state of the service lifecycle. + */ + public static final String failure = "failure"; + + public ApplicationLifecycle(Hub hub, String name){ + super(hub,name,ApplicationState.started); + } +} diff --git a/src/main/java/org/gcube/smartgears/lifecycle/application/ApplicationState.java b/src/main/java/org/gcube/smartgears/lifecycle/application/ApplicationState.java new file mode 100644 index 0000000..11d40a6 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/lifecycle/application/ApplicationState.java @@ -0,0 +1,95 @@ +package org.gcube.smartgears.lifecycle.application; + +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.gcube.smartgears.lifecycle.application.ApplicationLifecycle.*; + +import java.util.List; + +import org.gcube.smartgears.lifecycle.State; + +/** + * The state and state transitions of the lifecycle of an application managed as a gCube service. + * + * @author Fabio Simeoni + * + */ +public enum ApplicationState implements State { + + /** + * The state of applications that are in the process of initialisation. + */ + started(start){ + + public List next() { + return asList(active, failed); + } + + + @Override + public String remoteForm() { + return "STARTED"; + } + }, + + /** + * The state of applications that have completed initialisation and can accept client requests. + */ + active(activation) { + + public List next() { + return asList(failed,stopped); + } + + + @Override + public String remoteForm() { + return "ready"; + } + }, + + /** + * The state of applications that have can no longer accept requests, even though they may in the future. + */ + stopped(stop){ + + public List next() { + return asList(failed,active); + } + + + @Override + public String remoteForm() { + return "down"; + } + }, + + /** + * The permanent state of applications that has encountered some fatal failure and can no longer accept requests. + */ + failed(failure){ + + public List next() { + return emptyList(); + } + + + @Override + public String remoteForm() { + return "failed"; + } + }; + + private final String event; + + //used internally + ApplicationState(String event) { + this.event=event; + } + + @Override + public String event() { + return event; + } + +} diff --git a/src/main/java/org/gcube/smartgears/lifecycle/container/ContainerLifecycle.java b/src/main/java/org/gcube/smartgears/lifecycle/container/ContainerLifecycle.java new file mode 100644 index 0000000..8a6caff --- /dev/null +++ b/src/main/java/org/gcube/smartgears/lifecycle/container/ContainerLifecycle.java @@ -0,0 +1,58 @@ +package org.gcube.smartgears.lifecycle.container; + +import org.gcube.common.events.Hub; +import org.gcube.smartgears.lifecycle.DefaultLifecycle; +import org.gcube.smartgears.lifecycle.Lifecycle; + +/** + * The {@link Lifecycle} of the application container. + * + * @author Fabio Simeoni + * + */ +public class ContainerLifecycle extends DefaultLifecycle { + + /** + * The event qualifier that correspond to the {@link ContainerState#pending} state of the container lifecycle. + */ + public static final String pending = "pending"; + + /** + * The event qualifier that correspond to the {@link ContainerState#started} state of the container lifecycle. + */ + public static final String start = "start"; + + /** + * The event qualifier that correspond to the {@link ContainerState#partActive} state of the container lifecycle. + */ + public static final String activation = "activation"; + + /** + * The event qualifier that correspond to the {@link ContainerState#active} state of the container lifecycle. + */ + public static final String part_activation = "part_activation"; + + /** + * The event qualifier that correspond to the {@link ContainerState#stopped} state of the container lifecycle. + */ + public static final String stop = "stop"; + + /** + * The event qualifier that correspond to the {@link ContainerState#stopped} state of the container lifecycle. + */ + public static final String shutdown = "shutdown"; + + /** + * The event qualifier that correspond to the {@link ContainerState#fa} state of the container lifecycle. + */ + public static final String failure = "failure"; + + /** + * Creates an instance with an event {@link Hub}. + * + * @param hub the event hub + */ + public ContainerLifecycle(Hub hub){ + super(hub,"container",ContainerState.started); + } +} diff --git a/src/main/java/org/gcube/smartgears/lifecycle/container/ContainerState.java b/src/main/java/org/gcube/smartgears/lifecycle/container/ContainerState.java new file mode 100644 index 0000000..9d5a2ba --- /dev/null +++ b/src/main/java/org/gcube/smartgears/lifecycle/container/ContainerState.java @@ -0,0 +1,135 @@ +package org.gcube.smartgears.lifecycle.container; + +import static java.util.Arrays.*; +import static org.gcube.smartgears.lifecycle.container.ContainerLifecycle.*; + +import java.util.Collections; +import java.util.List; + +import org.gcube.smartgears.lifecycle.State; + +/** + * The state and state transitions of the lifecycle of an application managed as a gCube service. + * + * @author Fabio Simeoni + * + */ +public enum ContainerState implements State { + + /** + * The state of a container that is in the process of pre authorization. + */ + notAuthorized(pending){ + + public List next() { + return asList(active,failed); + } + + @Override + public String remoteForm() { + return "started"; + } + }, + + /** + * The state of a container that is in the process of initialisation. + */ + started(start){ + + public List next() { + return asList(active,failed); + } + + @Override + public String remoteForm() { + return "started"; + } + }, + + /** + * The state of a container in which not all applications are active. + */ + partActive(part_activation) { + + public List next() { + return asList(active,stopped,down); + } + + @Override + public String remoteForm() { + return "ready"; + } + }, + + /** + * The state of a container in which all applications are active. + */ + active(activation) { + + public List next() { + return asList(partActive,stopped,down); + } + + @Override + public String remoteForm() { + return "certified"; + } + }, + + /** + * The state of a container in which applications can no longer accept requests, even though they may in the future. + */ + stopped(stop){ + + public List next() { + return asList(partActive,active); + } + + @Override + public String remoteForm() { + return "down"; + } + }, + + /** + * The state of a container which has been explicitly shutdown. + */ + down(shutdown){ + + public List next() { + return asList(partActive,active); + } + + @Override + public String remoteForm() { + return "down"; + } + }, + + /** + * The state of a container that has not completed initialisation. + */ + failed(failure) { + + public List next() { + return Collections.emptyList(); + } + + @Override + public String remoteForm() { + return "failed"; + } + }; + + private final String event; + + //used internally + ContainerState(String event) { + this.event=event; + } + + @Override + public String event() { + return event; + } +} diff --git a/src/main/java/org/gcube/smartgears/managers/ApplicationManager.java b/src/main/java/org/gcube/smartgears/managers/ApplicationManager.java new file mode 100644 index 0000000..b4f9ad8 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/managers/ApplicationManager.java @@ -0,0 +1,363 @@ +package org.gcube.smartgears.managers; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.gcube.common.events.Observes.Kind.critical; +import static org.gcube.smartgears.Constants.context_attribute; +import static org.gcube.smartgears.Constants.profile_file_path; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.active; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.failed; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.stopped; +import static org.gcube.smartgears.provider.ProviderFactory.provider; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.ObjectOutputStream; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.servlet.FilterRegistration; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletRegistration; + +import org.gcube.common.authorization.client.proxy.AuthorizationProxy; +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; +import org.gcube.common.events.Observes; +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.configuration.application.ApplicationExtensions; +import org.gcube.smartgears.configuration.application.ApplicationHandlers; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.extensions.ApplicationExtension; +import org.gcube.smartgears.extensions.RequestExceptionBarrier; +import org.gcube.smartgears.handlers.ProfileEvents; +import org.gcube.smartgears.handlers.application.ApplicationLifecycleEvent; +import org.gcube.smartgears.handlers.application.ApplicationLifecycleHandler; +import org.gcube.smartgears.handlers.application.ApplicationPipeline; +import org.gcube.smartgears.handlers.application.RequestHandler; +import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; +import org.gcube.smartgears.lifecycle.container.ContainerLifecycle; +import org.gcube.smartgears.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Coordinates management of an application as a gCube resource. + * + * @author Fabio Simeoni + * + */ +public class ApplicationManager { + + private static Logger log = LoggerFactory.getLogger(ApplicationManager.class); + + private ApplicationPipeline lifecyclePipeline; + + private ApplicationContext context; + + /** + * Starts application management. + * + * @param container + * @param application the context of the application + * @return the context of the application + */ + public ApplicationContext start(ContainerContext container, ServletContext application) { + + try { + + context = provider().contextFor(container, application); + + context.configuration().validate(); + + if (context.configuration().secure() && + container.configuration().securePort()==null) + throw new IllegalStateException( + String.format("Application %s cannot be managed because is declared as secure without a secure connector port declared in the container", context.application().getContextPath())); + + + context.configuration().startTokens(generateTokensForApplication(container)); + + saveApplicationState(); + + // make context available to application in case it is gcube-aware + shareContextWith(application); + + // prepare for events as early as possible + registerObservers(); + + ApplicationHandlers handlers = provider().handlersFor(context); + handlers.validate(); + + + + ApplicationExtensions extensions = provider().extensionsFor(context); + extensions.validate(); + + List lifecycleHandlers = handlers.lifecycleHandlers(); + List requestHandlers = handlers.requestHandlers(); + + + log.trace("managing {} lifecycle with {}", context.name(), lifecycleHandlers); + log.trace("managing {} requests with {}", context.name(), requestHandlers); + log.trace("extending {} API with {}", context.name(), extensions); + + // order is important here: first add APIs + register(extensions); + + // then intercept them all + register(requestHandlers); + + // start lifecycle management + start(lifecycleHandlers); + + //adding the context name to the configuration + context.configuration().context(application.getContextPath()); + + // we're in business + context.lifecycle().moveTo(active); + + return context; + + } catch (RuntimeException e) { + + log.error("error starting application {} ", context.name(),e); + + if (context != null) + context.lifecycle().moveTo(failed); + + throw e; + } + + } + + private Set generateTokensForApplication(ContainerContext container){ + log.info("generating token for app {}",context.configuration().name()); + Set tokens = new HashSet(); + AuthorizationProxy authProxy = provider().authorizationProxy(); + for (String containerToken :container.configuration().startTokens()) + tokens.add(generateApplicationToken(containerToken, authProxy)); + return tokens; + } + + private String generateApplicationToken(String containerToken, AuthorizationProxy authProxy){ + SecurityTokenProvider.instance.set(containerToken); + try { + log.info("generating token for app {} with container token {} ",context.configuration().name(), containerToken); + return authProxy.generateServiceToken(Utils.getServiceInfo(context)); + } catch (Exception e) { + throw new RuntimeException("error contacting authorization service",e); + } finally{ + SecurityTokenProvider.instance.reset(); + } + + } + + + private void saveApplicationState() { + File file = context.configuration().persistence().file(profile_file_path); + try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))){ + oos.writeObject(context.id()); + }catch (Exception e) { + log.error("error serializing application {} state", context.name()); + throw new RuntimeException(e); + } + + } + + /** + * Stops application management. + * + */ + public void stop() { + + if (context == null) + return; + + log.info("stopping {} management", context.name()); + + try { + + context.lifecycle().tryMoveTo(stopped); + + context.events().fire(context, ApplicationLifecycle.stop); + + stopLifecycleHandlers(); + + log.info("stopping application events for {}", context.name()); + + context.events().stop(); + + } catch (Exception e) { + + log.warn("cannot stop {} management (see cause)", context.name(), e); + } + + } + + private void register(List rqHandlers) { + + ServletContext app = context.application(); + + // attach filters based on request pipeline to each servlet + Collection servlets = app.getServletRegistrations().values(); + + for (ServletRegistration servlet : servlets) { + + String name = servlet.getName(); + + if (name.equals("default") || name.equals("jsp")) // skip page-resolving servlets + continue; + + for (String mapping : servlet.getMappings()) { + + RequestManager requestFilter = new RequestManager(context, name, rqHandlers); + + FilterRegistration.Dynamic filter = app.addFilter(name + "-filter-"+mapping.replaceAll("/", ""), requestFilter); + + log.trace("filter {} for requestfilter {} in contextPath {} is null ?? {} ",name ,requestFilter, mapping, (filter==null)); + + filter.addMappingForUrlPatterns(null, false, mapping); + } + } + } + + private void register(ApplicationExtensions extensions) { + + ServletContext application = context.application(); + + for (ApplicationExtension extension : extensions.extensions()) + + try { + + extension.init(context); + + //register excludes + context.configuration().excludes().addAll(extension.excludes()); + + String mapping = extension.mapping(); + + application.addServlet(context.configuration().name() + "-" + extension.name(), extension) + .addMapping(mapping); + + // adds a filter to map request exceptions onto error responses, + // repeating for our extensions what we already do for our filters. + // we do not interfere with error management of native application servlets + RequestExceptionBarrier barrier = new RequestExceptionBarrier(); + FilterRegistration.Dynamic filter = application.addFilter("exception-barrier", barrier); + filter.addMappingForUrlPatterns(null, false, mapping); + + log.info("registered API extension {} @ {}", extension.name(), mapping); + + } catch (Exception e) { + + throw new RuntimeException("cannot register API extension " + extension.name(), e); + } + + } + + private void start(List handlers) { + + try { + + lifecyclePipeline = new ApplicationPipeline(handlers); + + lifecyclePipeline.forward(new ApplicationLifecycleEvent.Start(context)); + + } catch (RuntimeException e) { + context.lifecycle().tryMoveTo(failed); + throw e; + } + } + + private void stopLifecycleHandlers() { + + if (lifecyclePipeline == null) + return; + + // copy pipeline, flip it, and + ApplicationPipeline returnPipeline = lifecyclePipeline.reverse(); + + // start lifetime pipeline in inverse order with stop event + returnPipeline.forward(new ApplicationLifecycleEvent.Stop(context)); + + } + + private void registerObservers() { + Object observer = new Object() { + + @Observes(value = ContainerLifecycle.stop, kind = critical) + void onStopOf(ContainerLifecycle ignore) { + + if (!context.lifecycle().tryMoveTo(stopped)) + log.warn("cannot stop {} after container has stopped", context.name()); + } + + @Observes(value = ContextEvents.ADD_TOKEN_TO_APPLICATION, kind = critical) + void onAddToken(String containerToken) { + log.trace("event add received with token {} ",containerToken); + String appToken = generateApplicationToken(containerToken, provider().authorizationProxy()); + context.configuration().startTokens().add(appToken); + log.trace("app token created : {} ", appToken); + context.events().fire(appToken, ProfileEvents.addToContext); + context.events().fire(appToken, Constants.token_registered); + } + + @Observes(value = ContextEvents.REMOVE_TOKEN_FROM_APPLICATION, kind = critical) + void onRemoveToken(String containerToken) { + log.trace("event remove received with token {} ",containerToken); + String appToken = generateApplicationToken(containerToken, provider().authorizationProxy()); + context.configuration().startTokens().remove(appToken); + log.trace("app token removed : {} ", appToken); + context.events().fire(appToken, ProfileEvents.removeFromContext); + context.events().fire(appToken, Constants.token_removed); + } + + }; + + context.container().events().subscribe(observer); + + // we cleanup when container stops + context.application().addListener(new ServletContextListener() { + + @Override + public void contextInitialized(ServletContextEvent sce) { + log.info("initilizing context {} ",context.name()); + context.events().fire(context.application().getContextPath(), ApplicationLifecycle.activation); + log.info("webApp {} initialized ",context.name()); + } + + //when the container shuts down we go down + @Override + public void contextDestroyed(ServletContextEvent sce) { + + try { + + log.info("stopping {} on undeployment|shutdown",context.name()); + + stop(); + + log.info("suspending undeployment|shutdow to allow {} to stop gracefully",context.name()); + + SECONDS.sleep(3); + + log.info("resuming undeployment|shutdow after stopping {}",context.name()); + + } + catch (InterruptedException e) { + log.warn(context.name()+" cannot gracefully stop on undeployment|shutdow",e); + } + + + } + }); + } + + private void shareContextWith(ServletContext application) { + application.setAttribute(context_attribute, context); + } + +} diff --git a/src/main/java/org/gcube/smartgears/managers/ContainerManager.java b/src/main/java/org/gcube/smartgears/managers/ContainerManager.java new file mode 100644 index 0000000..675a74a --- /dev/null +++ b/src/main/java/org/gcube/smartgears/managers/ContainerManager.java @@ -0,0 +1,327 @@ +package org.gcube.smartgears.managers; + +import static org.gcube.smartgears.Constants.container_profile_file_path; +import static org.gcube.smartgears.lifecycle.container.ContainerState.active; +import static org.gcube.smartgears.lifecycle.container.ContainerState.down; +import static org.gcube.smartgears.lifecycle.container.ContainerState.failed; +import static org.gcube.smartgears.lifecycle.container.ContainerState.stopped; +import static org.gcube.smartgears.provider.ProviderFactory.provider; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.gcube.common.authorization.client.exceptions.ObjectNotFound; +import org.gcube.common.authorization.client.proxy.AuthorizationProxy; +import org.gcube.common.authorization.library.AuthorizationEntry; +import org.gcube.common.authorization.library.provider.ClientInfo; +import org.gcube.common.authorization.library.provider.ContainerInfo; +import org.gcube.common.events.Observes; +import org.gcube.common.events.Observes.Kind; +import org.gcube.smartgears.configuration.container.ContainerHandlers; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.handlers.ProfileEvents; +import org.gcube.smartgears.handlers.container.ContainerHandler; +import org.gcube.smartgears.handlers.container.ContainerLifecycleEvent; +import org.gcube.smartgears.handlers.container.ContainerPipeline; +import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; +import org.gcube.smartgears.lifecycle.container.ContainerState; +import org.gcube.smartgears.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Coordinates management of the container as a gCube resource. + * + * @author Fabio Simeoni + * + */ +public class ContainerManager { + + private static Logger log = LoggerFactory.getLogger(ContainerManager.class); + + public static ContainerManager instance = new ContainerManager(); + + private AuthorizationProxy authProvider = provider().authorizationProxy(); + + private ContainerContext context; + + private ContainerPipeline pipeline; + + private ContainerManager() {} + + /** + * Starts container management. + */ + public ContainerContext start(ContainerContext context) { + + this.context = context; + + try { + + // TODO Ask if is not enough that is already done in + // Bootstrap.initialiseContainer() function; + context.configuration().validate(); + + validateContainer(context); + + saveContainerState(); + + ContainerHandlers handlers = provider().containerHandlers(); + + log.trace("managing container lifecycle with {}", handlers.get()); + + startHandlers(handlers.get()); + + context.lifecycle().moveTo(active); + + log.trace("loading keys for starting token ..."); + //loadKeyForToken(context.configuration().startTokens()); + log.trace("keys loaded for starting token ..."); + + return context; + } + catch(RuntimeException e) { + + log.error("cannot manage container (see cause)",e); + + if (context!=null) + context.lifecycle().moveTo(failed); + + throw e; + } + + } + + + private void saveContainerState() { + File file = context.configuration().persistence().file(container_profile_file_path); + try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))){ + oos.writeObject(context.id()); + oos.writeObject(context.configuration().startTokens()); + }catch (Exception e) { + log.error("error serializing cointainer state"); + throw new RuntimeException(e); + } + + } + + private void validateContainer(ContainerContext context) { + List tokensToRemove = new ArrayList(); + Set foundContexts= new HashSet(); + + for (String token : context.configuration().startTokens()){ + String tokenContext = resolveTokenForAdd(foundContexts, token); + if (tokenContext!=null){ + log.info("the container will be started in context {}",tokenContext); + foundContexts.add(tokenContext); + } else + tokensToRemove.add(token); + } + + if (foundContexts.isEmpty()){ + log.error("no valid starting token are specified, moving the container to failed"); + throw new RuntimeException("no valid starting token are specified"); + } + + context.configuration().startTokens().removeAll(tokensToRemove); + context.configuration().allowedContexts(foundContexts); + } + + private String resolveTokenForAdd(Set alreadyAddedContext, String token){ + try { + AuthorizationEntry entry = authProvider.get(token); + ClientInfo info = entry.getClientInfo(); + log.info("resolved authorization entry for container {}",entry); + if (alreadyAddedContext.contains(entry.getContext())){ + log.warn("the token {} cannot be used, another token with the same context {} found ", entry.getContext()); + } else if(!entry.getContext().startsWith("/"+context.configuration().infrastructure())){ + log.warn("the token {} cannot be used, is not in the infrastructure {} of the container ", token,context.configuration().infrastructure()); + }else if (!(info instanceof ContainerInfo)){ + log.warn("the token {} cannot be used, is not for a container token ", token); + } else if (!((ContainerInfo)info).getHost().equals(context.configuration().hostname()) + || context.configuration().port()!=((ContainerInfo)info).getPort()){ + log.warn("the token {} cannot be used, the client id {} resolved with the token is not the same of the one specified in this container ", token, info.getId()); + } else + return entry.getContext(); + }catch(ObjectNotFound onf){ + log.error("token {} not valid", token); + } catch (Exception e) { + log.error("error contacting authorization for token {}",token,e); + } + return null; + } + + public void manage(ApplicationContext app) { + + app.events().subscribe(this); + + } + + @Observes(value={ApplicationLifecycle.failure,ApplicationLifecycle.stop},kind=Kind.critical) + void monitorApplication(ApplicationLifecycle lifecycle) { + context.lifecycle().tryMoveTo(ContainerState.partActive); + } + + @Observes(value=ContextEvents.ADD_TOKEN_TO_CONTAINER,kind=Kind.critical) + void addToken(String token) { + log.trace("adding token {} to container", token); + String newContext; + if ((newContext = resolveTokenForAdd(context.configuration().allowedContexts(), token))!=null) { + context.configuration().startTokens().add(token); + context.configuration().allowedContexts().add(newContext); + saveContainerState(); + //loadKeyForToken(Arrays.asList(token)); + context.events().fire(token, ContextEvents.ADD_TOKEN_TO_APPLICATION); + context.events().fire(token, ProfileEvents.addToContext); + log.trace("token added and event fired"); + } else log.warn("trying to add an invalid token"); + } + + @Observes(value=ContextEvents.REMOVE_TOKEN_FROM_CONTAINER,kind=Kind.critical) + void removeToken(String token) { + log.trace("removing token {} from container", token); + AuthorizationEntry entry; + try { + entry = authProvider.get(token); + } catch (Exception e) { + log.error("error resolving token to remove"); + return; + } + + if (context.configuration().startTokens().contains(token)) { + context.configuration().startTokens().remove(token); + context.configuration().allowedContexts().remove(entry.getContext()); + saveContainerState(); + context.events().fire(token, ContextEvents.REMOVE_TOKEN_FROM_APPLICATION); + context.events().fire(token, ProfileEvents.removeFromContext); + log.trace("token removed and event fired"); + } else log.warn("cannot remove token, it is not present in the container"); + } + + /** + * Stops container management on remote request. + * + */ + public void stop() { + + stop(false); + } + + + /** + * Stops container management on remote request or container shutdown. + * + */ + public void stop(boolean shutdown) { + + //two cases: stop-on-shutdown and stop-on-request, some listeners will be selective about this, + + //shutdown is triggered by probe app, which is notified among other apps + //if other app have been already notified, the container may already be part-active. + //apps still to notify will listen only on stop, hence won't react to this but will go down when their turn arrives. + + if (context == null) + return; + + log.info("stopping container management"); + + try { + + context.lifecycle().tryMoveTo(shutdown?down:stopped); + + stopHandlers(); + + //no further reactions + log.info("stopping container events"); + context.events().stop(); + Utils.scheduledServicePool.shutdownNow(); + + } + catch (RuntimeException e) { + + log.warn("cannot stop container management (see cause)", e); + } + + } + + + + //helpers + + private void startHandlers(List handlers) { + + try { + + pipeline = new ContainerPipeline(handlers); + + pipeline.forward(new ContainerLifecycleEvent.Start(context)); + + } catch (RuntimeException e) { + + context.lifecycle().tryMoveTo(failed); + throw e; + } + } + + + private void stopHandlers() { + + if (pipeline == null) + return; + + // copy pipeline, flip it, and + ContainerPipeline returnPipeline = pipeline.reverse(); + + // start lifetime pipeline in inverse order with stop event + returnPipeline.forward(new ContainerLifecycleEvent.Stop(context)); + + } +/* + private void loadKeyForToken(List tokens) { + String initialToken = SecurityTokenProvider.instance.get(); + + //TODO: change this + String filePath = "/tmp/keys"; + File PathDirs = new File(filePath+"/"); + PathDirs.mkdirs(); + try{ + for (String token : tokens) { + try{ + SecurityTokenProvider.instance.set(token); + File key = authProvider.getSymmKey(filePath); + log.trace("loading key {} file name ",key.getAbsolutePath()); + log.trace("loaded key {} file name ",key.getAbsolutePath()); + }catch(Exception e){ + log.warn("error loading key for token {}", token, e); + } + } + loadFileIntoClasspath(PathDirs); + }finally{ + SecurityTokenProvider.instance.set(initialToken); + } + } + + + private void loadFileIntoClasspath(File file){ + try { + URL url = file.toURI().toURL(); + + ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader().getParent()==null? + Thread.currentThread().getContextClassLoader() : Thread.currentThread().getContextClassLoader().getParent(); + + URLClassLoader classLoader = (URLClassLoader)currentClassloader; + Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + method.setAccessible(true); + method.invoke(classLoader, url); + } catch (Exception ex) { + log.error("error loading file into classpath",ex); + } + } + */ +} diff --git a/src/main/java/org/gcube/smartgears/managers/ContextEvents.java b/src/main/java/org/gcube/smartgears/managers/ContextEvents.java new file mode 100644 index 0000000..8b771c2 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/managers/ContextEvents.java @@ -0,0 +1,13 @@ +package org.gcube.smartgears.managers; + +public class ContextEvents { + + public static final String ADD_TOKEN_TO_CONTAINER ="AddTokenToContainer"; + + public static final String ADD_TOKEN_TO_APPLICATION ="AddTokenToApplication"; + + public static final String REMOVE_TOKEN_FROM_CONTAINER ="RemoveTokenFromContainer"; + + public static final String REMOVE_TOKEN_FROM_APPLICATION ="RemoveTokenFromApplication"; + +} diff --git a/src/main/java/org/gcube/smartgears/managers/RequestManager.java b/src/main/java/org/gcube/smartgears/managers/RequestManager.java new file mode 100644 index 0000000..b8fc520 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/managers/RequestManager.java @@ -0,0 +1,234 @@ +package org.gcube.smartgears.managers; + +import static org.gcube.smartgears.Constants.*; +import static org.gcube.smartgears.handlers.application.request.RequestError.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.smartgears.configuration.application.Exclude; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.application.DefaultApplicationContext; +import org.gcube.smartgears.handlers.application.ApplicationPipeline; +import org.gcube.smartgears.handlers.application.RequestEvent; +import org.gcube.smartgears.handlers.application.RequestHandler; +import org.gcube.smartgears.handlers.application.ResponseEvent; +import org.gcube.smartgears.handlers.application.request.RequestError; +import org.gcube.smartgears.handlers.application.request.RequestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link Filter} that executes a {@link ApplicationPipeline} of {@link RequestHandler}s before and a client request is delivered + * to a given servlet and before the response produced by the servlet is returned to the client. + * + * @author Fabio Simeoni + * + */ +public class RequestManager implements Filter { + + private static Logger log = LoggerFactory.getLogger(RequestManager.class); + + private final ApplicationContext context; + private final String servlet; + private final List handlers; + + /** + * Creates an instance with the name of the target servlet and a pipeline. + * + * @param servlet the name of the servlet + * @param pipeline the pipeline + */ + public RequestManager(ApplicationContext context, String servletName, List handlers) { + this.context = context; + this.servlet = servletName; + this.handlers = handlers; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException { + + HttpServletRequest httprequest = (HttpServletRequest) request; + HttpServletResponse httpresponse = (HttpServletResponse) response; + + List filterHandlers = getPipelineWithExcluded(httprequest, handlers); + + if (filterHandlers.isEmpty()) + + chain.doFilter(request, response); + + else { + + ApplicationPipeline pipeline = new ApplicationPipeline(filterHandlers); + + // create a per-request context with temporary properties + ApplicationContext ctx = new DefaultApplicationContext(context); + + RequestEvent event = new RequestEvent(servlet, ctx, httprequest,httpresponse); + + try { + pipeline.forward(event); + } + catch(Throwable t) { + handleError(httprequest,httpresponse,t); + return; + } + + try{ + // dispatch to other filters for this servlet + chain.doFilter(request, response); + }catch(Throwable t){ + t.printStackTrace(); + handleError(httprequest,httpresponse,t); + } + + ResponseEvent responseEvent = new ResponseEvent(servlet, ctx, httprequest, httpresponse); + + try { + + pipeline.reverse().forward(responseEvent); + } + catch(Throwable t) { + t.printStackTrace(); + handleError(httprequest,httpresponse,t); + return; + } + } + } + + private List getPipelineWithExcluded( + HttpServletRequest request, List handlersToFilter) { + + String query = request.getQueryString(); + + log.debug("servletPath is {} and pathInfo is {}",request.getServletPath(), request.getPathInfo()); + + if ("wsdl".equals(query) || "wsdl=1".equals(query)) + return Collections.emptyList(); + + String path = request.getServletPath()==null?"":request.getServletPath(); + + path += request.getPathInfo() ==null?"":request.getPathInfo(); + + + log.debug("check wich handler should be excluded {}", path); + + for (Exclude exclude : context.configuration().excludes()){ + String excludePath= exclude.getPath(); + log.trace("exclude is {}",exclude); + if ( + (EXCLUDE_ALL).equals(exclude) || + (excludePath.endsWith(EXCLUDE_ALL) && path!=null && path.startsWith(excludePath.substring(0,excludePath.length()-2))) || + excludePath.equals(path) || (path.endsWith("/") && excludePath.equals(path.substring(0, path.length()-1))) + ){ + //ALL handler are filtered + if (exclude.getHandlers().isEmpty()) return Collections.emptyList(); + + List filteredHandlers = new ArrayList<>(); + for (RequestHandler rh : filteredHandlers) + if (!exclude.getHandlers().contains(rh.getName())) + filteredHandlers.add(rh); + return filteredHandlers; + } + } + + //in case no path is recognized in excludes it return all handlers + return handlersToFilter; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + // propagate filter initialisation to handler + try { + + for (RequestHandler handler : this.handlers) + + handler.start(context); + + } catch (Throwable t) { + + throw new ServletException(t); + + } + } + + @Override + public void destroy() { + + for (RequestHandler handler : this.handlers) + try { + + handler.stop(); + + } catch (Throwable t) { + + log.error("cannot terminate handler {} for application {} ", handler, context.name()); + } + + } + + // helpers +/* + private boolean shouldExcludeRequest(HttpServletRequest request) { + + String query = request.getQueryString(); + + log.debug("servletPath is {} and pathInfo is {}",request.getServletPath(), request.getPathInfo()); + + if ("wsdl".equals(query) || "wsdl=1".equals(query)) + return true; + + String path = request.getServletPath()==null?"":request.getServletPath(); + + path += request.getPathInfo() ==null?"":request.getPathInfo(); + + + log.debug("check if should exclude call with path {}", path); + + for (Exclude exclude : context.configuration().excludes()){ + if (!exclude.getHandlers().isEmpty()) continue; + String excludePath= exclude.getPath(); + log.trace("exclude is {}",exclude); + if ( + (EXCLUDE_ALL).equals(exclude) || + (excludePath.endsWith(EXCLUDE_ALL) && path!=null && path.startsWith(excludePath.substring(0,excludePath.length()-2))) || + excludePath.equals(path) || (path.endsWith("/") && excludePath.equals(path.substring(0, path.length()-1))) + ) + return true; + } + return false; + }*/ + + private void handleError(HttpServletRequest request, HttpServletResponse response,Throwable t) throws IOException { + + RequestError error = t instanceof RequestException? + RequestException.class.cast(t).error(): + application_error; + + if (error == application_error) { + response.sendError(error.code(),error.message()); + }else { + if (error == request_not_authorized_error){ + response.setHeader("WWW-Authenticate", "Basic realm=\"Smartgears\""); + log.info("setting WWW-Authenticate to response header"); + } + response.getWriter().write("Error ("+error.code()+") : "+error.message()+"\nStacktrace:\n"); + t.printStackTrace(response.getWriter()); + response.setStatus(error.code()); + } + + } +} diff --git a/src/main/java/org/gcube/smartgears/persistence/DefaultPersistence.java b/src/main/java/org/gcube/smartgears/persistence/DefaultPersistence.java new file mode 100644 index 0000000..21dbbc3 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/persistence/DefaultPersistence.java @@ -0,0 +1,96 @@ +package org.gcube.smartgears.persistence; + + +import static org.gcube.smartgears.utils.Utils.*; + +import java.io.File; + +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.common.validator.annotations.NotNull; + +@XmlRootElement(name="persistence") +public class DefaultPersistence implements Persistence { + + @XmlAttribute(name="location") @NotNull + private String location; + + public DefaultPersistence() {} + + public DefaultPersistence(String location) { + + notNull("persistence location",location); + + this.location=location; + validate(); + } + + @Override + public String location() { + return location; + } + + @Override + public File writefile(String path) { + + notNull("relative path", path); + + return fileAt(new File(location, path).getAbsolutePath()).toWrite(); + } + + @Override + public File file(String path) { + + notNull("relative path", path); + + return fileAt(new File(location, path).getAbsolutePath()).toRead(); + } + + + //called after JAXB unmarshalling to purge unavailable handlers + void afterUnmarshal(Unmarshaller u, Object parent) { + + validate(); + } + + public void validate() { + + File locationDir = new File(location); + if (!(locationDir.exists() && locationDir.isDirectory() && locationDir.canRead() && locationDir.canWrite())) + throw new IllegalStateException("invalid node configuration: home "+location+" does not exist or is not a directory or cannot be accessed in read/write mode"); + + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((location == null) ? 0 : location.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + DefaultPersistence other = (DefaultPersistence) obj; + if (location == null) { + if (other.location != null) + return false; + } else if (!location.equals(other.location)) + return false; + return true; + } + + @Override + public String toString() { + return "local persistence in "+location; + } + +} diff --git a/src/main/java/org/gcube/smartgears/persistence/Persistence.java b/src/main/java/org/gcube/smartgears/persistence/Persistence.java new file mode 100644 index 0000000..058480e --- /dev/null +++ b/src/main/java/org/gcube/smartgears/persistence/Persistence.java @@ -0,0 +1,13 @@ +package org.gcube.smartgears.persistence; + +import java.io.File; + +public interface Persistence { + + String location(); + + File file(String path); + + File writefile(String path); + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/smartgears/probe/ContainerListener.java b/src/main/java/org/gcube/smartgears/probe/ContainerListener.java new file mode 100644 index 0000000..9f5068e --- /dev/null +++ b/src/main/java/org/gcube/smartgears/probe/ContainerListener.java @@ -0,0 +1,24 @@ +package org.gcube.smartgears.probe; + +import javax.servlet.ServletContextListener; +import javax.servlet.annotation.WebListener; + +import org.gcube.smartgears.managers.ContainerManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@WebListener +public class ContainerListener implements ServletContextListener { + + public static Logger log = LoggerFactory.getLogger(ContainerListener.class); + + public void contextDestroyed(javax.servlet.ServletContextEvent sce) { + log.trace("shutting down container from probe"); + ContainerManager.instance.stop(true); + }; + + public void contextInitialized(javax.servlet.ServletContextEvent sce) { + log.trace("starting up probe..."); + }; +} + diff --git a/src/main/java/org/gcube/smartgears/provider/DefaultProvider.java b/src/main/java/org/gcube/smartgears/provider/DefaultProvider.java new file mode 100644 index 0000000..6f91dcf --- /dev/null +++ b/src/main/java/org/gcube/smartgears/provider/DefaultProvider.java @@ -0,0 +1,387 @@ +package org.gcube.smartgears.provider; + +import static org.gcube.common.authorization.client.Constants.authorizationService; +import static org.gcube.smartgears.Constants.configuration_file_path; +import static org.gcube.smartgears.Constants.container_configuraton_file_path; +import static org.gcube.smartgears.Constants.container_handlers_file_path; +import static org.gcube.smartgears.Constants.container_profile_file_path; +import static org.gcube.smartgears.Constants.default_extensions_file_path; +import static org.gcube.smartgears.Constants.default_handlers_file_path; +import static org.gcube.smartgears.Constants.extensions_file_path; +import static org.gcube.smartgears.Constants.ghn_home_env; +import static org.gcube.smartgears.Constants.ghn_home_property; +import static org.gcube.smartgears.Constants.handlers_file_path; +import static org.gcube.smartgears.Constants.library_configuration_file_path; +import static org.gcube.smartgears.Constants.profile_file_path; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.util.List; +import java.util.UUID; + +import javax.servlet.ServletContext; + +import org.gcube.common.authorization.client.proxy.AuthorizationProxy; +import org.gcube.common.events.Hub; +import org.gcube.common.events.impl.DefaultHub; +import org.gcube.informationsystem.publisher.RegistryPublisherFactory; +import org.gcube.informationsystem.publisher.ScopedPublisher; +import org.gcube.smartgears.configuration.Mode; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.ApplicationConfigurationBinder; +import org.gcube.smartgears.configuration.application.ApplicationExtensions; +import org.gcube.smartgears.configuration.application.ApplicationHandlers; +import org.gcube.smartgears.configuration.application.BridgedApplicationConfiguration; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.configuration.container.ContainerConfigurationBinder; +import org.gcube.smartgears.configuration.container.ContainerHandlers; +import org.gcube.smartgears.configuration.library.SmartGearsConfiguration; +import org.gcube.smartgears.configuration.library.SmartGearsConfigurationBinder; +import org.gcube.smartgears.context.Properties; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.application.DefaultApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.context.container.DefaultContainerContext; +import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; +import org.gcube.smartgears.lifecycle.container.ContainerLifecycle; +import org.gcube.smartgears.persistence.DefaultPersistence; +import org.gcube.smartgears.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default implementation of the {@link Provider} interface. + * + * @author Fabio Simeoni + * + */ +public class DefaultProvider implements Provider { + + private static Logger log = LoggerFactory.getLogger(Provider.class); + + private ContainerContext containerContext; + //TODO: do the same with applicationContext (with a map) + + protected DefaultProvider(){}; + + @SuppressWarnings("unchecked") + @Override + public ContainerContext containerContext() { + + if(containerContext==null){ + ContainerConfiguration configuration = containerConfiguration(); + + if (configuration.persistence()==null) { + String location = Utils.home()+"/state"; + File dir = new File(location); + if (!dir.exists()) + dir.mkdirs(); + configuration.persistence(new DefaultPersistence(location)); + + log.trace("setting persistence location for container @ {}",dir.getAbsolutePath()); + } + + Hub hub = new DefaultHub(); + + ContainerLifecycle lifecycle = new ContainerLifecycle(hub); + + File file = configuration.persistence().file(container_profile_file_path); + + String id = null; + List tokens = null; + if (file.exists()){ + log.info("loading persisted state for container"); + try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))){ + id = (String)ois.readObject(); + tokens = (List) ois.readObject(); + }catch(Exception e){ + log.error("error loading persisted state, creating new uuid",e); + } + + } + if (id==null){ + id = UUID.randomUUID().toString(); + log.info("container id created is {}",id); + + } + + if (tokens!=null) + configuration.startTokens(tokens); + + containerContext = new DefaultContainerContext(id, configuration, hub, lifecycle, new Properties()); + } + return containerContext; + } + + @Override + public ContainerHandlers containerHandlers() { + + try { + + InputStream config = getClass().getResourceAsStream(container_handlers_file_path); + + if (config == null) + throw new IllegalStateException("invalid distribution: cannot find " + container_handlers_file_path); + + ContainerConfigurationBinder binder = new ContainerConfigurationBinder(); + + return binder.bindHandlers(config); + + } catch (RuntimeException e) { + + throw new RuntimeException("cannot install container handlers (see cause) ", e); + + } + } + + @Override + public ApplicationContext contextFor(ContainerContext context, ServletContext application) { + + ApplicationConfiguration configuration = null; + ApplicationConfiguration embedded = configurationFor(application); + ApplicationConfiguration external = context.configuration().app(application.getContextPath()); + + + + //shouldn't happen: management shouldn't have started at all + if (embedded==null && external==null) + throw new AssertionError("application @ "+application.getContextPath()+" is not distributed with " + + configuration_file_path+" and there is no external configuration for it in "+container_configuraton_file_path); + + //no embedded configuration + if (embedded == null) { + + configuration = external ; + + log.info("loaded configuration for application "+configuration.name()+" from "+container_configuraton_file_path); + } + else { + + configuration = embedded; + + if (external == null) + + log.info("loaded configuration for application "+configuration.name()+" from "+configuration_file_path); + + else { + + configuration.merge(external); + + log.info("loaded configuration for application "+configuration.name()+" from "+configuration_file_path+" and "+container_configuraton_file_path); + + } + } + + // TODO we can check scopes here instead of in BridgedApplicationConfiguration constructor + ApplicationConfiguration bridgedConfiguration = new BridgedApplicationConfiguration(context.configuration(), + configuration); + + Hub hub = new DefaultHub(); + + ApplicationLifecycle lifecycle = new ApplicationLifecycle(hub, configuration.name()); + + File file = bridgedConfiguration.persistence().file(profile_file_path); + String id= null; + if (file.exists()){ + log.info("loading persisted state for application {}", application.getContextPath()); + try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))){ + id = (String)ois.readObject(); + }catch(Exception e){ + log.error("error loading persisted state, creating new uuid",e); + } + } + if (id==null) + id = UUID.randomUUID().toString(); + + return new DefaultApplicationContext(id, context, application, bridgedConfiguration, hub, lifecycle, + new Properties()); + } + + @Override + public ApplicationHandlers handlersFor(ApplicationContext context) { + + try { + + InputStream config = context.application().getResourceAsStream(handlers_file_path); + + if (config == null) { + + log.trace("{} uses the default lifecycle as it does not include {}", context.name(), handlers_file_path); + + // it's in a library, using + config = getClass().getResourceAsStream(default_handlers_file_path); + + if (config == null) + throw new IllegalStateException("invalid distribution: cannot find " + default_handlers_file_path); + + } else + log.info("{} uses a custom lifecycle @ {}", context.name(), handlers_file_path); + + ApplicationConfigurationBinder binder = new ApplicationConfigurationBinder(); + + return binder.bindHandlers(config); + + } catch (RuntimeException e) { + + throw new RuntimeException("cannot install handlers for application @ " + context.name()+" (see cause) ", e); + + } + } + + @Override + public ApplicationExtensions extensionsFor(ApplicationContext context) { + + try { + + InputStream config = context.application().getResourceAsStream(extensions_file_path); + + if (config == null) { + + log.trace("{} uses default extensions as it does not include {}", context.name(), extensions_file_path); + + // it's in a library, using + config = getClass().getResourceAsStream(default_extensions_file_path); + + if (config == null) + throw new IllegalStateException("invalid distribution: cannot find " + default_extensions_file_path); + + } else + log.info("{} uses custom extensions @ {}", context.name(), extensions_file_path); + + ApplicationConfigurationBinder binder = new ApplicationConfigurationBinder(); + + return binder.bindExtensions(config); + + } catch (RuntimeException e) { + + throw new RuntimeException("cannot install extensions for application @ " + context.name()+" (see cause) ", e); + + } + } + + + @Override + public SmartGearsConfiguration smartgearsConfiguration() { + + try { + + InputStream config = getClass().getResourceAsStream(library_configuration_file_path); + + if (config == null) + throw new IllegalStateException("invalid distribution: cannot find " + library_configuration_file_path); + + SmartGearsConfigurationBinder binder = new SmartGearsConfigurationBinder(); + + SmartGearsConfiguration configuration = binder.bind(config); + + configuration.validate(); + + return configuration; + + } catch (RuntimeException e) { + + throw new RuntimeException("cannot read library configuration (see cause) ", e); + + } + + } + + // helpers + + private ApplicationConfiguration configurationFor(ServletContext application) { + + try { + + InputStream config = application.getResourceAsStream(configuration_file_path); + + if (config == null) + return null; + + ApplicationConfigurationBinder binder = new ApplicationConfigurationBinder(); + + return binder.bind(config); + + } catch (RuntimeException e) { + + throw new RuntimeException("invalid configuration (see cause)", e); + + } + } + + private ContainerConfiguration containerConfiguration() { + + String home = Utils.home(); + + if (home == null) + throw new IllegalStateException("invalid node configuration: the environment variable " + ghn_home_env + + " or the system property " + ghn_home_property + " must be defined"); + + File homeDir = new File(home); + + if (!(homeDir.exists() && homeDir.isDirectory() && homeDir.canRead() && homeDir.canWrite())) + throw new IllegalStateException("invalid node configuration: home "+home+" does not exist or is not a directory or cannot be accessed in read/write mode"); + + File config = new File(homeDir,container_configuraton_file_path); + + if (!(config.exists() && config.canRead())) + throw new IllegalStateException("invalid node configuration: file "+config.getAbsolutePath()+" does not exist or cannot be accessed"); + + + log.trace("reading container configuration @ {} ", config.getAbsolutePath()); + + ContainerConfigurationBinder binder = new ContainerConfigurationBinder(); + + FileInputStream stream = null; + try { + + stream = new FileInputStream(config); + + } + catch(Exception e) { + throw new RuntimeException("unexpected exception reading container configuration file see cause)",e); + } + + ContainerConfiguration configuration = binder.bind(stream); + + try { + stream.close(); + } + catch(Exception e) { + log.warn("could not close stream when reading container configuration @ "+config.getAbsolutePath()+" (see cause)",e); + } + + return configuration; + } +/* + @Override + public RegistryPublisher publisherFor(ContainerContext context) { + return context.configuration().mode()==Mode.online? + RegistryPublisherFactory.create(): new OfflinePublisher(); + } + + @Override + public RegistryPublisher publisherFor(ApplicationContext context) { + return context.configuration().mode()==Mode.online? + RegistryPublisherFactory.create(): new OfflinePublisher(); + }*/ + + @Override + public ScopedPublisher publisherFor(ContainerContext context) { + return context.configuration().mode()==Mode.online? RegistryPublisherFactory.scopedPublisher() + : new OfflinePublisher(); + } + + @Override + public ScopedPublisher publisherFor(ApplicationContext context) { + return context.configuration().mode()==Mode.online? RegistryPublisherFactory.scopedPublisher() + : new OfflinePublisher(); + } + + @Override + public AuthorizationProxy authorizationProxy() { + return authorizationService(); + } + +} diff --git a/src/main/java/org/gcube/smartgears/provider/OfflinePublisher.java b/src/main/java/org/gcube/smartgears/provider/OfflinePublisher.java new file mode 100644 index 0000000..4f1cc1d --- /dev/null +++ b/src/main/java/org/gcube/smartgears/provider/OfflinePublisher.java @@ -0,0 +1,56 @@ +package org.gcube.smartgears.provider; + +import java.lang.reflect.Method; +import java.util.List; + +import org.gcube.common.resources.gcore.Resource; +import org.gcube.informationsystem.publisher.ScopedPublisher; +import org.gcube.informationsystem.publisher.exception.RegistryNotFoundException; +import org.gcube.smartgears.configuration.Mode; + +/** + * An implementation of {@link ScopedPublisher} that simulates remote publication. + *

+ * Used for applications and or containers that operate in {@link Mode#offline}. + * + * @author Fabio Simeoni + * + */ +public class OfflinePublisher implements ScopedPublisher { + + @Override + public T update(T resource){ + // do nothing + return resource; + } + + @Override + public T create(T resource, List scopes) + throws RegistryNotFoundException { + // fragile! bypass restrictions reflectively and set new scope + for (String scope : scopes) + try { + Method m = resource.getClass().getSuperclass().getDeclaredMethod("addScope", String.class); + m.setAccessible(true); + m.invoke(resource, scope); + } catch (Exception e) { + throw new RuntimeException("could not simulate publication in scope " + scope, e); + } + return resource; + } + + @Override + public T remove(T resource, List scopes) + throws RegistryNotFoundException { + for (String scope : scopes) + try { + Method m = resource.getClass().getSuperclass().getDeclaredMethod("removeScope", String.class); + m.setAccessible(true); + m.invoke(resource, scope); + } catch (Exception e) { + throw new RuntimeException("could not simulate publication remove from scope " + scope, e); + } + return resource; + } + +} diff --git a/src/main/java/org/gcube/smartgears/provider/Provider.java b/src/main/java/org/gcube/smartgears/provider/Provider.java new file mode 100644 index 0000000..13f538d --- /dev/null +++ b/src/main/java/org/gcube/smartgears/provider/Provider.java @@ -0,0 +1,89 @@ +package org.gcube.smartgears.provider; + +import javax.servlet.ServletContext; + +import org.gcube.common.authorization.client.proxy.AuthorizationProxy; +import org.gcube.informationsystem.publisher.ScopedPublisher; +import org.gcube.smartgears.configuration.application.ApplicationExtensions; +import org.gcube.smartgears.configuration.application.ApplicationHandlers; +import org.gcube.smartgears.configuration.container.ContainerHandlers; +import org.gcube.smartgears.configuration.library.SmartGearsConfiguration; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; + +/** + * Provides dependencies for container and application management. + * + * @author Fabio Simeoni + * + */ +public interface Provider { + + //container-level dependencies + + /** + * Returns the runtime properties. + * @return the properties. + */ + SmartGearsConfiguration smartgearsConfiguration(); + + /** + * Assembles and returns the context of the container. + * @return the container's context + */ + ContainerContext containerContext(); + + /** + * Returns the handlers associated with the container. + * @return the handlers + */ + ContainerHandlers containerHandlers(); + + + /** + * Returns an implementation of the IS publisher for the container + * @param application the context of the container + * @return the publisher implementation + */ + ScopedPublisher publisherFor(ContainerContext application); + + //application-level dependencies + + /** + * Assembles and returns the context of a given application. + * @param container the context of the container + * @param application the servlet context of the application + * @return + */ + + ApplicationContext contextFor(ContainerContext container,ServletContext application); + + /** + * Returns the handlers associated with a given application. + * @param application the context of the application + * @return the handlers + */ + ApplicationHandlers handlersFor(ApplicationContext application); + + /** + * Returns the API extensions associated with a given application. + * @param application the context of the application + * @return the extensions + */ + ApplicationExtensions extensionsFor(ApplicationContext application); + + /** + * Returns an implementation of the IS publisher for a given application + * @param application the context of the application + * @return the publisher implementation + */ + ScopedPublisher publisherFor(ApplicationContext application); + + /** + * Returns an implementation of the IS publisher for a given application + * @param application the context of the application + * @return the publisher implementation + */ + AuthorizationProxy authorizationProxy(); + +} diff --git a/src/main/java/org/gcube/smartgears/provider/ProviderFactory.java b/src/main/java/org/gcube/smartgears/provider/ProviderFactory.java new file mode 100644 index 0000000..fef1da3 --- /dev/null +++ b/src/main/java/org/gcube/smartgears/provider/ProviderFactory.java @@ -0,0 +1,14 @@ +package org.gcube.smartgears.provider; + +public class ProviderFactory { + + private static Provider provider = new DefaultProvider(); + + public static Provider provider() { + return provider; + } + + public static void testProvider(Provider provider) { + ProviderFactory.provider=provider; + } +} diff --git a/src/main/java/org/gcube/smartgears/utils/ScopeEvent.java b/src/main/java/org/gcube/smartgears/utils/ScopeEvent.java new file mode 100644 index 0000000..6ccd36d --- /dev/null +++ b/src/main/java/org/gcube/smartgears/utils/ScopeEvent.java @@ -0,0 +1,17 @@ +package org.gcube.smartgears.utils; + +import java.util.Collection; + +public class ScopeEvent { + + private Collection scopes; + + public ScopeEvent(Collection scopes) { + super(); + this.scopes = scopes; + } + + public Collection getScopes() { + return scopes; + } +} diff --git a/src/main/java/org/gcube/smartgears/utils/Utils.java b/src/main/java/org/gcube/smartgears/utils/Utils.java new file mode 100644 index 0000000..bdf06aa --- /dev/null +++ b/src/main/java/org/gcube/smartgears/utils/Utils.java @@ -0,0 +1,221 @@ +package org.gcube.smartgears.utils; + +import static org.gcube.smartgears.Constants.ghn_home_env; +import static org.gcube.smartgears.Constants.ghn_home_property; +import static org.gcube.smartgears.handlers.application.request.RequestError.application_error; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.Flushable; +import java.io.IOException; +import java.util.Collection; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.common.authorization.library.provider.ServiceIdentifier; +import org.gcube.common.authorization.library.provider.ServiceInfo; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.application.request.RequestError; +import org.gcube.smartgears.handlers.application.request.RequestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Library-wide utils. + * + * @author Fabio Simeoni + * + */ +public class Utils { + + private static final Logger log = LoggerFactory.getLogger(Utils.class); + + public static final ScheduledExecutorService scheduledServicePool = Executors.newScheduledThreadPool(10); + + public static RuntimeException unchecked(Throwable t) { + + return (t instanceof RuntimeException) ? RuntimeException.class.cast(t) : new RuntimeException(t.getMessage(), + t); + + } + + public static String smartgearsVersion() { + return "1.0.0"; // @TODO + } + + public static void rethrowUnchecked(Throwable t) throws RuntimeException { + + throw unchecked(t); + + } + + public static void closeSafely(Closeable c) { + + if (c != null) { + try { + + if (c instanceof Flushable) + Flushable.class.cast(c).flush(); + + c.close(); + + } catch (IOException e) { + log.error("could not close {} due to error {}: msg{}", c, e.getClass().getSimpleName(), e.getMessage()); + } + + } + } + + public static void valid(String name, Object[] o) throws IllegalArgumentException { + notNull(name, o); + + } + + public static void notNull(Object[] o) throws IllegalArgumentException { + notNull("argument", o); + } + + public static void notNull(String name, Object o) throws IllegalArgumentException { + if (o == null) + throw new IllegalArgumentException(name + " is null"); + } + + public static void notEmpty(String name, String o) throws IllegalArgumentException { + notNull(name, o); + if (o.isEmpty()) + throw new IllegalArgumentException(name + " is empty"); + } + + public static void notEmpty(String name, Object[] o) throws IllegalArgumentException { + notNull(name, o); + if (o.length == 0) + throw new IllegalArgumentException(name + " is empty"); + } + + public static void notEmpty(String name, Collection o) throws IllegalArgumentException { + notNull(name, o); + if (o.isEmpty()) + throw new IllegalArgumentException(name + " is empty"); + } + + public static void valid(String name, String o) throws IllegalArgumentException { + notNull(name, o); + notEmpty(name, o); + } + + public static String home() { + + String home = System.getenv(ghn_home_env); + + if (home == null) + home = System.getProperty(ghn_home_property); + + return home; + + } + + public static void handleError(HttpServletRequest request, HttpServletResponse response, Throwable t) + throws IOException { + + RequestError error = t instanceof RequestException ? RequestException.class.cast(t).error() : application_error; + + if (error == application_error) { + response.getWriter().write("Error (" + error.code() + ") : " + t.getMessage() + "\nStacktrace:\n"); + t.printStackTrace(response.getWriter()); + response.setStatus(error.code()); + + } else + response.sendError(error.code(), t.getMessage()); + + } + + + public static interface ModeClause { + + File toRead(); + + File toWrite(); + } + + public static ModeClause fileAt(final String path) { + + notNull("file path",path); + + return new ModeClause() { + + @Override + public File toWrite() { + return file(path,true); + } + + @Override + public File toRead() { + return file(path,false); + } + }; + } + + private static File file(String path, boolean writeMode) throws IllegalArgumentException { + + + File file = new File(path); + + if (!writeMode) + + if (!file.exists() || file.length() == 0 || !file.canRead()) { + File backup = new File(file.getAbsolutePath() + ".backup"); + if (backup.exists()) + if (!backup.renameTo(file)) { + log.warn("accessing directly backup {} as it cannot be renamed to {}", backup.getAbsolutePath(), file.getAbsolutePath()); + return backup; //bets effort:read from backup + } + else { + log.info("cannot read {} but can access its backup {}", backup.getAbsolutePath(), file.getAbsolutePath()); + return file; + } + } + + + if (file.isDirectory()) + throw new IllegalArgumentException(path + " cannot be used in write mode because it's folder"); + + //create folder structure it does not exist + if (!file.getParentFile().exists()) + file.getParentFile().mkdirs(); + else + if (file.exists()) + + try ( + BufferedReader reader = new BufferedReader(new FileReader(file)); + BufferedWriter writer = new BufferedWriter(new FileWriter(new File(file.getAbsolutePath() + ".backup"))); + ) + { + String line; + while ((line = reader.readLine()) != null) + writer.write(line); + + } + catch (Exception e) { + log.warn("cannot back up "+file.getAbsolutePath()+" on writing ", e); + } + + + return file; + + } + + public static ServiceInfo getServiceInfo(ApplicationContext application){ + String hostedin = String.format("%s_%d", application.container().configuration().hostname(), application.container().configuration().port()); + return + new ServiceInfo(new ServiceIdentifier(application.configuration().serviceClass(), application.configuration().name(), hostedin)); + } + + +} diff --git a/src/main/resources/META-INF/container-handlers.xml b/src/main/resources/META-INF/container-handlers.xml new file mode 100644 index 0000000..9884b0e --- /dev/null +++ b/src/main/resources/META-INF/container-handlers.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/META-INF/default-extensions.xml b/src/main/resources/META-INF/default-extensions.xml new file mode 100644 index 0000000..26845b3 --- /dev/null +++ b/src/main/resources/META-INF/default-extensions.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/META-INF/default-handlers.xml b/src/main/resources/META-INF/default-handlers.xml new file mode 100644 index 0000000..d6a1181 --- /dev/null +++ b/src/main/resources/META-INF/default-handlers.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/META-INF/frontpage.html b/src/main/resources/META-INF/frontpage.html new file mode 100644 index 0000000..2b587fb --- /dev/null +++ b/src/main/resources/META-INF/frontpage.html @@ -0,0 +1,268 @@ + + + + + ${name} + + + +

+
+

${name}

+

v.${version}

+

managed by gCube

+
+
+

Welcome to ${name},

+

a resource of the ${infra} infrastructure shared in ${vos} VOs.

+

The resource is ${status}.

+ +
+
+ + + profile + + + configuration + + SmartGears ${smartgears-version} +
+
+ + \ No newline at end of file diff --git a/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer new file mode 100644 index 0000000..917ecac --- /dev/null +++ b/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer @@ -0,0 +1 @@ +org.gcube.smartgears.Bootstrap \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.gcube.smartgears.extensions.ApplicationExtension b/src/main/resources/META-INF/services/org.gcube.smartgears.extensions.ApplicationExtension new file mode 100644 index 0000000..3a14165 --- /dev/null +++ b/src/main/resources/META-INF/services/org.gcube.smartgears.extensions.ApplicationExtension @@ -0,0 +1 @@ +org.gcube.smartgears.extensions.resource.RemoteResource \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.gcube.smartgears.handlers.application.ApplicationHandler b/src/main/resources/META-INF/services/org.gcube.smartgears.handlers.application.ApplicationHandler new file mode 100644 index 0000000..5c7dfb1 --- /dev/null +++ b/src/main/resources/META-INF/services/org.gcube.smartgears.handlers.application.ApplicationHandler @@ -0,0 +1,3 @@ +org.gcube.smartgears.handlers.application.lifecycle.ProfileManager +org.gcube.smartgears.handlers.application.request.RequestValidator +org.gcube.smartgears.handlers.application.request.RequestAccounting \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.gcube.smartgears.handlers.container.ContainerHandler b/src/main/resources/META-INF/services/org.gcube.smartgears.handlers.container.ContainerHandler new file mode 100644 index 0000000..5c13143 --- /dev/null +++ b/src/main/resources/META-INF/services/org.gcube.smartgears.handlers.container.ContainerHandler @@ -0,0 +1,2 @@ +org.gcube.smartgears.handlers.container.lifecycle.ProfileManager +org.gcube.smartgears.handlers.container.lifecycle.AccountingManager \ No newline at end of file diff --git a/src/test/java/app/Request.java b/src/test/java/app/Request.java new file mode 100644 index 0000000..b58b1bf --- /dev/null +++ b/src/test/java/app/Request.java @@ -0,0 +1,179 @@ +package app; + +import static com.sun.jersey.api.client.Client.create; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.gcube.smartgears.Constants.scope_header; +import static org.gcube.smartgears.extensions.HttpExtension.Method.DELETE; +import static org.gcube.smartgears.extensions.HttpExtension.Method.GET; +import static utils.TestUtils.context_root; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.CountDownLatch; + +import org.gcube.common.authorization.library.provider.AuthorizationProvider; +import org.gcube.common.authorization.library.provider.UserInfo; +import org.gcube.common.authorization.library.utils.Caller; +import org.gcube.smartgears.extensions.HttpExtension.Method; + +import utils.TestUtils; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.UniformInterfaceException; +import com.sun.jersey.api.client.WebResource.Builder; +import com.sun.jersey.api.client.filter.LoggingFilter; +import com.sun.jersey.core.header.OutBoundHeaders; + + +public class Request { + + private String path=""; + private String scope = TestUtils.scope; + + private OutBoundHeaders headers = new OutBoundHeaders(); + private Method method = GET; + private String body = null; + private boolean logged = false; + + public static Request request() { + return new Request(); + } + + private Request() { + + } + + public Request at(String path) { + this.path=path; + return this; + } + + public Request logging() { + logged=true; + return this; + } + + public Request inScope(String scope) { + this.scope=scope; + return this; + } + + public Request with(String body) { + this.body=body; + return this; + } + + public Request with(String name, String value) { + this.headers.add(name, value); + return this; + } + + public Request using(Method method) { + this.method=method; + return this; + } + + public String path() { + return path; + } + + public String body() { + return body; + } + + public Method method() { + return method; + } + + public String scope() { + return scope; + } + + ClientResponse make(final int port) { + + + // we make a scoped call in a separate thread, with which we then synchronize for completion. + // this helps isolate the caller's thread (Main normally) from the app's thread, + // starting with the scope itself. + final CountDownLatch latch = new CountDownLatch(1); + + class Box { + + volatile UniformInterfaceException failure; + volatile ClientResponse response; + + } + + final Box box = new Box(); + + new Thread() { + + public void run() { + + AuthorizationProvider.instance.set(new Caller(new UserInfo("test", new ArrayList()),"DEFAULT")); + + try { + + Client client = create(); + + if (logged) + client.addFilter(new LoggingFilter(System.err)); + + Builder builder = client.resource(address(path,port)) + .entity(body).header(scope_header, scope); + + for (Entry> header : headers.entrySet()) + for (Object value : header.getValue()) + builder.header(header.getKey(), value); + + if (method==DELETE) + builder.delete(); + else { + + System.err.println("making request @ "+address(path,port)); + + ClientResponse response = builder.method(method.name(),ClientResponse.class); + + //throws an exception if there response has error status + if (response.getStatus()>300) + throw new UniformInterfaceException(response); + + box.response=response; + } + + + } catch (UniformInterfaceException t) { + box.failure=t; + } + + latch.countDown(); + }; + }.start(); + + try { + + if (!latch.await(2000, MILLISECONDS)) + throw new RuntimeException("application has not responded in time"); + + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + if (box.failure !=null) + throw box.failure; + + else + return box.response; + + } + + private String address(String path, long port) { + + path = (path.isEmpty() || path.startsWith("/"))?path:"/"+path; + + return "http://localhost:" + port+ "/" + context_root+path; + } + +} diff --git a/src/test/java/app/SomeApp.java b/src/test/java/app/SomeApp.java new file mode 100644 index 0000000..4e25724 --- /dev/null +++ b/src/test/java/app/SomeApp.java @@ -0,0 +1,450 @@ +package app; + +import static org.gcube.smartgears.Constants.configuration_file_path; +import static org.gcube.smartgears.Constants.extensions_file_path; +import static org.gcube.smartgears.Constants.ghn_home_property; +import static org.gcube.smartgears.Constants.handlers_file_path; +import static utils.TestUtils.context_root; +import static utils.TestUtils.context_root_path; +import static utils.TestUtils.location; +import static utils.TestUtils.servlet_name; + +import java.io.File; + +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.commons.io.FileUtils; +import org.apache.tomcat.util.scan.StandardJarScanner; +import org.gcube.informationsystem.publisher.RegistryPublisher; +import org.gcube.informationsystem.publisher.ScopedPublisher; +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.configuration.Mode; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.ApplicationExtensions; +import org.gcube.smartgears.configuration.application.ApplicationHandlers; +import org.gcube.smartgears.configuration.application.DefaultApplicationConfiguration; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.configuration.container.Site; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.managers.ContainerManager; +import org.gcube.smartgears.provider.ProviderFactory; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.impl.base.path.BasicPath; + +import utils.TestProvider; +import utils.TestUtils; + +import com.sun.jersey.api.client.ClientResponse; + +/** + * Simulates a single-servlet application to be transformed into a gCube resource. + *

+ *

    + *
  • uses a default configuration that can be customised (cf. {@link #configuration()}); + *
  • can be configured with the default handlers (cf. {@link #useDefaultHandlers()}) or custom ones ( + * {@link #handlers()}), including those that are not deployable through standard means, such as mocks ( + * {@link #bypassHandlerDeployment()}; + *
  • can be started to have a default behaviour when called, ({@link #start()}) or else a custom behaviour ( + * {@link #startWith(Runnable)}; + *
+ * can be called in a default scope ({@link #call()} or in a specific scope ({@link #callIn(String)}). Calls are blocking but always + * configured and executed in a separate thread; + * + * @author Fabio Simeoni + * + */ +public class SomeApp { + + private final Tomcat tomcat = new Tomcat(); + + private WebArchive war = defaultWar(); + + private ContainerConfiguration containerConfiguration; + private ApplicationConfiguration configuration; + private ApplicationHandlers handlers = new ApplicationHandlers(); + private ApplicationExtensions extensions = new ApplicationExtensions(); + private TestProvider provider = new TestProvider(); + private boolean deployHandlers = true; + private boolean deployExtensions = true; + private boolean deployConfiguration = true; + private boolean clean=true; + + public SomeApp() { + + if (ContainerManager.instance!=null) + ContainerManager.instance.stop(true); + + tomcat.getConnector().setPort(0); + tomcat.setBaseDir(location); + + System.setProperty(ghn_home_property,location); + + containerConfiguration = defaultContainerConfiguration(); + configuration = defaultConfiguration(); + } + + /** + * Sets a {@link TestProvider} to resolve dependencies at runtime. + * + * @param provider the provider + */ + public void set(TestProvider provider) { + this.provider = provider; + } + + /** + * Runs the test with the state left by previous run. + *

+ * Use only within a single test! + */ + public void dirtyRun() { + this.clean = false; + } + + /** + * Returns the configuration of the application. + *

+ * The initial configuration is based on defaults, but can be changed and extended. + * + * @return the configuration + */ + public ApplicationConfiguration configuration() { + return configuration; + } + + /** + * Returns the configuration of the containerConfiguration. + *

+ * The initial configuration is based on defaults, but can be changed and extended. + * + * @return the configuration + */ + public ContainerConfiguration containerConfiguration() { + return containerConfiguration; + } + + /** + * Returns the handlers that manage the application, none by default. + * + * @return the handlers + */ + public ApplicationHandlers handlers() { + return handlers; + } + + /** + * Avoids deployment of the configured handlers in the application's WAR. + *

+ * The handlers will instead be directly available at runtime. + */ + public void bypassHandlerDeployment() { + + provider.use(handlers()); + deployHandlers = false; + } + + + /** + * Avoids resource configuration deployment. + */ + public void asExternal() { + + configuration.context(context_root_path); + containerConfiguration.app(configuration); + + bypassConfigurationDeployment(); + bypassExtensionsDeployment(); + bypassHandlerDeployment(); + } + + /** + * Avoids resource configuration deployment. + */ + public void withExternal(ApplicationConfiguration config) { + + config.context(context_root_path); + + containerConfiguration.app(config.context()).merge(config); + } + + public void bypassConfigurationDeployment() { + + deployConfiguration = false; + + } + + + public void usePublisher(ScopedPublisher publisher) { + + provider.use(publisher); + } + + /** + * Uses default handlers. + */ + public void useDefaultHandlers() { + + deployHandlers = false; + } + + + /** + * Returns the extensions of the application, none by default. + * + * @return the handlers + */ + public ApplicationExtensions extensions() { + return extensions; + } + + /** + * Uses default extensions. + */ + public void useDefaultExtensions() { + + deployExtensions = false; + } + + /** + * Avoids deployment of the configured extensions in the application's WAR. + *

+ * The extensions will instead be directly available at runtime. + */ + public void bypassExtensionsDeployment() { + + provider.use(extensions()); + deployExtensions = false; + } + + /** + * Starts the application. + * + * @return the context of the application + */ + public ApplicationContext start() { + + return startWith(new Runnable() { + + @Override + public void run() { + System.err.println("test servlet invoked with no particular task"); + } + }); + } + + /** + * Starts the application, injecting test logic in its single servlet + * + * @param test test logic + * @return the context of the application + */ + public ApplicationContext startWith(Runnable test) { + + // install provider + ProviderFactory.testProvider(provider); + + if (clean) + cleanupInstallation(); + + installContainerConfiguration(); + + if (deployConfiguration) + deployConfiguration(); + + if (deployHandlers) + deployHandlers(); + + if (deployExtensions) + deployExtensions(); + + try { + + System.err.println("deploying with " + war.toString(true)); + + StandardContext ctx = (StandardContext) tomcat.addWebapp(context_root_path, warFile().getAbsolutePath()); + + // tells tomcat to look also for exploded directories such as this project's (finds initializer) + ((StandardJarScanner) ctx.getJarScanner()).setScanAllDirectories(true); + + //this starts webapp and always comes back + tomcat.start(); + + ApplicationContext context = provider.context; + + if (context==null) + throw new RuntimeException("app failed @ startup"); + + Wrapper webapp = (Wrapper) tomcat.getHost().findChild(context_root_path).findChild(servlet_name); + + if (webapp!=null) { + + webapp.setServlet(new TestServlet(test)); + + context.container().configuration().port(port()); + + containerConfiguration = context.container().configuration(); + } + + return context; + + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + /** + * Retuens true if the application was successfully start in the container. + * + * @return true if the application was successfully start in the container + */ + public boolean isActive() { + return tomcat.getHost().findChild("/" + context_root).findChild(servlet_name) != null; + } + + /** + * Makes a request to the application. + */ + + public String send(Request call) { + return call.make(port()).getEntity(String.class); + } + + /** + * Makes a request to the application. + */ + + public ClientResponse httpSend(Request call) { + return call.make(port()); + } + + + /** + * Stops the application + */ + public void stop() { + + try { + tomcat.stop(); + tomcat.destroy(); + + } catch (Exception e) { + System.err.println("WARNING: could not clearly stop container:"); + e.printStackTrace(); + } + } + + public File containerConfigurationFile() { + + return new File(location,Constants.container_configuraton_file_path); + } + // helpers + + /** + * Installs the container configuration. + */ + private void installContainerConfiguration() { + + TestUtils.serialise(containerConfiguration(),containerConfigurationFile()); + + } + + /** + * Includes the configuration in the application's WAR. + */ + private void deployConfiguration() { + + String xml = TestUtils.bind(configuration()); + + System.err.println("deploying with configuration:\n" + xml); + + war.addAsWebResource(new StringAsset(xml), new BasicPath(configuration_file_path)); + } + + /** + * Includes the handlers in the application's WAR. + */ + private void deployHandlers() { + + String xml = TestUtils.bind(handlers()); + + System.err.println("deploying with handlers:\n" + xml); + + war.addAsWebResource(new StringAsset(xml), new BasicPath(handlers_file_path)); + } + + /** + * Includes the extensions in the application's WAR. + */ + private void deployExtensions() { + + String xml = TestUtils.bind(extensions()); + + System.err.println("deploying with extensions:\n" + xml); + + war.addAsWebResource(new StringAsset(xml), new BasicPath(extensions_file_path)); + } + + private File warFile() { + + File warFile = new File(location, "test.war"); + + if (warFile.exists() && !warFile.delete()) + System.out.println("could not delete old deployment");; // seems safer than plain overwrite to avoid war corruption + + war.as(ZipExporter.class).exportTo(warFile, true); + + return warFile; + + } + + private WebArchive defaultWar() { + + WebArchive war = ShrinkWrap.create(WebArchive.class); + war.setWebXML(new File("src/test/java/app/web.xml")); + return war; + + } + + private ApplicationConfiguration defaultConfiguration() { + + return new DefaultApplicationConfiguration().mode(Mode.offline).serviceClass("test-class").name("test-app").version("1.0"); + + } + + private ContainerConfiguration defaultContainerConfiguration() { + + return new ContainerConfiguration().mode(Mode.offline).hostname("localhost").port(port()).infrastructure("gcube") + .site(new Site().country("it").location("rome").latitude("41.9000").longitude("12.5000")) + .property("test-prop1","foo") + .property("test-prop2","bar") + .publicationFrequency(5); + + } + + public int port() { + return tomcat.getConnector().getLocalPort(); + } + + private void cleanupInstallation() { + + System.out.println("cleaning installation in location "+location); + + File installation = new File(location); + + if (installation.exists()) + try { + FileUtils.deleteDirectory(installation); + } + catch(Exception e) { + throw new RuntimeException(e); + } + + installation.mkdirs(); + + } +} diff --git a/src/test/java/app/TestServlet.java b/src/test/java/app/TestServlet.java new file mode 100644 index 0000000..b74bf33 --- /dev/null +++ b/src/test/java/app/TestServlet.java @@ -0,0 +1,30 @@ +package app; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + + +@WebServlet(name = "test", urlPatterns = "/test") +public class TestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private Runnable test; + + public TestServlet(Runnable test) { + this.test=test; + } + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + + test.run(); + } +} diff --git a/src/test/java/app/web.xml b/src/test/java/app/web.xml new file mode 100644 index 0000000..5199029 --- /dev/null +++ b/src/test/java/app/web.xml @@ -0,0 +1,15 @@ + + + test-app + + + test + app.TestServlet + + + + test + /* + + + \ No newline at end of file diff --git a/src/test/java/test/LifecycleTest.java b/src/test/java/test/LifecycleTest.java new file mode 100644 index 0000000..60bb815 --- /dev/null +++ b/src/test/java/test/LifecycleTest.java @@ -0,0 +1,56 @@ +package test; + +import static java.util.concurrent.TimeUnit.*; +import static junit.framework.Assert.*; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.*; + +import java.util.concurrent.CountDownLatch; + +import org.gcube.common.events.Hub; +import org.gcube.common.events.Observes; +import org.gcube.common.events.impl.DefaultHub; +import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; +import org.gcube.smartgears.lifecycle.application.ApplicationState; +import org.junit.Test; + +public class LifecycleTest { + + Hub hub = new DefaultHub(); + ApplicationLifecycle lc = new ApplicationLifecycle(hub,"test"); + + @Test + public void movesFromStateToState() throws Exception { + + ApplicationState state = lc.state(); + lc.moveTo(active); + assertEquals(active, lc.state()); + assertEquals(state,lc.previous()); + } + + @Test(expected=IllegalStateException.class) + public void doesAllowIllegalTransitions() throws Exception { + + lc.moveTo(active); + lc.moveTo(started); + } + + @Test + public void producesEventsOnStateChanges() throws Exception { + + final CountDownLatch latch = new CountDownLatch(1); + + Object verifier = new Object() { + + @Observes(ApplicationLifecycle.activation) + void verifyStateChangeIsNotified(ApplicationLifecycle lifecycle) { + latch.countDown(); + } + }; + + hub.subscribe(verifier); + + lc.moveTo(active); + + assertTrue(latch.await(100,MILLISECONDS)); + } +} diff --git a/src/test/java/test/PipelineTest.java b/src/test/java/test/PipelineTest.java new file mode 100644 index 0000000..c4e93f3 --- /dev/null +++ b/src/test/java/test/PipelineTest.java @@ -0,0 +1,75 @@ +package test; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; + +import org.gcube.smartgears.handlers.application.ApplicationEvent; +import org.gcube.smartgears.handlers.application.ApplicationHandler; +import org.gcube.smartgears.handlers.application.ApplicationPipeline; +import org.junit.Test; + +public class PipelineTest { + + + @Test + @SuppressWarnings("all") + public void pipelineforwards() { + + List mocks = new ArrayList(); + + final ApplicationEvent e = mock(ApplicationEvent.class); + + for (int i=0;i<3;i++) + mocks.add(mock(ApplicationHandler.class)); + + final ApplicationPipeline pipeline = new ApplicationPipeline(mocks); + + //test + pipeline.forward(e); + pipeline.forward(e); + + for (ApplicationHandler mock : mocks) + verify(mock,times(2)).onEvent(e); + + + + } + + @Test + @SuppressWarnings("all") //a regression test! + public void resetsOnFailures() { + + List mocks = new ArrayList(); + + final ApplicationEvent e = mock(ApplicationEvent.class); + + for (int i=0;i<3;i++) + mocks.add(mock(ApplicationHandler.class)); + + //first time fails, second does not + doThrow(new RuntimeException()).doNothing().when(mocks.get(1)).onEvent(anyObject()); + + final ApplicationPipeline pipeline = new ApplicationPipeline(mocks); + + //test + try { + pipeline.forward(e); + fail(); + } + catch(Exception ex) { + + } + + pipeline.forward(e); + + //the first mock is invoked both times because cursor is reset even with failures + verify(mocks.get(0),times(2)).onEvent(e); + + + + } +} diff --git a/src/test/java/test/SmartgearsConfigurationTest.java b/src/test/java/test/SmartgearsConfigurationTest.java new file mode 100644 index 0000000..2d67dc8 --- /dev/null +++ b/src/test/java/test/SmartgearsConfigurationTest.java @@ -0,0 +1,37 @@ +package test; + +import static junit.framework.Assert.*; + +import java.io.ByteArrayInputStream; + +import org.gcube.smartgears.configuration.library.SmartGearsConfiguration; +import org.gcube.smartgears.configuration.library.SmartGearsConfigurationBinder; +import org.junit.Test; + +public class SmartgearsConfigurationTest { + + @Test + public void configurationBinds() throws Exception { + + String xml = ""; + + SmartGearsConfigurationBinder binder = new SmartGearsConfigurationBinder(); + + SmartGearsConfiguration bound = binder.bind(new ByteArrayInputStream(xml.getBytes())); + + bound.validate(); + + String version = bound.version(); + assertEquals("1.0.0-SNAPSHOT",version); + + assertEquals(sampleSmartgearsConfiguration(),bound); + + + } + + private SmartGearsConfiguration sampleSmartgearsConfiguration() { + + return new SmartGearsConfiguration().version("1.0.0-SNAPSHOT"); + + } +} diff --git a/src/test/java/test/application/AppLifecycleTest.java b/src/test/java/test/application/AppLifecycleTest.java new file mode 100644 index 0000000..8d339ea --- /dev/null +++ b/src/test/java/test/application/AppLifecycleTest.java @@ -0,0 +1,36 @@ +package test.application; + +import static org.junit.Assert.*; + +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.handlers.application.lifecycle.ProfileManager; +import org.gcube.smartgears.lifecycle.application.ApplicationState; +import org.gcube.smartgears.lifecycle.container.ContainerState; +import org.junit.Test; + +import app.SomeApp; + +public class AppLifecycleTest { + + @Test + public void applicationGoesDownIfContainerDoes() { + + SomeApp app = new SomeApp(); + + app.handlers().set(new ProfileManager()); + + ApplicationContext actx = app.start(); + + assertEquals(ApplicationState.active,actx.lifecycle().state()); + + ContainerContext ctx = actx.container(); + + assertEquals(ContainerState.active,ctx.lifecycle().state()); + + ctx.lifecycle().moveTo(ContainerState.stopped); + + assertEquals(ApplicationState.stopped,actx.lifecycle().state()); + } + +} diff --git a/src/test/java/test/application/CallValidationTest.java b/src/test/java/test/application/CallValidationTest.java new file mode 100644 index 0000000..e44f7e1 --- /dev/null +++ b/src/test/java/test/application/CallValidationTest.java @@ -0,0 +1,213 @@ +package test.application; + +import static app.Request.request; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.fail; +import static org.gcube.smartgears.handlers.application.request.RequestError.application_failed_error; +import static org.gcube.smartgears.handlers.application.request.RequestError.application_unavailable_error; +import static org.gcube.smartgears.handlers.application.request.RequestError.invalid_request_error; +import static org.gcube.smartgears.handlers.application.request.RequestError.request_not_authorized_error; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.failed; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.stopped; +import static utils.TestUtils.scope; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.configuration.application.Exclude; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.application.lifecycle.ProfileManager; +import org.gcube.smartgears.handlers.application.request.RequestValidator; +import org.junit.Test; + +import app.SomeApp; + +import com.sun.jersey.api.client.UniformInterfaceException; + +public class CallValidationTest { + + + @Test + public void rejectsCallsWhenServiceIsInactive() { + + SomeApp app = new SomeApp(); + + app.handlers().set(new ProfileManager()).set(new RequestValidator()); + + + Runnable test = new Runnable() { + + @Override + public void run() { + fail("call should have been rejected"); + } + }; + + ApplicationContext context = app.startWith(test); + + context.lifecycle().moveTo(stopped); + + try { + app.send(request()); + } + catch(UniformInterfaceException e) { + assertEquals(application_unavailable_error.code(), e.getResponse().getStatus()); + } + + + context.lifecycle().moveTo(failed); + + try { + app.send(request()); + } + catch(UniformInterfaceException e) { + assertEquals(application_failed_error.code(), e.getResponse().getStatus()); + + } + + } + + @Test + public void rejectsCallsWithoutScope() throws Throwable { + + SomeApp app = new SomeApp(); + + app.handlers().set(new ProfileManager()).set(new RequestValidator()); + + app.start(); + + try { + app.send(request().inScope(null));; //call in no scope + fail(); + } + catch(UniformInterfaceException e) { + assertEquals(request_not_authorized_error.code(), e.getResponse().getStatus()); + } + + } + + @Test + public void rejectsCallsWithBadScope() throws Throwable { + + SomeApp app = new SomeApp(); + + app.handlers().set(new ProfileManager()).set(new RequestValidator()); + + app.start(); + + try { + app.send(request().inScope("/bad/scope")); //call in no scope + fail(); + } + catch(UniformInterfaceException e) { + assertEquals(invalid_request_error.code(), e.getResponse().getStatus()); + } + + } + + @Test + public void propagatesScope() throws Throwable { + + SomeApp app = new SomeApp(); + + app.handlers().set(new ProfileManager()).set(new RequestValidator()); + + Runnable test = new Runnable() { + + @Override + public void run() { + assertEquals(scope,SecurityTokenProvider.instance.get()); + } + }; + + app.startWith(test); + + app.send(request()); + + } + + @Test + public void respectsAllExcludeWildCard() throws Throwable { + + SomeApp app = new SomeApp(); + + app.configuration().excludes().add(new Exclude(Constants.EXCLUDE_ALL)); + + app.handlers().set(new ProfileManager()).set(new RequestValidator()); + + final CountDownLatch latch = new CountDownLatch(1); + + Runnable test = new Runnable() { + + @Override + public void run() { + latch.countDown(); + } + }; + + app.startWith(test); + + app.send(request().inScope(null)); + + latch.await(500,TimeUnit.MILLISECONDS); + + } + + @Test + public void respectsExcludeWildCard() throws Throwable { + + SomeApp app = new SomeApp(); + + app.configuration().excludes().add(new Exclude("/path"+Constants.EXCLUDE_ALL)); + + app.handlers().set(new ProfileManager()).set(new RequestValidator()); + + final CountDownLatch latch = new CountDownLatch(1); + + Runnable test = new Runnable() { + + @Override + public void run() { + latch.countDown(); + } + }; + + app.startWith(test); + + app.send(request().at("path/test").inScope(null)); + + latch.await(500,TimeUnit.MILLISECONDS); + + } + + + @Test + public void respectsExactExclude() throws Throwable { + + SomeApp app = new SomeApp(); + + app.configuration().excludes().add(new Exclude("/path")); + + app.handlers().set(new ProfileManager()).set(new RequestValidator()); + + final CountDownLatch latch = new CountDownLatch(1); + + Runnable test = new Runnable() { + + @Override + public void run() { + latch.countDown(); + } + }; + + app.startWith(test); + + app.send(request().at("path").inScope(null)); + + latch.await(500,TimeUnit.MILLISECONDS); + + } + +} diff --git a/src/test/java/test/application/ConfigurationTest.java b/src/test/java/test/application/ConfigurationTest.java new file mode 100644 index 0000000..248f756 --- /dev/null +++ b/src/test/java/test/application/ConfigurationTest.java @@ -0,0 +1,115 @@ +package test.application; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; + +import org.gcube.smartgears.configuration.Mode; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.ApplicationConfigurationBinder; +import org.gcube.smartgears.configuration.application.ApplicationExtensions; +import org.gcube.smartgears.configuration.application.DefaultApplicationConfiguration; +import org.gcube.smartgears.configuration.application.Exclude; +import org.gcube.smartgears.extensions.ApplicationExtension; +import org.gcube.smartgears.persistence.DefaultPersistence; +import org.junit.Test; + +public class ConfigurationTest { + + @Test + public void configurationBinds() throws Exception { + + String xml = "" + + "name" + + "class" + + "version" + + "desc" + + "start/scope"+ + "another/start/scope"+ + "/path" + + "" + + ""; + + + ApplicationConfigurationBinder binder = new ApplicationConfigurationBinder(); + + ApplicationConfiguration bound = binder.bind(new ByteArrayInputStream(xml.getBytes())); + + System.out.println(bound); + + assertEquals(sampleConfiguration(),bound); + + } + + + @Test + public void extensionsBind() throws Exception { + + String xml = "" + + "" + + ""; + + + ApplicationConfigurationBinder binder = new ApplicationConfigurationBinder(); + + ApplicationExtensions bound = binder.bindExtensions(new ByteArrayInputStream(xml.getBytes())); + + assertNotNull(bound.extensions()); + assertEquals(1,bound.extensions().size()); + + ApplicationExtension ext = bound.extensions().get(0); + assertEquals("custom",ext.name()); + assertEquals("custom",ext.mapping()); + + + } + + + /*@Test + public void configurationsMerge() throws Exception { + + ApplicationConfiguration original = sampleConfiguration(); + + ApplicationConfiguration one = sampleConfiguration(); + + ApplicationConfiguration two = new DefaultApplicationConfiguration(); + two.mode(Mode.online); + two.persistence(new DefaultPersistence(new File(".").getAbsolutePath())); + two.startScopes("yet/another/one"); + + one.merge(two); + + assertEquals(one.mode(), two.mode()); + assertEquals(one.name(), original.name()); + assertEquals(one.persistence(), two.persistence()); + + Set merged = new HashSet<>(original.startScopes()); + + merged.addAll(two.startScopes()); + + assertEquals(merged,one.startScopes()); + + }*/ + + //helpers + + private ApplicationConfiguration sampleConfiguration() { + + + return new DefaultApplicationConfiguration() + .secure(true) + .mode(Mode.offline) + .context("ctx") + .name("name") + .serviceClass("class") + .excludes(new Exclude(Arrays.asList("request-validation","request-accounting"),"/path")) + .version("version") + .description("desc") + .persistence(new DefaultPersistence("target")); + + } + + +} diff --git a/src/test/java/test/application/ControllerTest.java b/src/test/java/test/application/ControllerTest.java new file mode 100644 index 0000000..cbdbb97 --- /dev/null +++ b/src/test/java/test/application/ControllerTest.java @@ -0,0 +1,277 @@ +package test.application; + +import static app.Request.*; +import static org.gcube.smartgears.Constants.*; +import static org.gcube.smartgears.extensions.ApiResource.*; +import static org.gcube.smartgears.extensions.HttpExtension.Method.*; +import static org.gcube.smartgears.handlers.application.request.RequestError.*; +import static org.junit.Assert.*; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.extensions.ApiResource; +import org.gcube.smartgears.extensions.ApiSignature; +import org.gcube.smartgears.extensions.HttpController; +import org.gcube.smartgears.extensions.HttpExtension; +import org.junit.Test; + +import app.Request; +import app.SomeApp; + +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.UniformInterfaceException; + +public class ControllerTest { + + String name = "name"; + String extension_path = "/ext"; + String extension_mapping = "/ext/*"; + String resource_path = "/resource"; + + @Test + public void dispatchesToResource() { + + // returns a given type + ApiSignature signature = handles(resource_path).with(method(GET).produces("text/plain")); + + // requires same type + Request request = request().at(resource()).with(accept, "text/plain"); + + SomeApp app = startAppWith(signature); + + app.send(request); + } + + @Test + public void toleratesTrainingSlashes() { + + // returns a given type + ApiSignature signature = handles(resource_path).with(method(GET).produces("text/plain")); + + // requires same type + Request request = request().at(resource()).with(accept, "text/plain"); + + SomeApp app = startAppWith(signature); + + app.send(request); + } + + @Test + public void handlesUnknownResources() { + + ApiSignature signature = handles(resource_path); + + // points to not existing resource + Request request = request().at(resource() + "/bad"); + + SomeApp app = startAppWith(signature); + + try { + app.send(request); + fail(); + } catch (UniformInterfaceException e) { + assertEquals(resource_notfound_error.code(), e.getResponse().getStatus()); + } + } + + @Test + public void handlesUnsupportedMethods() { + + ApiSignature signature = handles(resource_path).with(method(GET)).with(method(PUT)); + + Request request = request().at(resource()).using(POST); + + SomeApp app = startAppWith(signature); + + try { + app.send(request); + fail(); + } catch (UniformInterfaceException e) { + assertEquals(method_unsupported_error.code(), e.getResponse().getStatus()); + assertNotNull(e.getResponse().getHeaders().toString(),e.getResponse().getHeaders().get(allow)); + } + } + + @Test + public void enforcesAcceptHeaders() { + + ApiSignature signature = handles(resource_path).with(method(GET).produces("text/plain")); + + Request request = request().at(resource()).with(accept, "text/xml"); + + SomeApp app = startAppWith(signature); + + try { + app.send(request); + fail(); + } catch (UniformInterfaceException e) { + assertEquals(outgoing_contenttype_unsupported_error.code(), e.getResponse().getStatus()); + + } + } + + @Test + public void enforcesAcceptHeadersEvenWhenResourceDeclaresNone() { + + ApiSignature signature = handles(resource_path).with(method(GET)); + + Request request = request().at(resource()).with(accept, "text/xml"); + + SomeApp app = startAppWith(signature); + + try { + app.send(request); + fail(); + } catch (UniformInterfaceException e) { + assertEquals(outgoing_contenttype_unsupported_error.code(), e.getResponse().getStatus()); + } + } + + @Test + public void enforcesMultiValuedAcceptHeader() { + + ApiSignature signature = handles(resource_path).with(method(GET).produces("text/plain")); + + Request request = request().at(resource()).with(accept, "text/xml").with(accept,"text/plain"); + + SomeApp app = startAppWith(signature); + + app.send(request); + + } + + @Test + public void setsContentTypeIfUnsetAndUnambiguous() { + + ApiSignature signature = handles(resource_path).with(method(GET).produces("text/plain")); + + Request request = request().at(resource()).with(accept, "text/plain"); + + SomeApp app = startAppWith(signature); + + ClientResponse response = app.httpSend(request); + + assertTrue(response.getHeaders().get(content_type).get(0).contains("text/plain")); + } + + @Test + public void doesntSetContentTypeIfUnsetButAmbiguous() { + + ApiSignature signature = handles(resource_path).with(method(GET).produces("text/plain","text/xml")); + + Request request = request().at(resource()).with(accept, "text/plain"); + + SomeApp app = startAppWith(signature); + + ClientResponse response = app.httpSend(request); + + System.out.println(response.getHeaders()); + + assertNull(response.getHeaders().get(content_type)); + + } + + + @Test + public void enforcesMultiValuedContentTypeHeader() { + + ApiSignature signature = handles(resource_path).with(method(POST).accepts("application/xml")); + + Request request = request().at(resource()).using(POST).with(content_type, "text/xml").with(content_type,"text/plain"); + + SomeApp app = startAppWith(signature); + + try { + app.send(request); + fail(); + } catch (UniformInterfaceException e) { + assertEquals(incoming_contenttype_unsupported_error.code(), e.getResponse().getStatus()); + } + } + + @Test + public void acceptsContentTypeHeadersWhenResourceDeclaresNone() { + + ApiSignature signature = handles(resource_path).with(method(POST)); + + Request request = request().at(resource()).using(POST).with(content_type, "text/xml"); + + SomeApp app = startAppWith(signature); + + app.httpSend(request); + + } + + + + ///////////////////////////////// helpers + + private String resource() { + return Constants.root_mapping + extension_path + resource_path; + } + + SomeApp startAppWith(ApiSignature signature) { + + SomeApp app = new SomeApp(); + + app.extensions().set(controllerWith(signature)); + + app.bypassExtensionsDeployment(); + app.bypassHandlerDeployment(); + + app.start(); + + return app; + } + + + + @SuppressWarnings("serial") + HttpExtension controllerWith(final ApiSignature signature) { + return new HttpController(name, extension_mapping) { + + { + addResources(new ApiResource(signature) { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, + IOException { + if (!supports(Method.valueOf(req.getMethod()))) + super.doGet(req, resp); + else + resp.getWriter().write(req.getMethod() + " invoked @ " + signature.mapping()); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, + IOException { + if (!supports(Method.valueOf(req.getMethod()))) + super.doPost(req, resp); + else + resp.getWriter().write(req.getMethod() + " invoked @ " + signature.mapping()); + } + + @Override + public void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, + IOException { + if (!supports(Method.valueOf(req.getMethod()))) + super.doPut(req, resp); + else + resp.getWriter().write(req.getMethod() + " invoked @ " + signature.mapping()); + } + + }); + } + + @Override + public String toString() { + return "SUT controller"; + } + }; + } +} diff --git a/src/test/java/test/application/ExtensionsTest.java b/src/test/java/test/application/ExtensionsTest.java new file mode 100644 index 0000000..030fb8a --- /dev/null +++ b/src/test/java/test/application/ExtensionsTest.java @@ -0,0 +1,185 @@ +package test.application; + +import static app.Request.request; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.fail; +import static org.gcube.smartgears.handlers.application.request.RequestError.invalid_request_error; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.failed; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.extensions.ApplicationExtension; +import org.gcube.smartgears.extensions.HttpExtension; +import org.gcube.smartgears.extensions.resource.RemoteResource; +import org.gcube.smartgears.handlers.application.request.RequestError; +import org.junit.Test; + +import app.SomeApp; + +import com.sun.jersey.api.client.UniformInterfaceException; + +public class ExtensionsTest { + + String name = "name"; + String extension_path="/ext"; + + + @Test + public void areInstalledAndInitialised() { + + + final String response = "output"; + + @SuppressWarnings("serial") + ApplicationExtension extension = new HttpExtension(name,extension_path) { + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + assertNotNull(context()); //init has been invoked + res.getWriter().write(response); + } + + }; + + + + SomeApp app = new SomeApp(); + + app.extensions().set(extension); + + //we're only testing correct installation, not configuration now anything else + app.bypassExtensionsDeployment(); + + app.start(); + + String actual = app.send(request().at(Constants.root_mapping+extension_path)); + + assertEquals(response,actual); + } + + @SuppressWarnings("serial") + @XmlRootElement(name="unknown") + static class UnknownExtension extends HttpExtension { + + UnknownExtension() {} + + public UnknownExtension(String name, String mapping) { + super(name, mapping); + } + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + } + } + + @Test + public void failAppIfNotConfigured() { + + SomeApp app = new SomeApp(); + + app.bypassHandlerDeployment(); + + app.extensions().set(new UnknownExtension(name,extension_path)); + + ApplicationContext context= app.start(); + + assertEquals(failed,context.lifecycle().state()); + + } + + @Test + public void failAppIfConfiguredBadly() { + + SomeApp app = new SomeApp(); + + RemoteResource extension = new RemoteResource(); + extension.name(""); + extension.mapping(""); + + app.extensions().set(extension); + + app.bypassHandlerDeployment(); + + ApplicationContext context= app.start(); + + assertEquals(failed,context.lifecycle().state()); + + } + + + @Test + public void throwErrorsConvertedInHttpResponses() { + + + final RequestError error = invalid_request_error; + + @SuppressWarnings("serial") + ApplicationExtension extension = new HttpExtension(name,extension_path) { + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + error.fire(); + } + + }; + + SomeApp app = new SomeApp(); + + app.extensions().set(extension); + + //we're only testing correct installation, not configuration now anything else + app.bypassExtensionsDeployment(); + + app.start(); + + try { + app.send(request().at(Constants.root_mapping+extension_path)); + fail(); + } + catch(UniformInterfaceException e) { + + assertEquals(error.code(),e.getResponse().getStatus()); + } + + } + + @Test + public void areManagedLikeNativeServlets() { + + @SuppressWarnings("serial") + ApplicationExtension extension = new HttpExtension(name,extension_path) { + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + } + }; + + SomeApp app = new SomeApp(); + + app.extensions().set(extension); + + app.bypassExtensionsDeployment(); + + //installs default filters + app.useDefaultHandlers(); + + app.start(); + + //call in no scope + try { + app.send(request().at(Constants.root_mapping+extension_path).inScope(null)); + fail(); + } + catch(UniformInterfaceException e) { + + assertEquals(e.getResponse().getEntity(String.class),invalid_request_error.code(),e.getResponse().getStatus()); + } + } +} diff --git a/src/test/java/test/application/ProfileManagementTest.java b/src/test/java/test/application/ProfileManagementTest.java new file mode 100644 index 0000000..bed3475 --- /dev/null +++ b/src/test/java/test/application/ProfileManagementTest.java @@ -0,0 +1,74 @@ +package test.application; + +import static junit.framework.Assert.*; +import static org.gcube.smartgears.Constants.*; + +import java.io.File; + +import org.gcube.common.resources.gcore.GCoreEndpoint; +import org.gcube.common.resources.gcore.Resources; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.application.lifecycle.ProfileManager; +import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; +import org.junit.BeforeClass; +import org.junit.Test; + +import app.SomeApp; + +public class ProfileManagementTest { + + public static ApplicationContext ctx; + + @BeforeClass + public static void startApp() { + + SomeApp app = new SomeApp(); + + app.handlers().set(new ProfileManager()); + + ctx = app.start(); + + } + + @Test + public void createsStoresAndPublishesAValidProfile() throws Exception { + + GCoreEndpoint profile = ctx.profile(GCoreEndpoint.class); + + assertNotNull(profile); + + //assert profile has been created + File file = ctx.configuration().persistence().file(profile_file_path); + assertTrue(file.exists()); + + assertFalse(profile.scopes().isEmpty()); + + Resources.validate(profile); + + //assert status matches lifecycle's current state + ApplicationLifecycle lc = ctx.lifecycle(); + + assertEquals(profile.profile().deploymentData().status(),lc.state().remoteForm()); + + } + + @Test + public void loadsAndUpdatesProfile() throws Exception { + + SomeApp runtwice = new SomeApp(); + + runtwice.handlers().set(new ProfileManager()); + + runtwice.dirtyRun(); + + ApplicationContext ctx = runtwice.start(); + + + GCoreEndpoint profile = ctx.profile(GCoreEndpoint.class); + + assertNotNull(profile); + + Resources.validate(profile); + } + +} diff --git a/src/test/java/test/application/RemoteResourceTest.java b/src/test/java/test/application/RemoteResourceTest.java new file mode 100644 index 0000000..03188a4 --- /dev/null +++ b/src/test/java/test/application/RemoteResourceTest.java @@ -0,0 +1,129 @@ +package test.application; + +import static app.Request.request; +import static org.gcube.smartgears.Constants.application_xml; +import static org.gcube.smartgears.Constants.content_type; +import static org.gcube.smartgears.extensions.HttpExtension.Method.GET; +import static org.gcube.smartgears.extensions.HttpExtension.Method.POST; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.StringReader; + +import javax.xml.bind.JAXBContext; + +import org.gcube.common.resources.gcore.GCoreEndpoint; +import org.gcube.common.resources.gcore.Resources; +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.extensions.resource.ConfigurationResource; +import org.gcube.smartgears.extensions.resource.FrontPageResource; +import org.gcube.smartgears.extensions.resource.LifecycleResource; +import org.gcube.smartgears.extensions.resource.LifecycleResource.State; +import org.gcube.smartgears.extensions.resource.ProfileResource; +import org.gcube.smartgears.lifecycle.application.ApplicationState; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import app.Request; +import app.SomeApp; + +public class RemoteResourceTest { + + static final String path = "/resource"; + static JAXBContext jaxb; + static ApplicationContext context; + + static SomeApp app; + + @BeforeClass + public static void setup() throws Exception { + + jaxb = JAXBContext.newInstance(State.class); + + app = new SomeApp(); + + app.useDefaultHandlers(); + app.useDefaultExtensions(); + + context = app.start(); + } + + @Test + public void showsFrontpage() throws Exception { + + // unscoped request + Request request = request().at(resource(FrontPageResource.mapping)).inScope(null); + + app.send(request); + + //Thread.sleep(50000); enable to check interactively in browser + } + + @Test + public void showsConfiguration() throws Exception { + + //unscoped request + Request request = request().at(resource(ConfigurationResource.mapping)).inScope(null); + + app.send(request); + } + + @Test + public void showsProfile() throws Exception { + + //unscoped request + Request request = request().at(resource(ProfileResource.mapping)).inScope(null); + + String outcome = app.send(request); + + GCoreEndpoint profile = Resources.unmarshal(GCoreEndpoint.class, new StringReader(outcome)); + + assertEquals(context.profile(GCoreEndpoint.class).id(), profile.id()); + assertEquals(context.profile(GCoreEndpoint.class).profile().deploymentData().status(), profile.profile().deploymentData().status()); + } + + + + + + @Test + public void currentState() throws Exception { + + Request request = request().at(resource(LifecycleResource.mapping)).using(GET).inScope(null); + + String outcome = app.send(request); + + State state = (State) jaxb.createUnmarshaller().unmarshal(new StringReader(outcome)); + + assertEquals(context.lifecycle().state(),ApplicationState.valueOf(state.value)); + + } + + @Ignore + @Test + public void changeState() throws Exception { + + ApplicationState newstate = ApplicationState.stopped; + + assertFalse(context.lifecycle().state()==newstate); + + Request request = request().at(resource(LifecycleResource.mapping)).using(POST).inScope(null) + .with(content_type, application_xml).with("stopped"); + + app.httpSend(request); + + assertTrue(context.lifecycle().state()==newstate); + + request = request().at(resource(LifecycleResource.mapping)).using(POST).inScope(null) + .with(content_type, application_xml).with("active"); + + } + + // helper + private String resource(String resource) { + return Constants.root_mapping + path + resource; + } +} diff --git a/src/test/java/test/application/StartupTest.java b/src/test/java/test/application/StartupTest.java new file mode 100644 index 0000000..8f0457a --- /dev/null +++ b/src/test/java/test/application/StartupTest.java @@ -0,0 +1,234 @@ +package test.application; + +import static app.Request.request; +import static org.gcube.smartgears.Constants.profile_file_path; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.active; +import static org.gcube.smartgears.lifecycle.application.ApplicationState.failed; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import org.gcube.common.resources.gcore.GCoreEndpoint; +import org.gcube.common.resources.gcore.Resource; +import org.gcube.informationsystem.publisher.ScopedPublisher; +import org.gcube.informationsystem.publisher.exception.RegistryNotFoundException; +import org.gcube.smartgears.Constants; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.DefaultApplicationConfiguration; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.handlers.application.ApplicationEvent; +import org.gcube.smartgears.handlers.application.ApplicationLifecycleEvent.Start; +import org.gcube.smartgears.handlers.application.ApplicationLifecycleHandler; +import org.gcube.smartgears.handlers.application.RequestHandler; +import org.gcube.smartgears.persistence.DefaultPersistence; +import org.junit.After; +import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.Mockito; + +import utils.TestUtils.Box; +import app.SomeApp; + +public class StartupTest { + + SomeApp app = new SomeApp(); + + @After + public void teardown() { + app.stop(); + } + + @Test + public void succeedsWithDefaults() throws Exception { + + + app.useDefaultHandlers(); + app.useDefaultExtensions(); + + ApplicationContext ctx = app.start(); + + assertEquals(active,ctx.lifecycle().state()); + + //profile is shared in servlet context + assertEquals(ctx,ctx.application().getAttribute(Constants.context_attribute)); + + } + + @Test + public void stillStoresProfileWhenPublicationFails() throws Exception { + + app.useDefaultHandlers(); + + ScopedPublisher failingPublisher = Mockito.mock(ScopedPublisher.class); + when(failingPublisher.create(any(Resource.class), Matchers.anyListOf(String.class))).thenThrow(new RegistryNotFoundException()); + + app.usePublisher(failingPublisher); + + ApplicationContext ctx = app.start(); + + Thread.sleep(100); //a little bit of time for failure to propagate + + //application has failed + assertEquals(failed,ctx.lifecycle().state()); + + //profile has been created + File file = ctx.configuration().persistence().file(profile_file_path); + assertTrue(file.exists()); + + } + + @Test + @SuppressWarnings("all") + public void invokesLifecycleHandlers() { + + ApplicationLifecycleHandler witness = mock(ApplicationLifecycleHandler.class); + + app.handlers().set(witness); + + //as we're using mocks, let us bypass JAXB configuration mechanisms + app.bypassHandlerDeployment(); + + app.start(); + + verify(witness).onEvent(any(ApplicationEvent.class)); + } + + @Test + @SuppressWarnings("all") + public void registersRequestsHandlers() { + + Box handlerIsInvoked = new Box(); + + RequestHandler witness = mock(RequestHandler.class); + + app.handlers().set(witness); + + app.bypassHandlerDeployment(); + + app.start(); + + app.send(request()); + + //invoked for request and response + verify(witness,times(2)).onEvent(isA(ApplicationEvent.class)); + } + + //@Ignore //inexplicable sometimes fails as configuration is not removed + @Test + public void failsIfConfigurationIsInvalid() { + + app.configuration().name(null).serviceClass(null).description(null).version(null).persistence(null); + + ApplicationContext ctx = app.start(); + + assertEquals(failed,ctx.lifecycle().state()); + } + + @Test + public void failsIfHandlerFails() throws Throwable { + + ApplicationLifecycleHandler failingHandler = mock(ApplicationLifecycleHandler.class); + doThrow(new RuntimeException("simulated handler failure")).when(failingHandler).onEvent(isA(Start.class)); + + app.handlers().set(failingHandler); + + app.bypassHandlerDeployment(); + + ApplicationContext ctx = app.start(); + + assertEquals(failed,ctx.lifecycle().state()); + + } + + @Test + public void canUseExternalConfiguration() { + + app.asExternal(); + + app.start(); + + assertTrue(app.isActive()); + } + + + @Test + public void canUseMergedConfiguration() { + + ApplicationConfiguration config = new DefaultApplicationConfiguration(); + config.persistence(new DefaultPersistence(new File(".").getAbsolutePath())); + + ApplicationContext context = app.start(); + + assertTrue(app.isActive()); + + app.withExternal(config); + + assertEquals(config.persistence(),context.configuration().persistence()); + + } + + + @Test + public void failsIfAllStartScopesAreInvalid() throws Exception { + + ApplicationConfiguration config = new DefaultApplicationConfiguration(); + + //config.startScopes("bad/scope","even/badder"); + + app.useDefaultHandlers(); + + ApplicationContext context = app.start(); + + app.withExternal(config); + + assertEquals(failed,context.lifecycle().state()); + + + + } + + @Test + public void canStartInVreScope() throws Exception { + + ApplicationConfiguration config = new DefaultApplicationConfiguration(); + + //tring vre = "/"+app.containerConfiguration().infrastructure()+"/"+app.containerConfiguration().startVOs().get(0)+"/vre"; + + //config.startScopes(vre,"/bad/scope"); + + app.useDefaultHandlers(); + + ApplicationContext context = app.start(); + + app.withExternal(config); + + assertEquals(active,context.lifecycle().state()); + + Set runningScopes = new HashSet<>(context.profile(GCoreEndpoint.class).scopes().asCollection()); + + //assertEquals(singleton(vre),runningScopes); + + + + + } + + + @Test(expected=RuntimeException.class) + public void failsIfConfigurationIsMissing() { + + app.bypassConfigurationDeployment(); + + app.start(); + } +} diff --git a/src/test/java/test/container/ConfigurationTest.java b/src/test/java/test/container/ConfigurationTest.java new file mode 100644 index 0000000..1721a0e --- /dev/null +++ b/src/test/java/test/container/ConfigurationTest.java @@ -0,0 +1,72 @@ +package test.container; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; +import java.util.List; + +import org.gcube.smartgears.configuration.Mode; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.DefaultApplicationConfiguration; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.configuration.container.ContainerConfigurationBinder; +import org.gcube.smartgears.configuration.container.Site; +import org.gcube.smartgears.persistence.DefaultPersistence; +import org.junit.Test; + +public class ConfigurationTest { + + @Test + public void containerConfigurationBinds() throws Exception { + + String appXml = "" + "name" + "class" + + "version" + "desc" + "" + + ""; + + String xml = "" + + "localhost" + + "8080" + + "8484" + + "gcube" + + // spaces are intentional! + "token1" + "token2" + "" + appXml + "" + + "it" + "rome" + "41.9000" + + "12.5000" + "" + "" + + "" + "30" + + ""; + + ContainerConfigurationBinder binder = new ContainerConfigurationBinder(); + + ContainerConfiguration bound = binder.bind(new ByteArrayInputStream(xml.getBytes())); + + bound.validate(); + + List scopes = bound.startTokens(); + + assertTrue(scopes.contains("token1")); + assertTrue(scopes.contains("token2")); + + assertEquals(sampleContainerConfiguration(), bound); + + } + + private ContainerConfiguration sampleContainerConfiguration() { + + return new ContainerConfiguration().mode(Mode.offline).hostname("localhost").port(8080).securePort(8484).infrastructure("gcube") + .startTokens(Arrays.asList("token1", "token2")) + .site(new Site().country("it").location("rome").latitude("41.9000").longitude("12.5000")) + .property("prop1", "val1").property("prop2", "val2").publicationFrequency(30) + .app(sampleAppConfiguration()) + .persistence(new DefaultPersistence("target")); + + } + + private ApplicationConfiguration sampleAppConfiguration() { + + return new DefaultApplicationConfiguration().mode(Mode.offline).name("name").serviceClass("class") + .version("version").description("desc").persistence(new DefaultPersistence("target")); + + } +} diff --git a/src/test/java/test/container/ContainerLifecycleTest.java b/src/test/java/test/container/ContainerLifecycleTest.java new file mode 100644 index 0000000..91a2cf0 --- /dev/null +++ b/src/test/java/test/container/ContainerLifecycleTest.java @@ -0,0 +1,71 @@ +package test.container; + +import static org.junit.Assert.*; + +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.lifecycle.application.ApplicationState; +import org.gcube.smartgears.lifecycle.container.ContainerState; +import org.gcube.smartgears.managers.ContainerManager; +import org.junit.After; +import org.junit.Ignore; +import org.junit.Test; + +import app.SomeApp; + +public class ContainerLifecycleTest { + + SomeApp app = new SomeApp(); + + @After + public void teardown() { + app.stop(); + } + + @Test + public void containerGoesToPartActiveWhenAppFails() { + + + ApplicationContext actx = app.start(); + + ContainerContext ctx = actx.container(); + + assertEquals(ContainerState.active,ctx.lifecycle().state()); + + actx.lifecycle().moveTo(ApplicationState.failed); + + assertEquals(ContainerState.partActive,ctx.lifecycle().state()); + } + + @Test + public void containerGoesToPartActiveWhenAppStops() { + + ApplicationContext actx = app.start(); + + ContainerContext ctx = actx.container(); + + assertEquals(ContainerState.active,ctx.lifecycle().state()); + + actx.lifecycle().moveTo(ApplicationState.stopped); + + assertEquals(ContainerState.partActive,ctx.lifecycle().state()); + } + + + //used interactively to study shutdown + @Ignore + @Test + public void containerShutsdown() throws Exception { + + ApplicationContext actx = app.start(); + + ContainerContext ctx = actx.container(); + + assertEquals(ContainerState.active,ctx.lifecycle().state()); + + ContainerManager.instance.stop(true); + app.stop(); + + Thread.sleep(10000); + } +} diff --git a/src/test/java/test/container/ProfileManagementTest.java b/src/test/java/test/container/ProfileManagementTest.java new file mode 100644 index 0000000..1b45685 --- /dev/null +++ b/src/test/java/test/container/ProfileManagementTest.java @@ -0,0 +1,98 @@ +package test.container; + +import static junit.framework.Assert.*; +import static org.gcube.smartgears.Constants.*; +import static org.gcube.smartgears.lifecycle.container.ContainerState.*; + +import java.io.File; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.gcube.common.events.Observes; +import org.gcube.common.resources.gcore.HostingNode; +import org.gcube.common.resources.gcore.Resources; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.junit.Test; + +import app.SomeApp; + +public class ProfileManagementTest { + + @Test + public void createsStoresAndPublishesAValidProfile() throws Exception { + + ContainerContext ctx = startAppAndGetContainerContext(); + + HostingNode node = ctx.profile(HostingNode.class); + + assertNotNull(node); + + //assert profile has been created + File profile = ctx.configuration().persistence().file(container_profile_file_path); + assertTrue(profile.exists()); + + assertFalse(node.scopes().isEmpty()); + + Resources.validate(node); + + } + + @Test + public void loadsAndUpdatesProfile() throws Exception { + + startAppAndGetContainerContext(); + + SomeApp runtwice = new SomeApp(); + + runtwice.dirtyRun(); + + ContainerContext ctx = runtwice.start().container(); + + assertNotNull(ctx.profile(HostingNode.class)); + + HostingNode node = ctx.profile(HostingNode.class); + + Resources.validate(node); + } + + + @Test + public void periodicallyUpdatesAndPublishesProfile() throws Exception { + + SomeApp app = new SomeApp(); + + app.containerConfiguration().publicationFrequency(1); + + ContainerContext ctx = app.start().container(); + + final CountDownLatch latch = new CountDownLatch(1); + + assertEquals(active,ctx.lifecycle().state()); + + ctx.events().subscribe(new Object() { + + @Observes + void profileHasChangedAfterPeriodicUpdate(HostingNode ignore) { + latch.countDown(); + } + + }); + + if (!latch.await(4,TimeUnit.SECONDS)) + fail(); + + ctx.lifecycle().moveTo(stopped); //should stop periodic updates + + } + + + ContainerContext startAppAndGetContainerContext() { + + SomeApp app = new SomeApp(); + + ApplicationContext appCtx = app.start(); + + return appCtx.container(); + } +} diff --git a/src/test/java/test/container/StartupTest.java b/src/test/java/test/container/StartupTest.java new file mode 100644 index 0000000..cbb5a0a --- /dev/null +++ b/src/test/java/test/container/StartupTest.java @@ -0,0 +1,65 @@ +package test.container; + +import static org.gcube.smartgears.Constants.*; +import static org.gcube.smartgears.lifecycle.container.ContainerState.*; +import static org.junit.Assert.*; + +import org.gcube.smartgears.context.application.ApplicationContext; +import org.junit.After; +import org.junit.Test; + +import app.SomeApp; + +public class StartupTest { + + SomeApp app = new SomeApp(); + + @After + public void teardown() { + app.stop(); + } + + @Test(expected=RuntimeException.class) + public void failsIfHomeIsNotConfigured() { + + System.clearProperty(ghn_home_property); + + app.start(); + + assertFalse(app.isActive()); + } + + @Test(expected=RuntimeException.class) + public void failsIfInstallationFolderIsInvalid() { + + System.setProperty(ghn_home_property,"foo"); + + app.start(); + + assertFalse(app.isActive()); + + } + + @Test(expected=RuntimeException.class) + public void failsIfConfigurationIsInvalid() { + + app.containerConfiguration().hostname(null); + + app.start(); + + assertFalse(app.isActive()); + + } + + @Test + public void leavesContainerToActive() { + + ApplicationContext ctx = app.start(); + + assertEquals(active,ctx.container().lifecycle().state()); + + assertTrue(app.isActive()); + } + + +} diff --git a/src/test/java/utils/TestProvider.java b/src/test/java/utils/TestProvider.java new file mode 100644 index 0000000..6d1de8a --- /dev/null +++ b/src/test/java/utils/TestProvider.java @@ -0,0 +1,57 @@ +package utils; + +import javax.servlet.ServletContext; + +import org.gcube.informationsystem.publisher.RegistryPublisher; +import org.gcube.informationsystem.publisher.ScopedPublisher; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.ApplicationExtensions; +import org.gcube.smartgears.configuration.application.ApplicationHandlers; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.gcube.smartgears.provider.DefaultProvider; + +public class TestProvider extends DefaultProvider { + + public ApplicationContext context; + public ApplicationConfiguration configuration; + public ApplicationHandlers handlers; + public ApplicationExtensions extensions; + public ScopedPublisher publisher; + + public void use(ScopedPublisher publisher) { + this.publisher=publisher; + } + + public void use(ApplicationConfiguration configuration) { + this.configuration=configuration; + } + + public void use(ApplicationHandlers handlers) { + this.handlers=handlers; + } + + public void use(ApplicationExtensions extensions) { + this.extensions=extensions; + } + + @Override + public ScopedPublisher publisherFor(ApplicationContext context) { + return publisher==null?super.publisherFor(context):publisher; + } + + @Override + public ApplicationContext contextFor(ContainerContext container,ServletContext application) { + return context = super.contextFor(container,application); + } + + @Override + public ApplicationHandlers handlersFor(ApplicationContext context) { + return handlers==null?super.handlersFor(context):handlers; + } + + @Override + public ApplicationExtensions extensionsFor(ApplicationContext context) { + return extensions==null?super.extensionsFor(context):extensions; + } +} diff --git a/src/test/java/utils/TestUtils.java b/src/test/java/utils/TestUtils.java new file mode 100644 index 0000000..61ebc8e --- /dev/null +++ b/src/test/java/utils/TestUtils.java @@ -0,0 +1,185 @@ +package utils; + +import java.io.File; +import java.io.FileWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; + +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.configuration.application.ApplicationExtensions; +import org.gcube.smartgears.configuration.application.ApplicationHandlers; +import org.gcube.smartgears.configuration.application.DefaultApplicationConfiguration; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.extensions.ApplicationExtension; +import org.gcube.smartgears.handlers.application.ApplicationHandler; + + +public class TestUtils { + + + public static String location = "target/ghn-home"; + public static String context_root = "test-app"; + public static String context_root_path = "/" + context_root; + public static String servlet_name = "test"; + public static String scope = "/gcube/devsec"; + + public static class Box { + + T t; + + public void put(T t) { + this.t=t; + } + + public T get() { + return t; + } + + } + + /** + * Serialises a {@link ContainerConfiguration} to XML in a file. + * + * @param config the configuration + * @param the file + * @return the serialisation + * @throws RuntimeException if the configuration cannot be serialised + */ + public static void serialise(ContainerConfiguration config, File file) { + + //serialises configuration + + try { + JAXBContext ctx = JAXBContext.newInstance(ContainerConfiguration.class); + + FileWriter writer = new FileWriter(file); + + ctx.createMarshaller().marshal(config, writer); + + writer.flush(); + writer.close(); + + } catch (Exception e) { + + throw new RuntimeException("invalid service configuration", e); + } + + } + /** + * Serialises a {@link ApplicationConfiguration} to XML. + * + * @param config the configuration + * @return the serialisation + * @throws RuntimeException if the configuration cannot be serialised + */ + public static String bind(ApplicationConfiguration config) { + + try { + + //collect handler classes + List> classes = new ArrayList>(); + + classes.add(DefaultApplicationConfiguration.class); + if (config.persistence()!=null) + classes.add(config.persistence().getClass()); + + //serialises configuration + + JAXBContext ctx = JAXBContext.newInstance(classes.toArray(new Class[0])); + + StringWriter writer = new StringWriter(); + + ctx.createMarshaller().marshal(config, writer); + + return writer.toString(); + + + } catch (JAXBException e) { + + throw new RuntimeException("invalid application configuration", e); + + } + } + + /** + * Serialises application handlers. + * + * @param handlers the handlers + * @return the serialisation + * @throws RuntimeException if the handlers cannot be serialised + */ + public static String bind(ApplicationHandlers handlers) { + + try { + + //collect handler classes + List> classes = new ArrayList>(); + + classes.add(ApplicationHandlers.class); + + for (ApplicationHandler h : handlers.lifecycleHandlers()) + classes.add(h.getClass()); + + for (ApplicationHandler h : handlers.requestHandlers()) + classes.add(h.getClass()); + + + //serialises configuration + + JAXBContext ctx = JAXBContext.newInstance(classes.toArray(new Class[0])); + + StringWriter writer = new StringWriter(); + + ctx.createMarshaller().marshal(handlers, writer); + + return writer.toString(); + + } catch (JAXBException e) { + + throw new RuntimeException("invalid handler configuration", e); + + } + } + + /** + * Serialises application extensions. + * + * @param extensions the extensions + * @return the serialisation + * @throws RuntimeException if the extensions cannot be serialised + */ + public static String bind(ApplicationExtensions extensions) { + + try { + + //collect handler classes + List> classes = new ArrayList>(); + + classes.add(ApplicationExtensions.class); + + for (ApplicationExtension h : extensions.extensions()) + classes.add(h.getClass()); + + + //serialises configuration + + JAXBContext ctx = JAXBContext.newInstance(classes.toArray(new Class[0])); + + StringWriter writer = new StringWriter(); + + ctx.createMarshaller().marshal(extensions, writer); + + return writer.toString(); + + } catch (JAXBException e) { + + throw new RuntimeException("invalid handler configuration", e); + + } + } + +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..4f36cc8 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{0}: %msg%n + + + + + + + + + + + \ No newline at end of file