├── .gitignore ├── README.md ├── app ├── controllers │ └── Application.scala └── views │ ├── index.scala.html │ └── main.scala.html ├── conf ├── application.conf └── routes ├── modules ├── common │ ├── app │ │ └── controllers │ │ │ └── common │ │ │ └── Application.scala │ ├── conf │ │ ├── common-application.conf │ │ └── common.routes │ └── test │ │ └── controllers │ │ └── common │ │ └── ApplicationSpec.scala ├── serviceA │ ├── app │ │ ├── controllers │ │ │ └── serviceA │ │ │ │ └── Application.scala │ │ └── views │ │ │ ├── index.scala.html │ │ │ └── main.scala.html │ ├── conf │ │ ├── serviceA-application.conf │ │ └── serviceA.routes │ ├── public │ │ ├── images │ │ │ └── favicon.png │ │ ├── javascripts │ │ │ └── jquery-1.9.0.min.js │ │ └── stylesheets │ │ │ └── main.css │ └── test │ │ └── controllers │ │ └── serviceA │ │ └── ApplicationSpec.scala └── serviceB │ ├── app │ └── controllers │ │ └── serviceB │ │ └── Application.scala │ ├── conf │ ├── serviceB-application.conf │ └── serviceB.routes │ └── test │ └── controllers │ └── common │ └── ApplicationSpec.scala └── project ├── Build.scala ├── build.properties └── plugins.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | 17 | .DS_Store 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | multiproject 2 | ============ 3 | 4 | A modular Play 2.3.6 application using multi-projects 5 | 6 | See our blog post at http://eng.42go.com/multi-project-deployment-in-play-framework for more details. 7 | -------------------------------------------------------------------------------- /app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | 6 | object Application extends Controller { 7 | 8 | def index = Action { 9 | Ok(views.html.index("Your new application is ready.")) 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play 2.1") { 4 | 5 | @play20.welcome(message) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | application.secret="CE=H;[?kR^uDYDPter9by^nxAJqwP?dsO[n7?i^qETxhBDthh0Ew<`uTc9I7;j[w" 9 | 10 | # The application languages 11 | # ~~~~~ 12 | application.langs="en" 13 | 14 | # Global object class 15 | # ~~~~~ 16 | # Define the Global object class for this application. 17 | # Default to Global in the root package. 18 | # application.global=Global 19 | 20 | # Router 21 | # ~~~~~ 22 | # Define the Router object to use for this application. 23 | # This router will be looked up first when the application is starting up, 24 | # so make sure this is the entry point. 25 | # Furthermore, it's assumed your route file is named properly. 26 | # So for an application router like `my.application.Router`, 27 | # you may need to define a router file `conf/my.application.routes`. 28 | # Default to Routes in the root package (and conf/routes) 29 | # play.http.router=my.application.Routes 30 | 31 | # Database configuration 32 | # ~~~~~ 33 | # You can declare as many datasources as you want. 34 | # By convention, the default datasource is named `default` 35 | # 36 | # db.default.driver=org.h2.Driver 37 | # db.default.url="jdbc:h2:mem:play" 38 | # db.default.user=sa 39 | # db.default.password="" 40 | 41 | # Evolutions 42 | # ~~~~~ 43 | # You can disable evolutions if needed 44 | # evolutionplugin=disabled 45 | 46 | # Logger 47 | # ~~~~~ 48 | # You can also configure logback (http://logback.qos.ch/), by providing a logger.xml file in the conf directory . 49 | 50 | # Root logger: 51 | logger.root=ERROR 52 | 53 | # Logger used by the framework: 54 | logger.play=INFO 55 | 56 | # Logger provided to your application: 57 | logger.application=DEBUG 58 | 59 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | 2 | # Delegate routes to sub-modules 3 | 4 | -> / common.Routes 5 | 6 | -> / serviceA.Routes 7 | 8 | -> / serviceB.Routes 9 | -------------------------------------------------------------------------------- /modules/common/app/controllers/common/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers.common 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | 6 | object Application extends Controller { 7 | 8 | def status = Action { 9 | Ok("Everything is great!") 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /modules/common/conf/common-application.conf: -------------------------------------------------------------------------------- 1 | 2 | include "../../../conf/application.conf" 3 | 4 | # common development configuration 5 | play.http.router=common.Routes 6 | 7 | # Include any common specific configurations here, 8 | # for dbs, plugins, etc. 9 | 10 | 11 | -------------------------------------------------------------------------------- /modules/common/conf/common.routes: -------------------------------------------------------------------------------- 1 | 2 | # These are common routes that you may want all services to respond to. 3 | GET /status controllers.common.Application.status() 4 | -------------------------------------------------------------------------------- /modules/common/test/controllers/common/ApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | package controllers.common 2 | 3 | import org.specs2.mutable._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | import java.io.File 8 | 9 | /** 10 | * Add your spec here. 11 | * You can mock out a whole application including requests, plugins etc. 12 | * For more information, consult the wiki. 13 | */ 14 | class ApplicationSpec extends Specification { 15 | 16 | val modulePath = new File("./modules/common/") 17 | 18 | "CommonApplication" should { 19 | 20 | "send 404 on a bad request" in { 21 | running(FakeApplication(path = modulePath)) { 22 | route(FakeRequest(GET, "/boum")) must beNone // tbd. does not pass after Play 2.4 23 | } 24 | } 25 | 26 | "render the status page" in { 27 | running(FakeApplication(path = modulePath)) { 28 | val home = route(FakeRequest(GET, "/status")).get 29 | 30 | status(home) must equalTo(OK) 31 | contentAsString(home) must contain ("Everything is great") 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /modules/serviceA/app/controllers/serviceA/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers.serviceA 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | 6 | object Application extends Controller { 7 | 8 | def home = Action { 9 | Ok(views.html.index("Hello there!")) 10 | } 11 | 12 | def main = Action { 13 | Ok("Only serviceA will respond to this.") 14 | } 15 | 16 | def greet(name: String) = Action { 17 | Ok(s"Hello $name!") 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /modules/serviceA/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play 2.1") { 4 | 5 | @play20.welcome(message) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /modules/serviceA/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /modules/serviceA/conf/serviceA-application.conf: -------------------------------------------------------------------------------- 1 | 2 | include "../../../conf/application.conf" 3 | 4 | # serviceA development configuration 5 | play.http.router=serviceA.Routes 6 | 7 | # Include any serviceA specific configurations here, 8 | # for dbs, plugins, etc. 9 | 10 | 11 | -------------------------------------------------------------------------------- /modules/serviceA/conf/serviceA.routes: -------------------------------------------------------------------------------- 1 | 2 | GET / controllers.serviceA.Application.home() 3 | 4 | # Note that you do not need to prefix routes or controller packages with the service name. 5 | # Doing so, however, can make code management much easier. 6 | 7 | GET /serviceA controllers.serviceA.Application.main() 8 | GET /serviceA/:name controllers.serviceA.Application.greet(name: String) 9 | 10 | # Map static resources from the /public folder to the /assets URL path 11 | GET /assets/*file controllers.Assets.at(path="/public", file) 12 | 13 | -> / common.Routes 14 | -------------------------------------------------------------------------------- /modules/serviceA/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kifi/multiproject/62d49587a98f0f372c4656535d52effd67449b6b/modules/serviceA/public/images/favicon.png -------------------------------------------------------------------------------- /modules/serviceA/public/javascripts/jquery-1.9.0.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.9.0 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license */(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("