Repository: iainporter/rest-java Branch: master Commit: 30a904561b85 Files: 146 Total size: 368.0 KB Directory structure: gitextract_6sr2pa59/ ├── .gitignore ├── LICENSE.md ├── README.md ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── src/ ├── main/ │ ├── config/ │ │ └── ci/ │ │ └── gradle.properties │ ├── java/ │ │ └── com/ │ │ └── porterhead/ │ │ └── rest/ │ │ ├── api/ │ │ │ ├── ErrorResponse.java │ │ │ ├── PagedQueryRequest.java │ │ │ ├── PagedResponse.java │ │ │ └── ValidationError.java │ │ ├── authorization/ │ │ │ ├── AuthorizationRequestContext.java │ │ │ ├── AuthorizationService.java │ │ │ ├── exception/ │ │ │ │ └── InvalidAuthorizationHeaderException.java │ │ │ └── impl/ │ │ │ ├── RequestSigningAuthorizationService.java │ │ │ ├── SecurityContextImpl.java │ │ │ └── SessionTokenAuthorizationService.java │ │ ├── config/ │ │ │ ├── ApplicationConfig.java │ │ │ ├── ApplicationDevConfig.java │ │ │ ├── ApplicationProductionConfig.java │ │ │ └── ApplicationStagingConfig.java │ │ ├── exception/ │ │ │ ├── ApplicationRuntimeException.java │ │ │ ├── BaseWebApplicationException.java │ │ │ ├── NotFoundException.java │ │ │ └── ValidationException.java │ │ ├── filter/ │ │ │ ├── ResourceFilterFactory.java │ │ │ └── SecurityContextFilter.java │ │ ├── gateway/ │ │ │ └── EmailServicesGateway.java │ │ ├── model/ │ │ │ └── BaseEntity.java │ │ ├── resource/ │ │ │ ├── GenericExceptionMapper.java │ │ │ └── HealthCheckResource.java │ │ ├── service/ │ │ │ └── BaseService.java │ │ ├── user/ │ │ │ ├── EmailServiceTokenModel.java │ │ │ ├── SocialUserRepository.java │ │ │ ├── UserRepository.java │ │ │ ├── UserService.java │ │ │ ├── UserServiceImpl.java │ │ │ ├── VerificationTokenRepository.java │ │ │ ├── VerificationTokenService.java │ │ │ ├── VerificationTokenServiceImpl.java │ │ │ ├── api/ │ │ │ │ ├── AuthenticatedUserToken.java │ │ │ │ ├── CreateUserRequest.java │ │ │ │ ├── EmailVerificationRequest.java │ │ │ │ ├── ExternalUser.java │ │ │ │ ├── LoginRequest.java │ │ │ │ ├── LostPasswordRequest.java │ │ │ │ ├── OAuth2Request.java │ │ │ │ ├── PasswordRequest.java │ │ │ │ ├── SocialProfile.java │ │ │ │ └── UpdateUserRequest.java │ │ │ ├── domain/ │ │ │ │ ├── AuthorizationToken.java │ │ │ │ ├── Role.java │ │ │ │ ├── SocialUser.java │ │ │ │ ├── SocialUserBuilder.java │ │ │ │ ├── User.java │ │ │ │ └── VerificationToken.java │ │ │ ├── exception/ │ │ │ │ ├── AlreadyVerifiedException.java │ │ │ │ ├── AuthenticationException.java │ │ │ │ ├── AuthorizationException.java │ │ │ │ ├── DuplicateUserException.java │ │ │ │ ├── TokenHasExpiredException.java │ │ │ │ ├── TokenNotFoundException.java │ │ │ │ └── UserNotFoundException.java │ │ │ ├── mail/ │ │ │ │ ├── MailSenderService.java │ │ │ │ ├── MockJavaMailSender.java │ │ │ │ └── impl/ │ │ │ │ └── MailSenderServiceImpl.java │ │ │ ├── resource/ │ │ │ │ ├── PasswordResource.java │ │ │ │ ├── UserResource.java │ │ │ │ └── VerificationResource.java │ │ │ └── social/ │ │ │ ├── JpaConnectionRepository.java │ │ │ ├── JpaUsersConnectionRepository.java │ │ │ └── SocialConfig.java │ │ └── util/ │ │ ├── DateUtil.java │ │ ├── HashUtil.java │ │ └── StringUtil.java │ ├── resources/ │ │ ├── META-INF/ │ │ │ ├── persistence.xml │ │ │ ├── spring/ │ │ │ │ ├── component-scan-context.xml │ │ │ │ ├── data-context.xml │ │ │ │ ├── email-services-context.xml │ │ │ │ ├── email-template-context.xml │ │ │ │ ├── root-context.xml │ │ │ │ └── social-configuration-context.xml │ │ │ └── velocity/ │ │ │ ├── LostPasswordEmail.vm │ │ │ ├── RegistrationEmail.vm │ │ │ └── VerifyEmail.vm │ │ ├── logback.xml │ │ ├── properties/ │ │ │ ├── app.properties │ │ │ ├── dev-app.properties │ │ │ ├── production-app.properties │ │ │ └── staging-app.properties │ │ └── schema/ │ │ ├── indexes.sql │ │ ├── message_store.sql │ │ └── truncate_data.sql │ └── webapp/ │ ├── META-INF/ │ │ └── MANIFEST.MF │ ├── WEB-INF/ │ │ ├── spring/ │ │ │ └── appservlet/ │ │ │ └── servlet-context.xml │ │ └── web.xml │ ├── css/ │ │ └── styles.css │ ├── dashboard.html │ ├── forgot_password.html │ ├── index.html │ ├── js/ │ │ ├── bootstrap.js │ │ ├── cookie.js │ │ ├── enc-base64-min.js │ │ ├── grid.locale-en.js │ │ ├── javarest.js │ │ ├── jquery-full-house.js │ │ ├── sha256.js │ │ ├── store.js │ │ ├── user.js │ │ └── verify.js │ ├── request_email.html │ ├── reset_password.html │ ├── signup.html │ └── validate.html └── test/ ├── groovy/ │ ├── BaseIntegrationTst.groovy │ └── UserIntegrationTest.groovy ├── java/ │ └── com/ │ └── porterhead/ │ └── rest/ │ ├── authorization/ │ │ ├── BaseAuthorizationTst.java │ │ ├── RequestSigningAuthorizationServiceTest.java │ │ ├── SecurityContextTest.java │ │ └── SessionTokenAuthorizationServiceTest.java │ ├── filter/ │ │ └── SecurityContextFilterTest.java │ ├── mock/ │ │ └── AppMockConfiguration.java │ ├── resource/ │ │ ├── BaseResourceTst.java │ │ ├── ConsumerSimpleSecurityFilter.java │ │ ├── HealthCheckResourceTest.java │ │ └── SimpleSecurityFilter.java │ └── user/ │ ├── BaseServiceTest.java │ ├── MailSenderServiceTest.java │ ├── UserServiceTest.java │ ├── VerificationServiceTest.java │ ├── api/ │ │ ├── CreateUserRequestTest.java │ │ ├── LoginRequestTest.java │ │ ├── PasswordRequestTest.java │ │ └── ValidationTst.java │ ├── builder/ │ │ └── ExternalUserBuilder.java │ ├── resource/ │ │ ├── PasswordResourceTest.java │ │ ├── UserResourceTest.java │ │ └── VerificationResourceTest.java │ └── social/ │ ├── AbstractSocialTst.java │ ├── JpaConnectionRepositoryTest.java │ └── JpaUsersConnectionRepositoryTest.java └── resources/ ├── integration-test-context.xml └── social-test-context.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store .gradle/* build/* out/* *.iml *.ipr *.iws /build/ /bin/ # eclipse .settings/ .project .classpath # Intellij .idea/ *.iml *.iws ================================================ FILE: LICENSE.md ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ JAVA REST Application ==================== Simple and easily understandable web project that demonstrates the use of: * Jersey + JAX-RS * Spring Integration * Spring Data + Hibernate * Groovy Integration tests * OAuth * Velocity + Java Mail * Facebook Login * Password Reset * Login/Sign Up + Email Verification * JSR 303 Validation NOTE. For a similar project that uses most of the same components but is built around OAuth2 see Securing Rest Services with OAuth2 and Spring Security to build: gradle clean build integrationTest or use the gradle wrapper: ./gradlew clean build integrationTest go to /build/reports/emma for test coverage reports to run: gradle tomcatRun navigate to http://localhost:8080/java-rest/ see blog posts: THANK YOU ================================================ FILE: build.gradle ================================================ apply plugin: 'idea' apply plugin: 'java' apply plugin: 'tomcat' apply plugin: 'war' apply plugin: 'groovy' apply plugin: 'eclipse' apply plugin: 'jetty' ext { springVersion = "4.1.5.RELEASE" h2Version = "1.3.155" springDataVersion = "1.1.0.RELEASE" springSocialVersion = "1.0.2.RELEASE" springSocialFacebookVersion = "1.0.1.RELEASE" springSocialTwitterVersion = "1.0.2.RELEASE" springSecurity = "3.1.2.RELEASE" springIntegrationVersion = "4.1.2.RELEASE" aspectjrtVersion = "1.6.11" jacksonVersion = "1.9.3" commonsIoVersion = "1.4" jerseyVersion = "1.9.1" hibernateVersion = "3.6.7.Final" h2Version = "1.3.154" slf4jVersion = "1.6.1" logbackVersion = "1.0.1" velocityVersion = "1.7" groovyVersion = "2.0.0" guavaVersion = "12.0" hibernateValidatorVersion = "4.3.1.Final" environmentBase = 'src/main/config/' env = "ci" tomcatVersion = '7.0.25' } configurations { emma } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.0' } } repositories { mavenCentral() maven { url "http://maven.springframework.org/milestone" } } // A list of all of our project dependencies. dependencies { compile "org.hibernate:hibernate-core:$hibernateVersion", "org.hibernate:hibernate-entitymanager:$hibernateVersion", "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.0.Final", "org.hibernate:hibernate-validator:4.1.0.Final", "org.springframework.data:spring-data-jpa:$springDataVersion", "com.h2database:h2:$h2Version", "mysql:mysql-connector-java:5.1.16", "commons-dbcp:commons-dbcp:1.3", "commons-codec:commons-codec:1.6", "org.springframework.social:spring-social-core:$springSocialVersion", "org.springframework.social:spring-social-facebook:$springSocialFacebookVersion", "org.springframework.social:spring-social-twitter:$springSocialTwitterVersion", "org.springframework.social:spring-social-test:$springSocialVersion", "org.springframework.security:spring-security-core:$springSecurity", "org.aspectj:aspectjrt:$aspectjrtVersion", "org.codehaus.jackson:jackson-core-asl:$jacksonVersion", "org.codehaus.jackson:jackson-mapper-asl:$jacksonVersion", "org.codehaus.jackson:jackson-xc:$jacksonVersion", "commons-io:commons-io:$commonsIoVersion", "javax.ws.rs:jsr311-api:1.1", "javax.annotation:jsr250-api:1.0", "com.sun.jersey:jersey-core:$jerseyVersion", "com.sun.jersey:jersey-server:$jerseyVersion", "com.sun.jersey:jersey-json:$jerseyVersion", "com.sun.jersey.contribs:jersey-spring:$jerseyVersion", "org.springframework:spring-core:$springVersion", "org.springframework:spring-beans:$springVersion", "org.springframework:spring-context:$springVersion", "org.springframework:spring-aop:$springVersion", "org.springframework:spring-webmvc:$springVersion", "org.springframework:spring-tx:$springVersion", "org.springframework:spring-orm:$springVersion", "org.springframework:spring-jdbc:$springVersion", "org.springframework:spring-aspects:$springVersion", "org.aspectj:aspectjweaver:1.5.4", "cglib:cglib:2.2", "com.google.guava:guava:$guavaVersion", "org.hibernate:hibernate-validator:$hibernateValidatorVersion", "javax.validation:validation-api:1.1.0.Final", "org.slf4j:slf4j-api:$slf4jVersion", "ch.qos.logback:logback-classic:$logbackVersion", "ch.qos.logback:logback-core:$logbackVersion", "org.mockito:mockito-all:1.8.5", "org.springframework.integration:spring-integration-core:$springIntegrationVersion", "org.springframework.integration:spring-integration-jdbc:$springIntegrationVersion", "org.springframework.integration:spring-integration-mail:$springIntegrationVersion", "org.springframework.integration:spring-integration-http:$springIntegrationVersion", "org.apache.velocity:velocity:$velocityVersion", "javax.mail:mail:1.4.5", "joda-time:joda-time:2.1", "commons-lang:commons-lang:2.6", "commons-cli:commons-cli:1.2" // Pull in test-time dependencies. testCompile "junit:junit:4.12", "org.springframework:spring-test:$springVersion", //"org.codehaus.groovy.modules.http-builder:http-builder:0.5.2", //"org.codehaus.groovy:groovy-all:$groovyVersion", "org.hamcrest:hamcrest-all:1.1", "com.sun.jersey:jersey-test-framework:$jerseyVersion", "com.sun.jersey.jersey-test-framework:jersey-test-framework-grizzly2:$jerseyVersion", "com.sun.jersey.jersey-test-framework:jersey-test-framework-core:$jerseyVersion", "com.sun.jersey:jersey-grizzly2:$jerseyVersion", "org.glassfish.grizzly:grizzly-http:2.1.2", "org.glassfish.gmbal:gmbal-api-only:3.0.0-b023", "org.glassfish.external:management-api:3.0.0-b012", "org.glassfish.grizzly:grizzly-http-server:2.1.2", "org.glassfish.grizzly:grizzly-rcm:2.1.2", "org.glassfish.grizzly:grizzly-http-servlet:2.1.2", "org.glassfish:javax.servlet:3.1", "com.sun.jersey:jersey-client:$jerseyVersion" testCompile("org.codehaus.groovy.modules.http-builder:http-builder:0.5.2") { exclude group: 'org.codehaus.groovy' } testCompile "org.codehaus.groovy:groovy-all:2.0.0" providedCompile "javax.servlet:servlet-api:2.5" // Pull in tomcat dependencies tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}", "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}" tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") { exclude group: "org.eclipse.jdt.core.compiler", module: "ecj" } emma "emma:emma:2.1.5320" emma "emma:emma_ant:2.1.5320" } war { baseName = "java-rest" } test { exclude("**/*IntegrationTest*") // jvmArgs "-XX:-UseSplitVerifier", "-Demma.coverage.out.file=$buildDir/tmp/emma/metadata.emma", "-Demma.coverage.out.merge=true" // // doFirst { // println "Instrumenting the classes at " + sourceSets.main.output.classesDir.absolutePath // // define the custom EMMA ant tasks // ant.taskdef(resource: "emma_ant.properties", classpath: configurations.emma.asPath) // // ant.path(id: "run.classpath") { // pathelement(location: sourceSets.main.output.classesDir.absolutePath) // } // def emmaInstDir = new File(sourceSets.main.output.classesDir.parentFile.parentFile, "tmp/emma/instr") // emmaInstDir.mkdirs() // println "Creating $emmaInstDir to instrument from " + sourceSets.main.output.classesDir.absolutePath // // instruct our compiled classes and store them at $buildDir/tmp/emma/instr // ant.emma(enabled: 'true', verbosity: 'trace1') { // instr(filter: "-com.porterhead.com.porterhead.rest.command.*", merge: "true", destdir: emmaInstDir.absolutePath, instrpathref: "run.classpath", // metadatafile: new File(emmaInstDir, '/metadata.emma').absolutePath) { // instrpath { // fileset(dir: sourceSets.main.output.classesDir.absolutePath, includes: "**/*.class") // } // } // } // setClasspath(files("$buildDir/tmp/emma/instr") + configurations.emma + getClasspath()) // } // doLast { // def srcDir = sourceSets.main.java.srcDirs.toArray()[0] // println "Creating test coverage reports for classes " + srcDir // def emmaInstDir = new File(sourceSets.main.output.classesDir.parentFile.parentFile, "tmp/emma") // ant.emma(enabled: "true", verbosity: 'trace1') { // new File("$buildDir/reports/emma").mkdirs() // report(sourcepath: srcDir) { // fileset(dir: emmaInstDir.absolutePath) { // include(name: "**/*.emma") // } // txt(outfile: "$buildDir/reports/emma/coverage.txt") // html(outfile: "$buildDir/reports/emma/coverage.html") // xml(outfile: "$buildDir/reports/emma/coverage.xml") // } // } // println "Test coverage reports available at $buildDir/reports/emma." // println "txt: $buildDir/reports/emma/coverage.txt" // println "Test $buildDir/reports/emma/coverage.html" // println "Test $buildDir/reports/emma/coverage.xml" // } } task wrapper(type: Wrapper) { gradleVersion = '2.0' } [jettyRun, jettyRunWar, jettyStop]*.stopPort = 8081 [jettyRun, jettyRunWar, jettyStop]*.stopKey = 'stopKey' task integrationTest(type: Test) { include '**/*IntegrationTest*.*' doFirst { jettyRun.daemon = true jettyRun.execute() } doLast { jettyStop.execute() } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue Jul 29 21:04:58 BST 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=http\://services.gradle.org/distributions/gradle-2.0-bin.zip ================================================ FILE: gradle.properties ================================================ systemProp.spring.profiles.active=dev please accept pull request ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: src/main/config/ci/gradle.properties ================================================ tomcatContainerId=tomcat6x tomcatPort=8080 tomcatHostname=localhost tomcatUsername=admin tomcatPassword=admin tomcatContext=rest-java ================================================ FILE: src/main/java/com/porterhead/rest/api/ErrorResponse.java ================================================ package com.porterhead.rest.api; import javax.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; import java.util.List; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 19/10/2012 */ @XmlRootElement public class ErrorResponse { private String errorCode; private String consumerMessage; private String applicationMessage; private List validationErrors = new ArrayList(); public String getErrorCode() { return errorCode; } public void setErrorCode(String errorCode) { this.errorCode = errorCode; } public String getConsumerMessage() { return consumerMessage; } public void setConsumerMessage(String consumerMessage) { this.consumerMessage = consumerMessage; } public String getApplicationMessage() { return applicationMessage; } public void setApplicationMessage(String applicationMessage) { this.applicationMessage = applicationMessage; } public List getValidationErrors() { return validationErrors; } public void setValidationErrors(List validationErrors) { this.validationErrors = validationErrors; } } ================================================ FILE: src/main/java/com/porterhead/rest/api/PagedQueryRequest.java ================================================ package com.porterhead.rest.api; /** * @version 1.0 * @author: Iain Porter * @since 28/02/2013 */ public class PagedQueryRequest { public final static int DEFAULT_PAGE_SIZE = 50; public final static int MAX_PAGE_SIZE = 100; public final static String SORT_ORDER_ASCENDING = "asc"; public final static String SORT_ORDER_DESCENDING = "desc"; private int page; private int pageSize; private String sortProperty; private String sortDirection; private String searchToken; public PagedQueryRequest(){} public PagedQueryRequest(int page, int pageSize) { this.page = page; this.pageSize = pageSize; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } public int getPageSize() { return pageSize == 0 ? DEFAULT_PAGE_SIZE : pageSize > MAX_PAGE_SIZE ? MAX_PAGE_SIZE : pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public String getSortProperty() { return sortProperty; } public void setSortProperty(String sortProperty) { this.sortProperty = sortProperty; } public String getSortDirection() { return sortDirection; } public void setSortDirection(String sortDirection) { this.sortDirection = sortDirection; } public String getSearchToken() { return searchToken; } public void setSearchToken(String searchToken) { this.searchToken = searchToken; } } ================================================ FILE: src/main/java/com/porterhead/rest/api/PagedResponse.java ================================================ package com.porterhead.rest.api; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; /** * @version 1.0 * @author: Iain Porter * @since 28/02/2013 */ @XmlRootElement public class PagedResponse { private int total; private int page; private long records; private List rows; public PagedResponse() {} public int getTotal() { return total; } public void setTotal(int total) { this.total = total; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } public long getRecords() { return records; } public void setRecords(long records) { this.records = records; } public List getRows() { return rows; } public void setRows(List rows) { this.rows = rows; } } ================================================ FILE: src/main/java/com/porterhead/rest/api/ValidationError.java ================================================ package com.porterhead.rest.api; import javax.xml.bind.annotation.XmlRootElement; /** * @version 1.0 * @author: Iain Porter * @since 08/05/2013 */ @XmlRootElement public class ValidationError { private String propertyName; private String propertyValue; private String message; public String getPropertyName() { return propertyName; } public void setPropertyName(String propertyName) { this.propertyName = propertyName; } public String getPropertyValue() { return propertyValue; } public void setPropertyValue(String propertyValue) { this.propertyValue = propertyValue; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } ================================================ FILE: src/main/java/com/porterhead/rest/authorization/AuthorizationRequestContext.java ================================================ package com.porterhead.rest.authorization; /** * * @version 1.0 * @author: Iain Porter * @since 28/01/2013 */ public class AuthorizationRequestContext { /** * The relative url of the request which starts at the root of the requested resource */ private final String requestUrl; /** * The Http method (POST, GET, DELETE, PUT) */ private final String httpMethod; /** * An Iso8061 formatted date timestamp */ private final String requestDateString; /** * Client generated unique nonce value */ private final String nonceToken; /** * The AuthorizationToken which should be in a format that the appropriate AuthorizationService can understand */ private final String authorizationToken; public AuthorizationRequestContext(String requestUrl, String httpMethod, String requestDateString, String nonceToken, String hashedToken) { this.requestUrl = requestUrl; this.httpMethod = httpMethod; this.requestDateString = requestDateString; this.nonceToken = nonceToken; this.authorizationToken = hashedToken; } public String getRequestUrl() { return requestUrl; } public String getHttpMethod() { return httpMethod; } public String getRequestDateString() { return requestDateString; } public String getNonceToken() { return nonceToken; } public String getAuthorizationToken() { return authorizationToken; } } ================================================ FILE: src/main/java/com/porterhead/rest/authorization/AuthorizationService.java ================================================ package com.porterhead.rest.authorization; import com.porterhead.rest.user.api.ExternalUser; /** * * @author: Iain Porter */ public interface AuthorizationService { /** * Given an AuthorizationRequestContext validate and authorize a User * * @param authorizationRequestContext the context required to authorize a user for a particular request * @return ExternalUser */ public ExternalUser authorize(AuthorizationRequestContext authorizationRequestContext); } ================================================ FILE: src/main/java/com/porterhead/rest/authorization/exception/InvalidAuthorizationHeaderException.java ================================================ package com.porterhead.rest.authorization.exception; import com.porterhead.rest.exception.BaseWebApplicationException; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 20/10/2012 */ public class InvalidAuthorizationHeaderException extends BaseWebApplicationException { public static final String DEVELOPER_MESSAGE = "Authorization failed. This could be due to missing properties in the header or" + " the Authorization header may have been incorrectly hashed"; public InvalidAuthorizationHeaderException() { super(401, "40101", "Authorization failed", DEVELOPER_MESSAGE); } } ================================================ FILE: src/main/java/com/porterhead/rest/authorization/impl/RequestSigningAuthorizationService.java ================================================ package com.porterhead.rest.authorization.impl; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.porterhead.rest.authorization.AuthorizationRequestContext; import com.porterhead.rest.authorization.AuthorizationService; import com.porterhead.rest.config.ApplicationConfig; import com.porterhead.rest.user.UserRepository; import com.porterhead.rest.user.UserService; import com.porterhead.rest.user.api.ExternalUser; import com.porterhead.rest.user.domain.AuthorizationToken; import com.porterhead.rest.user.domain.User; import com.porterhead.rest.user.exception.AuthorizationException; import com.porterhead.rest.util.DateUtil; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.digest.DigestUtils; import org.joda.time.DateTime; import org.joda.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Assert; import java.util.Date; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Any Resource that requires a role must have a header property of the following format: *

* * Authorization: : * *

* The signed request hash is comprised of the session token + : + the relative url + , + the Http method + , + Date + , + nonce * This string is then Sha-256 encoded and then Base64 encoded *

* An example: * * Example: * 9fbc6f9a-af1b-4767-a492-c8462fd2a4d9:user/2e2ce9e8-798e-42b6-9326-fd2e56aef7aa/cards,POST,2012-06-30T12:00:00+01:00,34e321a7c4 *

* *

* This will be SHA-256 hashed and then Base64 encoded to produce: *

* * HR/3DJp8RCGo50Wu+/3cr7ibdoNXKg1eYMt3HO5QoP4= * *

* Authorization: 2e2ce9e8-798e-42b6-9326-fd2e56aef7aa:HR/3DJp8RCGo50Wu+/3cr7ibdoNXKg1eYMt3HO5QoP4= * * @author: Iain Porter */ public class RequestSigningAuthorizationService implements AuthorizationService { Logger LOG = LoggerFactory.getLogger(RequestSigningAuthorizationService.class); /** * If the nonce already exists in the cache the difference between its timestamp and the current time will be * greater than this value */ private static final int NONCE_CHECK_TOLERANCE_IN_MILLIS = 20; /** * Maximum Number of Nonce values in the cache * The capacity will never be reached as long as the number of requests is below this value within the time range specified by * ApplicationConfig.getSessionDateOffsetInMinutes() */ private static final int NONCE_CACHE_SIZE = 10000; /** * A an expiry cache that evicts nonce values after a configurable time period */ private LoadingCache nonceCache; /** * The configuration for the application */ private ApplicationConfig config; /** * User service required for persisting user objects */ private UserService userService; /** * directly access user objetcs */ private final UserRepository userRepository; @Autowired public RequestSigningAuthorizationService(UserRepository userRepository, UserService userService, ApplicationConfig applicationConfig) { this.userService = userService; this.userRepository = userRepository; this.config = applicationConfig; initNonceCache(); } private void initNonceCache() { nonceCache = CacheBuilder.newBuilder() .maximumSize(NONCE_CACHE_SIZE) .expireAfterWrite(config.getSessionDateOffsetInMinutes(), TimeUnit.MINUTES) .build( new CacheLoader() { public Nonce load(String key) throws Exception { return new Nonce(new DateTime(), key); } }); } /** * If the request contains values in the AuthorizationRequestContext attempt to find and validate a user * * @param context * @return The request signature was valid and a user is returned or null if the context did not contain the information necessary * to load a user */ public ExternalUser authorize(AuthorizationRequestContext context) { ExternalUser externalUser = null; if (context.getAuthorizationToken() != null && context.getRequestDateString() != null && context.getNonceToken() != null) { String userId = null; String hashedToken = null; String[] token = context.getAuthorizationToken().split(":"); if (token.length == 2) { userId = token[0]; hashedToken = token[1]; //make sure date and nonce is valid validateRequestDate(context.getRequestDateString()); validateNonce(context.getNonceToken()); User user = userRepository.findByUuid(userId); if (user != null) { externalUser = new ExternalUser(user); if (!isAuthorized(user, context, hashedToken)) { throw new AuthorizationException("Request rejected due to an authorization failure"); } } } } return externalUser; } /** * Authorize a hashed token against a request string * The hashed token will be comprised of: * the User's session token + the relative request Url + the Http Verb + the Date as ISO 8061 String + a nonce token generated by the client * * Example: * 9fbc6f9a-af1b-4767-a492-c8462fd2a4d9:user/2e2ce9e8-798e-42b6-9326-fd2e56aef7aa,GET,2012-06-30T12:00:00+01:00,34e321a7c4 *

* *

* This will be SHA-256 hashed and then Base64 encoded to produce: *

* * HR/3DJp8RCGo50Wu+/3cr7ibdoNXKg1eYMt3HO5QoP4= * * * @param user should have a session token that will validate the request signature * @param authorizationRequest the request containing all the details needed to authorize the request * @param hashedToken the token to match against * @return true if the token is authorized */ private boolean isAuthorized(User user, AuthorizationRequestContext authorizationRequest, String hashedToken) { Assert.notNull(user); Assert.notNull(authorizationRequest.getAuthorizationToken()); String unEncodedString = composeUnEncodedRequest(authorizationRequest); AuthorizationToken authorizationToken = user.getAuthorizationToken(); String userTokenHash = encodeAuthToken(authorizationToken.getToken(), unEncodedString); if (hashedToken.equals(userTokenHash)) { return true; } LOG.error("Hash check failed for hashed token: {} for the following request: {} for user: {}", new Object[]{authorizationRequest.getAuthorizationToken(), unEncodedString, user.getId()}); return false; } /** * Encode the token by prefixing it with the User's Session Token * * @param token * @return encoded token */ private String encodeAuthToken(String token, String unencodedRequest) { byte[] digest = DigestUtils.sha256(token + ":" + unencodedRequest); return new String(Base64.encodeBase64(digest)); } /** * The recipe to compose a signed request * * @param authRequest * @return the string value to hash */ private String composeUnEncodedRequest(AuthorizationRequestContext authRequest) { StringBuilder sb = new StringBuilder(); sb.append(authRequest.getRequestUrl()); sb.append(','); sb.append(authRequest.getHttpMethod().toUpperCase()); sb.append(','); sb.append(authRequest.getRequestDateString()); sb.append(',').append(authRequest.getNonceToken()); return sb.toString(); } /** * Ensure that the date of the request falls within the configured range * @param requestDateString */ private void validateRequestDate(String requestDateString) { Date date = DateUtil.getDateFromIso8061DateString(requestDateString); DateTime now = new DateTime(); DateTime offset = new DateTime(date); if (!(offset.isAfter(now.minusMinutes(config.getSessionDateOffsetInMinutes())) && offset.isBefore(now.plusMinutes(config.getSessionDateOffsetInMinutes())))) { LOG.error("Date in header is out of range: {}", requestDateString); throw new AuthorizationException("Date in header is out of range: " + requestDateString); } } /** * The nonce value sent by the client and used in the request signature should be unique across the system * Nonce values will only be considered unique within the time limits of the cache. * The value will be protected if the cache expiry time is within the limits of the request date range. * If the date in the request is stale then the nonce value wil be irrelevant * * Note that the caching strategy will not work in a cluster. A distributed cache will be needed. * * @param nonceValue */ private void validateNonce(String nonceValue) { Nonce nonce = nonceCache.getUnchecked(nonceValue); Duration tolerance = new Duration(nonce.timestamp, new DateTime()); if (tolerance.isLongerThan(Duration.millis(NONCE_CHECK_TOLERANCE_IN_MILLIS))) { LOG.error("Nonce value was not unique: {}", nonceValue); throw new AuthorizationException("Nonce value is not unique"); } } private static class Nonce { private DateTime timestamp; private String nonceValue; Nonce(DateTime time, String nonce) { this.timestamp = time; this.nonceValue = nonce; } } } ================================================ FILE: src/main/java/com/porterhead/rest/authorization/impl/SecurityContextImpl.java ================================================ package com.porterhead.rest.authorization.impl; import com.porterhead.rest.authorization.exception.InvalidAuthorizationHeaderException; import com.porterhead.rest.user.api.ExternalUser; import com.porterhead.rest.user.domain.Role; import javax.ws.rs.core.SecurityContext; import java.security.Principal; /** * Implementation of {@link javax.ws.rs.core.SecurityContext} * * User: porter * Date: 16/03/2012 * Time: 16:13 */ public class SecurityContextImpl implements SecurityContext { private final ExternalUser user; public SecurityContextImpl(ExternalUser user) { this.user = user; } public Principal getUserPrincipal() { return user; } public boolean isUserInRole(String role) { if(role.equalsIgnoreCase(Role.anonymous.name())) { return true; } if(user == null) { throw new InvalidAuthorizationHeaderException(); } return user.getRole().equalsIgnoreCase(role); } public boolean isSecure() { return false; } public String getAuthenticationScheme() { return SecurityContext.BASIC_AUTH; } } ================================================ FILE: src/main/java/com/porterhead/rest/authorization/impl/SessionTokenAuthorizationService.java ================================================ package com.porterhead.rest.authorization.impl; import com.porterhead.rest.authorization.AuthorizationRequestContext; import com.porterhead.rest.authorization.AuthorizationService; import com.porterhead.rest.user.UserRepository; import com.porterhead.rest.user.api.ExternalUser; import com.porterhead.rest.user.domain.AuthorizationToken; import com.porterhead.rest.user.domain.User; import com.porterhead.rest.user.exception.AuthorizationException; import java.util.Date; /** * * Simple authorization service that requires a session token in the Authorization header * This is then matched to a user * * @version 1.0 * @author: Iain Porter * @since 29/01/2013 */ public class SessionTokenAuthorizationService implements AuthorizationService { /** * directly access user objects */ private final UserRepository userRepository; public SessionTokenAuthorizationService(UserRepository repository) { this.userRepository = repository; } public ExternalUser authorize(AuthorizationRequestContext securityContext) { String token = securityContext.getAuthorizationToken(); ExternalUser externalUser = null; if(token == null) { return externalUser; } User user = userRepository.findBySession(token); if(user == null) { throw new AuthorizationException("Session token not valid"); } AuthorizationToken authorizationToken = user.getAuthorizationToken(); if (authorizationToken.getToken().equals(token)) { externalUser = new ExternalUser(user); } return externalUser; } } ================================================ FILE: src/main/java/com/porterhead/rest/config/ApplicationConfig.java ================================================ package com.porterhead.rest.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; /** * User: porter * Date: 17/05/2012 * Time: 19:07 */ @Configuration @PropertySource({"classpath:/properties/app.properties"}) public class ApplicationConfig { private final static String HOSTNAME_PROPERTY = "hostNameUrl"; private final static String SECURITY_AUTHORIZATION_REQUIRE_SIGNED_REQUESTS = "security.authorization.requireSignedRequests"; private final static String AUTHORIZATION_EXPIRY_DURATION = "authorization.timeToLive.inSeconds"; private final static String SESSION_DATE_OFFSET_IN_MINUTES = "session.date.offset.inMinutes"; private final static String TOKEN_EMAIL_REGISTRATION_DURATION = "token.emailRegistration.timeToLive.inMinutes"; private final static String TOKEN_EMAIL_VERIFICATION_DURATION = "token.emailVerification.timeToLive.inMinutes"; private final static String TOKEN_LOST_PASSWORD_DURATION = "token.lostPassword.timeToLive.inMinutes"; private final static String EMAIL_SERVICES_FROM_ADDRESS = "email.services.fromAddress"; private final static String EMAIL_SERVICES_REPLYTO_ADDRESS = "email.services.replyTo"; private final static String EMAIL_SERVICES_VERIFICATION_EMAIL_SUBJECT_TEXT = "email.services.emailVerificationSubjectText"; private final static String EMAIL_SERVICES_REGISTRATION_EMAIL_SUBJECT_TEXT = "email.services.emailRegistrationSubjectText"; private final static String EMAIL_SERVICES_LOST_PASSWORD_SUBJECT_TEXT = "email.services.lostPasswordSubjectText"; @Autowired protected Environment environment; public String getHostNameUrl() { return environment.getProperty(HOSTNAME_PROPERTY); } public String getFacebookClientId() { return environment.getProperty("facebook.clientId"); } public String getFacebookClientSecret() { return environment.getProperty("facebook.clientSecret"); } public int getAuthorizationExpiryTimeInSeconds() { return Integer.parseInt(environment.getProperty(AUTHORIZATION_EXPIRY_DURATION)); } public int getSessionDateOffsetInMinutes() { return Integer.parseInt(environment.getProperty(SESSION_DATE_OFFSET_IN_MINUTES)); } public int getEmailRegistrationTokenExpiryTimeInMinutes() { return Integer.parseInt(environment.getProperty(TOKEN_EMAIL_REGISTRATION_DURATION)); } public int getEmailVerificationTokenExpiryTimeInMinutes() { return Integer.parseInt(environment.getProperty(TOKEN_EMAIL_VERIFICATION_DURATION)); } public int getLostPasswordTokenExpiryTimeInMinutes() { return Integer.parseInt(environment.getProperty(TOKEN_LOST_PASSWORD_DURATION)); } public String getEmailVerificationSubjectText() { return environment.getProperty(EMAIL_SERVICES_VERIFICATION_EMAIL_SUBJECT_TEXT); } public String getEmailRegistrationSubjectText() { return environment.getProperty(EMAIL_SERVICES_REGISTRATION_EMAIL_SUBJECT_TEXT); } public String getLostPasswordSubjectText() { return environment.getProperty(EMAIL_SERVICES_LOST_PASSWORD_SUBJECT_TEXT); } public String getEmailFromAddress() { return environment.getProperty(EMAIL_SERVICES_FROM_ADDRESS); } public String getEmailReplyToAddress() { return environment.getProperty(EMAIL_SERVICES_REPLYTO_ADDRESS); } public Boolean requireSignedRequests() { return environment.getProperty(SECURITY_AUTHORIZATION_REQUIRE_SIGNED_REQUESTS).equalsIgnoreCase("true"); } } ================================================ FILE: src/main/java/com/porterhead/rest/config/ApplicationDevConfig.java ================================================ package com.porterhead.rest.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; /** * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 21/09/2012 */ @Configuration @Profile(value={"dev", "local"}) @PropertySource({"classpath:/properties/dev-app.properties"}) public class ApplicationDevConfig { @Bean public TextEncryptor textEncryptor() { return Encryptors.noOpText(); } } ================================================ FILE: src/main/java/com/porterhead/rest/config/ApplicationProductionConfig.java ================================================ package com.porterhead.rest.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; /** * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 21/09/2012 */ @Configuration @Profile(value="production") @PropertySource({"classpath:/properties/production-app.properties"}) public class ApplicationProductionConfig { @Autowired Environment environment; @Bean public TextEncryptor textEncryptor() { return Encryptors.queryableText(environment.getProperty("security.encryptPassword"), environment.getProperty("security.encryptSalt")); } } ================================================ FILE: src/main/java/com/porterhead/rest/config/ApplicationStagingConfig.java ================================================ package com.porterhead.rest.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 21/09/2012 */ @Configuration @Profile(value="staging") @PropertySource({"classpath:/properties/staging-app.properties"}) public class ApplicationStagingConfig { @Autowired Environment environment; @Bean public TextEncryptor textEncryptor() { return Encryptors.queryableText(environment.getProperty("security.encryptPassword"), environment.getProperty("security.encryptSalt")); } } ================================================ FILE: src/main/java/com/porterhead/rest/exception/ApplicationRuntimeException.java ================================================ package com.porterhead.rest.exception; public class ApplicationRuntimeException extends BaseWebApplicationException { public ApplicationRuntimeException(String applicationMessage) { super(500, "50002", "Internal System error", applicationMessage); } } ================================================ FILE: src/main/java/com/porterhead/rest/exception/BaseWebApplicationException.java ================================================ package com.porterhead.rest.exception; import com.porterhead.rest.api.ErrorResponse; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 19/10/2012 */ public abstract class BaseWebApplicationException extends WebApplicationException { private final int status; private final String errorMessage; private final String errorCode; private final String developerMessage; public BaseWebApplicationException(int httpStatus, String errorCode, String errorMessage, String developerMessage) { this.status = httpStatus; this.errorMessage = errorMessage; this.errorCode = errorCode; this.developerMessage = developerMessage; } @Override public Response getResponse() { return Response.status(status).type(MediaType.APPLICATION_JSON_TYPE).entity(getErrorResponse()).build(); } public ErrorResponse getErrorResponse() { ErrorResponse response = new ErrorResponse(); response.setErrorCode(errorCode); response.setApplicationMessage(developerMessage); response.setConsumerMessage(errorMessage); return response; } } ================================================ FILE: src/main/java/com/porterhead/rest/exception/NotFoundException.java ================================================ package com.porterhead.rest.exception; import javax.ws.rs.WebApplicationException; /** * User: porter * Date: 03/05/2012 * Time: 12:27 */ public class NotFoundException extends WebApplicationException { public NotFoundException() { super(404); } } ================================================ FILE: src/main/java/com/porterhead/rest/exception/ValidationException.java ================================================ package com.porterhead.rest.exception; import com.porterhead.rest.api.ErrorResponse; import com.porterhead.rest.api.ValidationError; import javax.validation.ConstraintViolation; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * User: porter * Date: 03/05/2012 * Time: 21:43 */ public class ValidationException extends WebApplicationException { private final int status = 400; private String errorMessage; private String developerMessage; private List errors = new ArrayList(); public ValidationException() { errorMessage = "Validation Error"; developerMessage = "The data passed in the request was invalid. Please check and resubmit"; } public ValidationException(String message) { super(); errorMessage = message; } public ValidationException(Set> violations) { this(); for(ConstraintViolation constraintViolation : violations) { ValidationError error = new ValidationError(); error.setMessage(constraintViolation.getMessage()); error.setPropertyName(constraintViolation.getPropertyPath().toString()); error.setPropertyValue(constraintViolation.getInvalidValue() != null ? constraintViolation.getInvalidValue().toString() : null); errors.add(error); } } @Override public Response getResponse() { return Response.status(status).type(MediaType.APPLICATION_JSON_TYPE).entity(getErrorResponse()).build(); } public ErrorResponse getErrorResponse() { ErrorResponse response = new ErrorResponse(); response.setApplicationMessage(developerMessage); response.setConsumerMessage(errorMessage); response.setValidationErrors(errors); return response; } } ================================================ FILE: src/main/java/com/porterhead/rest/filter/ResourceFilterFactory.java ================================================ package com.porterhead.rest.filter; import com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory; import com.sun.jersey.api.model.AbstractMethod; import com.sun.jersey.spi.container.ResourceFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.ws.rs.ext.Provider; import java.util.ArrayList; import java.util.List; /** * Add the SecurityContextFilter to the list of Filters to apply to requests * * This factory is registered with the Web Context: * * * com.sun.jersey.spi.container.ResourceFilters com.porterhead.com.porterhead.rest.filter.ResourceFilterFactory * * * * @author: Iain Porter */ @Component @Provider public class ResourceFilterFactory extends RolesAllowedResourceFilterFactory { @Autowired private SecurityContextFilter securityContextFilter; @Override public List create(AbstractMethod am) { List filters = super.create(am); if (filters == null) { filters = new ArrayList(); } List securityFilters = new ArrayList(filters); //put the Security Filter first in line securityFilters.add(0, securityContextFilter); return securityFilters; } } ================================================ FILE: src/main/java/com/porterhead/rest/filter/SecurityContextFilter.java ================================================ package com.porterhead.rest.filter; import com.porterhead.rest.authorization.AuthorizationRequestContext; import com.porterhead.rest.authorization.AuthorizationService; import com.porterhead.rest.authorization.impl.RequestSigningAuthorizationService; import com.porterhead.rest.authorization.impl.SecurityContextImpl; import com.porterhead.rest.authorization.impl.SessionTokenAuthorizationService; import com.porterhead.rest.config.ApplicationConfig; import com.porterhead.rest.user.UserRepository; import com.porterhead.rest.user.UserService; import com.porterhead.rest.user.api.ExternalUser; import com.sun.jersey.spi.container.ContainerRequest; import com.sun.jersey.spi.container.ContainerRequestFilter; import com.sun.jersey.spi.container.ContainerResponseFilter; import com.sun.jersey.spi.container.ResourceFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.ws.rs.ext.Provider; /** * A Servlet filter class for authorizing requests. * * * The role of this filter class is to set a {@link javax.ws.rs.core.SecurityContext} in the {@link com.sun.jersey.spi.container.ContainerRequest} * * @see {@link com.porterhead.rest.authorization.impl.SecurityContextImpl} * * @author: Iain Porter */ @Component @Provider public class SecurityContextFilter implements ResourceFilter, ContainerRequestFilter { private static final Logger LOG = LoggerFactory.getLogger(SecurityContextFilter.class); protected static final String HEADER_AUTHORIZATION = "Authorization"; protected static final String HEADER_DATE = "x-java-rest-date"; protected static final String HEADER_NONCE = "nonce"; private AuthorizationService authorizationService; ApplicationConfig config; @Autowired public SecurityContextFilter(UserRepository userRepository, UserService userService, ApplicationConfig config) { delegateAuthorizationService(userRepository, userService, config); this.config = config; } /** * If there is an Authorisation header in the request extract the session token and retrieve the user * * Delegate to the AuthorizationService to validate the request * * If the request has a valid session token and the user is validated then a user object will be added to the security context * * Any Resource Controllers can assume the user has been validated and can merely authorize based on the role * * Resources with @PermitAll annotation do not require an Authorization header but will still be filtered * * @param request the ContainerRequest to filter * * @return the ContainerRequest with a SecurityContext added */ public ContainerRequest filter(ContainerRequest request) { String authToken = request.getHeaderValue(HEADER_AUTHORIZATION); String requestDateString = request.getHeaderValue(HEADER_DATE); String nonce = request.getHeaderValue(HEADER_NONCE); AuthorizationRequestContext context = new AuthorizationRequestContext(request.getPath(), request.getMethod(), requestDateString, nonce, authToken); ExternalUser externalUser = authorizationService.authorize(context); request.setSecurityContext(new SecurityContextImpl(externalUser)); return request; } /** * Specify the AuthorizationService that the application should use * * @param userRepository * @param userService * @param config */ private void delegateAuthorizationService(UserRepository userRepository, UserService userService, ApplicationConfig config) { if(config.requireSignedRequests()) { this.authorizationService = new RequestSigningAuthorizationService(userRepository, userService, config); } else { this.authorizationService = new SessionTokenAuthorizationService(userRepository); } } public ContainerRequestFilter getRequestFilter() { return this; } public ContainerResponseFilter getResponseFilter() { return null; } @Autowired public void setConfig(ApplicationConfig config) { this.config = config; } } ================================================ FILE: src/main/java/com/porterhead/rest/gateway/EmailServicesGateway.java ================================================ package com.porterhead.rest.gateway; import com.porterhead.rest.user.EmailServiceTokenModel; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 11/09/2012 */ public interface EmailServicesGateway { public void sendVerificationToken(EmailServiceTokenModel model); } ================================================ FILE: src/main/java/com/porterhead/rest/model/BaseEntity.java ================================================ package com.porterhead.rest.model; import org.springframework.data.jpa.domain.AbstractPersistable; import org.springframework.util.Assert; import javax.persistence.Column; import javax.persistence.MappedSuperclass; import javax.persistence.Version; import java.util.Date; import java.util.UUID; /** * Base class for all JPA Entities * * @author : Iain Porter */ @MappedSuperclass public abstract class BaseEntity extends AbstractPersistable { @Version private int version; /** * All objects will have a unique UUID which allows for the decoupling from DB generated ids * */ @Column(length=36) private String uuid; private Date timeCreated; public BaseEntity() { this(UUID.randomUUID()); } public BaseEntity(UUID guid) { Assert.notNull(guid, "UUID is required"); setUuid(guid.toString()); this.timeCreated = new Date(); } public UUID getUuid() { return UUID.fromString(uuid); } public void setUuid(String uuid) { this.uuid = uuid; } public int hashCode() { return getUuid().hashCode(); } /** * In most instances we can rely on the UUID to identify the object. * Subclasses may want a user friendly identifier for constructing easy to read urls * * * /user/1883c578-76be-47fb-a5c1-7bbea3bf7fd0 using uuid as the identifier * * /user/jsmith using the username as the identifier * * * * @return Object unique identifier for the object */ public Object getIdentifier() { return getUuid().toString(); } public int getVersion() { return version; } public Date getTimeCreated() { return timeCreated; } } ================================================ FILE: src/main/java/com/porterhead/rest/resource/GenericExceptionMapper.java ================================================ package com.porterhead.rest.resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; /** * User: porter * Date: 22/03/2012 * Time: 15:56 */ @Provider public class GenericExceptionMapper implements ExceptionMapper { private static Logger LOG = LoggerFactory.getLogger(GenericExceptionMapper.class); public Response toResponse(Exception exception) { if (exception instanceof WebApplicationException) { LOG.info("Web Application Exception: " + exception); return ((WebApplicationException) exception).getResponse(); } LOG.error("Internal Server Error: " + exception); LOG.error("Internal Server Error: " + exception.getCause()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } } ================================================ FILE: src/main/java/com/porterhead/rest/resource/HealthCheckResource.java ================================================ package com.porterhead.rest.resource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import javax.annotation.security.PermitAll; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * User: porter * Date: 11/04/2012 * Time: 15:21 */ @Path("/healthcheck") @Component @Produces({MediaType.TEXT_PLAIN}) @PropertySource("classpath:properties/app.properties") public class HealthCheckResource { @Autowired Environment env; @PermitAll @GET public Response ping() { return Response.ok().entity("Running version " + env.getProperty("application.version")).build(); } } ================================================ FILE: src/main/java/com/porterhead/rest/service/BaseService.java ================================================ package com.porterhead.rest.service; import com.porterhead.rest.exception.ValidationException; import javax.validation.ConstraintViolation; import javax.validation.Validator; import java.util.Set; /** * @version 1.0 * @author: Iain Porter * @since 08/05/2013 */ public abstract class BaseService { private Validator validator; public BaseService(Validator validator) { this.validator = validator; } protected void validate(Object request) { Set> constraintViolations = validator.validate(request); if (constraintViolations.size() > 0) { throw new ValidationException(constraintViolations); } } } ================================================ FILE: src/main/java/com/porterhead/rest/user/EmailServiceTokenModel.java ================================================ package com.porterhead.rest.user; import com.porterhead.rest.user.domain.User; import com.porterhead.rest.user.domain.VerificationToken; import org.apache.commons.codec.binary.Base64; import java.io.Serializable; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 13/09/2012 */ public class EmailServiceTokenModel implements Serializable { private final String emailAddress; private final String token; private final VerificationToken.VerificationTokenType tokenType; private final String hostNameUrl; public EmailServiceTokenModel(User user, VerificationToken token, String hostNameUrl) { this.emailAddress = user.getEmailAddress(); this.token = token.getToken(); this.tokenType = token.getTokenType(); this.hostNameUrl = hostNameUrl; } public String getEmailAddress() { return emailAddress; } public String getEncodedToken() { return new String(Base64.encodeBase64(token.getBytes())); } public String getToken() { return token; } public VerificationToken.VerificationTokenType getTokenType() { return tokenType; } public String getHostNameUrl() { return hostNameUrl; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/SocialUserRepository.java ================================================ package com.porterhead.rest.user; import com.porterhead.rest.user.domain.SocialUser; import com.porterhead.rest.user.domain.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.util.MultiValueMap; import java.util.List; import java.util.Set; /** * User: porter * Date: 15/05/2012 * Time: 16:35 */ public interface SocialUserRepository extends JpaRepository { List findAllByUser(User user); List findByUserAndProviderId(User user, String providerId); List findByProviderIdAndProviderUserId(String providerId, String providerUserId); //TODO will need a JPA Query here List findByUserAndProviderUserId(User user, MultiValueMap providerUserIds); @Query("Select userId from SocialUser where providerId = ? AND providerUserId in (?)") Set findByProviderIdAndProviderUserId(String providerId, Set providerUserIds); SocialUser findByUserAndProviderIdAndProviderUserId(User user, String providerId, String providerUserId); } ================================================ FILE: src/main/java/com/porterhead/rest/user/UserRepository.java ================================================ package com.porterhead.rest.user; import com.porterhead.rest.user.domain.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.Date; import java.util.List; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 12/04/2012 */ public interface UserRepository extends JpaRepository { User findByEmailAddress(String emailAddress); @Query("select u from User u where uuid = ?") User findByUuid(String uuid); @Query("select u from User u where u in (select user from AuthorizationToken where lastUpdated < ?)") List findByExpiredSession(Date lastUpdated); @Query("select u from User u where u = (select user from AuthorizationToken where token = ?)") User findBySession(String token); } ================================================ FILE: src/main/java/com/porterhead/rest/user/UserService.java ================================================ package com.porterhead.rest.user; import com.porterhead.rest.user.api.*; import com.porterhead.rest.user.domain.AuthorizationToken; import com.porterhead.rest.user.domain.Role; import com.porterhead.rest.user.domain.User; import org.springframework.social.connect.Connection; /** * @author: Iain Porter * * Service to manage users */ public interface UserService { /** * Create a new User with the given role * * @param request * @param role * @return AuthenticatedUserToken */ public AuthenticatedUserToken createUser(CreateUserRequest request, Role role); /** * Create a Default User with a given role * * @param role * @return AuthenticatedUserToken */ public AuthenticatedUserToken createUser(Role role); /** * Login a User * * @param request * @return AuthenticatedUserToken */ public AuthenticatedUserToken login(LoginRequest request); /** * Log in a User using Connection details from an authorized request from the User's supported Social provider * encapsulated in the {@link org.springframework.social.connect.Connection} parameter * * @param connection containing the details of the authorized user account form the Social provider * @return the User account linked to the {@link com.porterhead.rest.user.domain.SocialUser} account */ public AuthenticatedUserToken socialLogin(Connection connection); /** * Get a User based on a unique identifier * * Identifiers supported are uuid, emailAddress * * @param userIdentifier * @return User */ public ExternalUser getUser(ExternalUser requestingUser, String userIdentifier); /** * Delete user, only authenticated user accounts can be deleted * * @param userMakingRequest the user authorized to delete the user * @param userId the id of the user to delete */ public void deleteUser(ExternalUser userMakingRequest, String userId); /** * Save User * * @param userId * @param request */ public ExternalUser saveUser(String userId, UpdateUserRequest request); /** * Create an AuthorizationToken for the User * * @return */ public AuthorizationToken createAuthorizationToken(User user); } ================================================ FILE: src/main/java/com/porterhead/rest/user/UserServiceImpl.java ================================================ package com.porterhead.rest.user; import com.porterhead.rest.config.ApplicationConfig; import com.porterhead.rest.service.BaseService; import com.porterhead.rest.user.api.*; import com.porterhead.rest.user.domain.AuthorizationToken; import com.porterhead.rest.user.domain.Role; import com.porterhead.rest.user.domain.User; import com.porterhead.rest.user.exception.AuthenticationException; import com.porterhead.rest.user.exception.AuthorizationException; import com.porterhead.rest.user.exception.DuplicateUserException; import com.porterhead.rest.user.exception.UserNotFoundException; import com.porterhead.rest.user.social.JpaUsersConnectionRepository; import com.porterhead.rest.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.Connection; import org.springframework.social.connect.UserProfile; import org.springframework.social.connect.UsersConnectionRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import javax.validation.Validator; import java.util.List; /** * Service for managing User accounts * * @author: Iain Porter */ @Service("userService") public class UserServiceImpl extends BaseService implements UserService { /** * For Social API handling */ private UsersConnectionRepository jpaUsersConnectionRepository; private UserRepository userRepository; private ApplicationConfig applicationConfig; public UserServiceImpl(Validator validator) { super(validator); } @Autowired public UserServiceImpl(UsersConnectionRepository usersConnectionRepository, Validator validator, ApplicationConfig applicationConfig) { this(validator); this.jpaUsersConnectionRepository = usersConnectionRepository; ((JpaUsersConnectionRepository)this.jpaUsersConnectionRepository).setUserService(this); this.applicationConfig = applicationConfig; } private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class); /** * {@inheritDoc} * * This method creates a User with the given Role. A check is made to see if the username already exists and a duplication * check is made on the email address if it is present in the request. *

* The password is hashed and a AuthorizationToken generated for subsequent authorization of role-protected requests. * */ @Transactional public AuthenticatedUserToken createUser(CreateUserRequest request, Role role) { validate(request); User searchedForUser = userRepository.findByEmailAddress(request.getUser().getEmailAddress()); if (searchedForUser != null) { throw new DuplicateUserException(); } User newUser = createNewUser(request, role); AuthenticatedUserToken token = new AuthenticatedUserToken(newUser.getUuid().toString(), createAuthorizationToken(newUser).getToken()); userRepository.save(newUser); return token; } @Transactional public AuthenticatedUserToken createUser(Role role) { User user = new User(); user.setRole(role); AuthenticatedUserToken token = new AuthenticatedUserToken(user.getUuid().toString(), createAuthorizationToken(user).getToken()); userRepository.save(user); return token; } /** * {@inheritDoc} * * Login supports authentication against an email attribute. * If a User is retrieved that matches, the password in the request is hashed * and compared to the persisted password for the User account. */ @Transactional public AuthenticatedUserToken login(LoginRequest request) { validate(request); User user = null; user = userRepository.findByEmailAddress(request.getUsername()); if (user == null) { throw new AuthenticationException(); } String hashedPassword = null; try { hashedPassword = user.hashPassword(request.getPassword()); } catch (Exception e) { throw new AuthenticationException(); } if (hashedPassword.equals(user.getHashedPassword())) { return new AuthenticatedUserToken(user.getUuid().toString(), createAuthorizationToken(user).getToken()); } else { throw new AuthenticationException(); } } /** * {@inheritDoc} * * Associate a Connection with a User account. If one does not exist a new User is created and linked to the * {@link com.porterhead.rest.user.domain.SocialUser} represented in the Connection details. * *

* * A AuthorizationToken is generated and any Profile data that can be collected from the Social account is propagated to the User object. * */ @Transactional public AuthenticatedUserToken socialLogin(Connection connection) { List userUuids = jpaUsersConnectionRepository.findUserIdsWithConnection(connection); if(userUuids.size() == 0) { throw new AuthenticationException(); } User user = userRepository.findByUuid(userUuids.get(0)); //take the first one if there are multiple userIds for this provider Connection if (user == null) { throw new AuthenticationException(); } updateUserFromProfile(connection, user); return new AuthenticatedUserToken(user.getUuid().toString(), createAuthorizationToken(user).getToken()); } /** * Allow user to get their own profile or a user with administrator role to get any profile * * @param requestingUser * @param userIdentifier * @return user */ @Transactional public ExternalUser getUser(ExternalUser requestingUser, String userIdentifier) { Assert.notNull(requestingUser); Assert.notNull(userIdentifier); User user = ensureUserIsLoaded(userIdentifier); if(!requestingUser.getId().equals(user.getUuid().toString()) && !requestingUser.getRole().equalsIgnoreCase(Role.administrator.toString())) { throw new AuthorizationException("User not authorized to load profile"); } return new ExternalUser(user); } @Transactional public void deleteUser(ExternalUser userMakingRequest, String userId) { Assert.notNull(userMakingRequest); Assert.notNull(userId); User userToDelete = ensureUserIsLoaded(userId); if (userMakingRequest.getRole().equalsIgnoreCase(Role.administrator.toString()) && (userToDelete.hasRole(Role.anonymous) || userToDelete.hasRole(Role.authenticated))) { userRepository.delete(userToDelete); } else { throw new AuthorizationException("User cannot be deleted. Only users with anonymous or authenticated role can be deleted."); } } @Transactional public ExternalUser saveUser(String userId, UpdateUserRequest request) { validate(request); User user = ensureUserIsLoaded(userId); if(request.getFirstName() != null) { user.setFirstName(request.getFirstName()); } if(request.getLastName() != null) { user.setLastName(request.getLastName()); } if(request.getEmailAddress() != null) { if(!request.getEmailAddress().equals(user.getEmailAddress())) { user.setEmailAddress(request.getEmailAddress()); user.setVerified(false); } } userRepository.save(user); return new ExternalUser(user); } @Override public AuthorizationToken createAuthorizationToken(User user) { if(user.getAuthorizationToken() == null || user.getAuthorizationToken().hasExpired()) { user.setAuthorizationToken(new AuthorizationToken(user, applicationConfig.getAuthorizationExpiryTimeInSeconds())); userRepository.save(user); } return user.getAuthorizationToken(); } private User createNewUser(CreateUserRequest request, Role role) { User userToSave = new User(request.getUser()); try { userToSave.setHashedPassword(userToSave.hashPassword(request.getPassword().getPassword())); } catch (Exception e) { throw new AuthenticationException(); } userToSave.setRole(role); return userToSave; } private void updateUserFromProfile(Connection connection, User user) { UserProfile profile = connection.fetchUserProfile(); user.setEmailAddress(profile.getEmail()); user.setFirstName(profile.getFirstName()); user.setLastName(profile.getLastName()); //users logging in from social network are already verified user.setVerified(true); if(user.hasRole(Role.anonymous)) { user.setRole(Role.authenticated); } userRepository.save(user); } private User ensureUserIsLoaded(String userIdentifier) { User user = null; if (StringUtil.isValidUuid(userIdentifier)) { user = userRepository.findByUuid(userIdentifier); } else { user = userRepository.findByEmailAddress(userIdentifier); } if (user == null) { throw new UserNotFoundException(); } return user; } @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/VerificationTokenRepository.java ================================================ package com.porterhead.rest.user; import com.porterhead.rest.user.domain.VerificationToken; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; /** * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 14/09/2012 */ public interface VerificationTokenRepository extends JpaRepository { @Query("select t from VerificationToken t where uuid = ?") VerificationToken findByUuid(String uuid); @Query("select t from VerificationToken t where token = ?") VerificationToken findByToken(String token); } ================================================ FILE: src/main/java/com/porterhead/rest/user/VerificationTokenService.java ================================================ package com.porterhead.rest.user; import com.porterhead.rest.user.api.LostPasswordRequest; import com.porterhead.rest.user.api.PasswordRequest; import com.porterhead.rest.user.domain.VerificationToken; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 10/09/2012 */ public interface VerificationTokenService { public VerificationToken sendEmailVerificationToken(String userId); public VerificationToken sendEmailRegistrationToken(String userId); public VerificationToken sendLostPasswordToken(LostPasswordRequest lostPasswordRequest); public VerificationToken verify(String base64EncodedToken); public VerificationToken generateEmailVerificationToken(String emailAddress); public VerificationToken resetPassword(String base64EncodedToken, PasswordRequest passwordRequest); } ================================================ FILE: src/main/java/com/porterhead/rest/user/VerificationTokenServiceImpl.java ================================================ package com.porterhead.rest.user; import com.porterhead.rest.config.ApplicationConfig; import com.porterhead.rest.gateway.EmailServicesGateway; import com.porterhead.rest.service.BaseService; import com.porterhead.rest.user.api.LostPasswordRequest; import com.porterhead.rest.user.api.PasswordRequest; import com.porterhead.rest.user.domain.Role; import com.porterhead.rest.user.domain.User; import com.porterhead.rest.user.domain.VerificationToken; import com.porterhead.rest.user.exception.*; import com.porterhead.rest.util.StringUtil; import org.apache.commons.codec.binary.Base64; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import javax.validation.Validator; /** * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 10/09/2012 */ @Service("verificationTokenService") public class VerificationTokenServiceImpl extends BaseService implements VerificationTokenService { private VerificationTokenRepository tokenRepository; private EmailServicesGateway emailServicesGateway; private UserRepository userRepository; ApplicationConfig config; public VerificationTokenServiceImpl(Validator validator) { super(validator); } @Autowired public VerificationTokenServiceImpl(UserRepository userRepository, VerificationTokenRepository tokenRepository, EmailServicesGateway emailServicesGateway, Validator validator) { this(validator); this.userRepository = userRepository; this.tokenRepository = tokenRepository; this.emailServicesGateway = emailServicesGateway; } @Transactional public VerificationToken sendEmailVerificationToken(String userId) { User user = ensureUserIsLoaded(userId); return sendEmailVerificationToken(user); } private VerificationToken sendEmailVerificationToken(User user) { VerificationToken token = new VerificationToken(user, VerificationToken.VerificationTokenType.emailVerification, config.getEmailVerificationTokenExpiryTimeInMinutes()); user.addVerificationToken(token); userRepository.save(user); emailServicesGateway.sendVerificationToken(new EmailServiceTokenModel(user, token, getConfig().getHostNameUrl())); return token; } @Transactional public VerificationToken sendEmailRegistrationToken(String userId) { User user = ensureUserIsLoaded(userId); VerificationToken token = new VerificationToken(user, VerificationToken.VerificationTokenType.emailRegistration, config.getEmailRegistrationTokenExpiryTimeInMinutes()); user.addVerificationToken(token); userRepository.save(user); emailServicesGateway.sendVerificationToken(new EmailServiceTokenModel(user, token, getConfig().getHostNameUrl())); return token; } /** * generate token if user found otherwise do nothing * * @param lostPasswordRequest * @return a token or null if user not found */ @Transactional public VerificationToken sendLostPasswordToken(LostPasswordRequest lostPasswordRequest) { validate(lostPasswordRequest); VerificationToken token = null; User user = userRepository.findByEmailAddress(lostPasswordRequest.getEmailAddress()); if (user != null) { token = user.getActiveLostPasswordToken(); if (token == null) { token = new VerificationToken(user, VerificationToken.VerificationTokenType.lostPassword, config.getLostPasswordTokenExpiryTimeInMinutes()); user.addVerificationToken(token); userRepository.save(user); } emailServicesGateway.sendVerificationToken(new EmailServiceTokenModel(user, token, getConfig().getHostNameUrl())); } return token; } @Transactional public VerificationToken verify(String base64EncodedToken) { VerificationToken token = loadToken(base64EncodedToken); if (token.isVerified() || token.getUser().isVerified()) { throw new AlreadyVerifiedException(); } token.setVerified(true); token.getUser().setVerified(true); userRepository.save(token.getUser()); return token; } @Transactional public VerificationToken generateEmailVerificationToken(String emailAddress) { Assert.notNull(emailAddress); User user = userRepository.findByEmailAddress(emailAddress); if (user == null) { throw new UserNotFoundException(); } if (user.isVerified()) { throw new AlreadyVerifiedException(); } //if token still active resend that VerificationToken token = user.getActiveEmailVerificationToken(); if (token == null) { token = sendEmailVerificationToken(user); } else { emailServicesGateway.sendVerificationToken(new EmailServiceTokenModel(user, token, getConfig().getHostNameUrl())); } return token; } @Transactional public VerificationToken resetPassword(String base64EncodedToken, PasswordRequest passwordRequest) { Assert.notNull(base64EncodedToken); validate(passwordRequest); VerificationToken token = loadToken(base64EncodedToken); if (token.isVerified()) { throw new AlreadyVerifiedException(); } token.setVerified(true); User user = token.getUser(); try { user.setHashedPassword(user.hashPassword(passwordRequest.getPassword())); } catch (Exception e) { throw new AuthenticationException(); } //set user to verified if not already and authenticated role user.setVerified(true); if (user.hasRole(Role.anonymous)) { user.setRole(Role.authenticated); } userRepository.save(user); return token; } private VerificationToken loadToken(String base64EncodedToken) { Assert.notNull(base64EncodedToken); String rawToken = new String(Base64.decodeBase64(base64EncodedToken)); VerificationToken token = tokenRepository.findByToken(rawToken); if (token == null) { throw new TokenNotFoundException(); } if (token.hasExpired()) { throw new TokenHasExpiredException(); } return token; } @Autowired public void setConfig(ApplicationConfig config) { this.config = config; } @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } public ApplicationConfig getConfig() { return this.config; } private User ensureUserIsLoaded(String userIdentifier) { User user = null; if (StringUtil.isValidUuid(userIdentifier)) { user = userRepository.findByUuid(userIdentifier); } else { user = userRepository.findByEmailAddress(userIdentifier); } if (user == null) { throw new UserNotFoundException(); } return user; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/AuthenticatedUserToken.java ================================================ package com.porterhead.rest.user.api; import javax.xml.bind.annotation.XmlRootElement; /** * @author: Iain Porter */ @XmlRootElement public class AuthenticatedUserToken { private String userId; private String token; public AuthenticatedUserToken(){} public AuthenticatedUserToken(String userId, String sessionToken) { this.userId = userId; this.token = sessionToken; } public String getUserId() { return userId; } public String getToken() { return token; } public void setUserId(String userId) { this.userId = userId; } public void setToken(String token) { this.token = token; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/CreateUserRequest.java ================================================ package com.porterhead.rest.user.api; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; /** * @author: Iain Porter */ @XmlRootElement public class CreateUserRequest { @NotNull @Valid private ExternalUser user; @NotNull @Valid private PasswordRequest password; public CreateUserRequest() { } public CreateUserRequest(final ExternalUser user, final PasswordRequest password) { this.user = user; this.password = password; } public ExternalUser getUser() { return user; } public void setUser(ExternalUser user) { this.user = user; } public PasswordRequest getPassword() { return password; } public void setPassword(PasswordRequest password) { this.password = password; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/EmailVerificationRequest.java ================================================ package com.porterhead.rest.user.api; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 15/09/2012 */ @XmlRootElement public class EmailVerificationRequest { @NotNull private String emailAddress; public EmailVerificationRequest() {} public EmailVerificationRequest(String emailAddress) { this.emailAddress = emailAddress; } public String getEmailAddress() { return emailAddress; } public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/ExternalUser.java ================================================ package com.porterhead.rest.user.api; import com.porterhead.rest.user.domain.AuthorizationToken; import com.porterhead.rest.user.domain.SocialUser; import com.porterhead.rest.user.domain.User; import org.codehaus.jackson.annotate.JsonIgnore; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; import java.security.Principal; import java.util.ArrayList; import java.util.List; /** * * @author: Iain Porter */ @XmlRootElement public class ExternalUser implements Principal { private String id; @Length(max=50) private String firstName; @Length(max=50) private String lastName; @NotNull @Email private String emailAddress; private boolean isVerified; @JsonIgnore private String role; private List socialProfiles = new ArrayList(); public ExternalUser() {} public ExternalUser(String userId) { this.id = userId; } public ExternalUser(User user) { this.id = user.getUuid().toString(); this.emailAddress = user.getEmailAddress(); this.firstName = user.getFirstName(); this.lastName = user.getLastName(); this.isVerified = user.isVerified(); for(SocialUser socialUser: user.getSocialUsers()) { SocialProfile profile = new SocialProfile(); profile.setDisplayName(socialUser.getDisplayName()); profile.setImageUrl(socialUser.getImageUrl()); profile.setProfileUrl(socialUser.getProfileUrl()); profile.setProvider(socialUser.getProviderId()); profile.setProviderUserId(socialUser.getProviderUserId()); socialProfiles.add(profile); } role = user.getRole().toString(); } public ExternalUser(User user, AuthorizationToken activeSession) { this(user); } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmailAddress() { return emailAddress; } public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } public List getSocialProfiles() { return socialProfiles; } public String getId() { return id; } public boolean isVerified() { return isVerified; } public String getName() { return emailAddress; } public String getRole() { return role; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/LoginRequest.java ================================================ package com.porterhead.rest.user.api; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; /** * * @author: Iain Porter */ @XmlRootElement public class LoginRequest { @NotNull private String username; @Length(min=8, max=30) @NotNull private String password; public LoginRequest(){} public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/LostPasswordRequest.java ================================================ package com.porterhead.rest.user.api; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 26/09/2012 */ @XmlRootElement public class LostPasswordRequest { @NotNull private String emailAddress; public LostPasswordRequest() {} public LostPasswordRequest(final String emailAddress) { this.emailAddress = emailAddress; } public String getEmailAddress() { return emailAddress; } public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/OAuth2Request.java ================================================ package com.porterhead.rest.user.api; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; /** * User: porter * Date: 18/05/2012 * Time: 09:59 */ @XmlRootElement public class OAuth2Request { private String accessToken; @NotNull public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/PasswordRequest.java ================================================ package com.porterhead.rest.user.api; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 28/09/2012 */ @XmlRootElement public class PasswordRequest { @Length(min=8, max=30) @NotNull private String password; public PasswordRequest() {} public PasswordRequest(final String password) { this.password = password; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/SocialProfile.java ================================================ package com.porterhead.rest.user.api; import javax.xml.bind.annotation.XmlRootElement; /** * @author: Iain Porter */ @XmlRootElement public class SocialProfile { String provider; String providerUserId; String profileUrl; String imageUrl; String displayName; public SocialProfile() {} public String getProvider() { return provider; } public void setProvider(String provider) { this.provider = provider; } public String getProviderUserId() { return providerUserId; } public void setProviderUserId(String providerUserId) { this.providerUserId = providerUserId; } public String getProfileUrl() { return profileUrl; } public void setProfileUrl(String profileUrl) { this.profileUrl = profileUrl; } public String getImageUrl() { return imageUrl; } public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/api/UpdateUserRequest.java ================================================ package com.porterhead.rest.user.api; import org.hibernate.validator.constraints.Email; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 05/10/2012 */ @XmlRootElement public class UpdateUserRequest { private String firstName; private String lastName; @Email @NotNull private String emailAddress; public UpdateUserRequest(){} public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmailAddress() { return emailAddress; } public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/domain/AuthorizationToken.java ================================================ package com.porterhead.rest.user.domain; import org.springframework.data.jpa.domain.AbstractPersistable; import javax.persistence.*; import java.util.Date; import java.util.UUID; /** * * @version 1.0 * @author: Iain Porter iain.porter * @since 28/12/2012 */ @Entity @Table(name="rest_authorization_token") public class AuthorizationToken extends AbstractPersistable { private final static Integer DEFAULT_TIME_TO_LIVE_IN_SECONDS = (60 * 60 * 24 * 30); //30 Days @Column(length=36) private String token; private Date timeCreated; private Date expirationDate; @JoinColumn(name = "user_id") @OneToOne(fetch = FetchType.LAZY) private User user; public AuthorizationToken() {} public AuthorizationToken(User user) { this(user, DEFAULT_TIME_TO_LIVE_IN_SECONDS); } public AuthorizationToken(User user, Integer timeToLiveInSeconds) { this.token = UUID.randomUUID().toString(); this.user = user; this.timeCreated = new Date(); this.expirationDate = new Date(System.currentTimeMillis() + (timeToLiveInSeconds * 1000L)); } public boolean hasExpired() { return this.expirationDate != null && this.expirationDate.before(new Date()); } public String getToken() { return token; } public User getUser() { return user; } public Date getTimeCreated() { return timeCreated; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/domain/Role.java ================================================ package com.porterhead.rest.user.domain; /** * User: porter * Date: 03/04/2012 * Time: 13:17 */ public enum Role { authenticated, administrator, anonymous } ================================================ FILE: src/main/java/com/porterhead/rest/user/domain/SocialUser.java ================================================ package com.porterhead.rest.user.domain; import com.porterhead.rest.model.BaseEntity; import javax.persistence.*; /** * User: porter * Date: 15/05/2012 * Time: 13:57 */ @Entity @Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"userId", "providerId", "providerUserId"}), @UniqueConstraint(columnNames = {"userId", "providerId", "rank"})}) public class SocialUser extends BaseEntity { @ManyToOne(cascade = CascadeType.MERGE, fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "userId", nullable = false, updatable = false) private User user; private String providerId; private String providerUserId; private int rank; private String displayName; private String profileUrl; private String imageUrl; @Column(length = 500) private String accessToken; private String secret; private String refreshToken; private Long expireTime; public String getProviderId() { return providerId; } public void setProviderId(String providerId) { this.providerId = providerId; } public String getProviderUserId() { return providerUserId; } public void setProviderUserId(String providerUserId) { this.providerUserId = providerUserId; } public int getRank() { return rank; } public void setRank(int rank) { this.rank = rank; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getProfileUrl() { return profileUrl; } public void setProfileUrl(String profileUrl) { this.profileUrl = profileUrl; } public String getImageUrl() { return imageUrl; } public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public String getRefreshToken() { return refreshToken; } public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } public Long getExpireTime() { return expireTime; } public void setExpireTime(Long expireTime) { this.expireTime = expireTime; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/domain/SocialUserBuilder.java ================================================ package com.porterhead.rest.user.domain; /** * User: porter * Date: 21/05/2012 * Time: 08:12 */ public class SocialUserBuilder { SocialUser user; public static SocialUserBuilder create() { return new SocialUserBuilder(); } public SocialUserBuilder() { user = new SocialUser(); } public SocialUser build() { return user; } public SocialUserBuilder withUser(User user) { this.user.setUser(user); return this; } public SocialUserBuilder withProviderId(String id) { this.user.setProviderId(id); return this; } public SocialUserBuilder withProviderUserId(String id) { this.user.setProviderUserId(id); return this; } public SocialUserBuilder withRank(int rank) { this.user.setRank(rank); return this; } public SocialUserBuilder withDisplayName(String name) { this.user.setDisplayName(name); return this; } public SocialUserBuilder withProfileUrl(String url) { this.user.setProfileUrl(url); return this; } public SocialUserBuilder withImageUrl(String url) { this.user.setImageUrl(url); return this; } public SocialUserBuilder withAccessToken(String token) { this.user.setAccessToken(token); return this; } public SocialUserBuilder withSecret(String secret) { this.user.setSecret(secret); return this; } public SocialUserBuilder withRefreshToken(String token) { this.user.setRefreshToken(token); return this; } public SocialUserBuilder withExpireTime(Long time) { this.user.setExpireTime(time); return this; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/domain/User.java ================================================ package com.porterhead.rest.user.domain; import com.porterhead.rest.model.BaseEntity; import com.porterhead.rest.user.api.ExternalUser; import com.porterhead.rest.util.HashUtil; import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollectionOption; import org.springframework.util.StringUtils; import javax.persistence.*; import java.security.MessageDigest; import java.util.*; /** * User: porter * Date: 09/03/2012 * Time: 18:56 */ @Entity @Table(name="rest_user") public class User extends BaseEntity { /** * Add additional salt to password hashing */ private static final String HASH_SALT = "d8a8e885-ecce-42bb-8332-894f20f0d8ed"; private static final int HASH_ITERATIONS = 1000; private String firstName; private String lastName; private String emailAddress; private String hashedPassword; private boolean isVerified; @Enumerated(EnumType.STRING) private Role role; @OneToMany(cascade={CascadeType.ALL}, mappedBy="user", fetch=FetchType.EAGER) private Set socialUsers = new HashSet(); @OneToMany(mappedBy="user", targetEntity=VerificationToken.class, cascade= CascadeType.ALL) @LazyCollection(LazyCollectionOption.FALSE) private List verificationTokens = new ArrayList(); @OneToOne(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL) private AuthorizationToken authorizationToken; public User() { this(UUID.randomUUID()); } public User(UUID uuid) { super(uuid); setRole(Role.anonymous); //all users are anonymous until credentials are proved } public User(ExternalUser externalUser) { this(); this.firstName = externalUser.getFirstName(); this.lastName = externalUser.getLastName(); this.emailAddress = externalUser.getEmailAddress(); } public void setHashedPassword(String hashedPassword) { this.hashedPassword = hashedPassword; } public String getHashedPassword() { return this.hashedPassword; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmailAddress() { return emailAddress; } public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } public boolean hasRole(Role role) { return role.equals(this.role); } public boolean equals(Object otherUser) { boolean response = false; if(otherUser == null) { response = false; } else if(! (otherUser instanceof User)) { response = false; } else { if(((User)otherUser).getUuid().equals(this.getUuid())) { response = true; } } return response; } public int hashCode() { return getUuid().hashCode(); } public String getName() { if(StringUtils.hasText(getFirstName())) { return getFirstName() + " " + getLastName(); } return ""; } public Set getSocialUsers() { return socialUsers; } public void setSocialUsers(Set socialUsers) { this.socialUsers = socialUsers; } public void addSocialUser(SocialUser socialUser) { getSocialUsers().add(socialUser); } public synchronized void addVerificationToken(VerificationToken token) { verificationTokens.add(token); } public synchronized List getVerificationTokens() { return Collections.unmodifiableList(this.verificationTokens); } public synchronized void setAuthorizationToken(AuthorizationToken token) { this.authorizationToken = token; } public synchronized AuthorizationToken getAuthorizationToken() { return authorizationToken; } /** * If the user has a VerificationToken of type VerificationTokenType.lostPassword * that is active return it otherwise return null * * @return verificationToken */ public VerificationToken getActiveLostPasswordToken() { return getActiveToken(VerificationToken.VerificationTokenType.lostPassword); } /** * If the user has a VerificationToken of type VerificationTokenType.emailVerification * that is active return it otherwise return null * * @return verificationToken */ public VerificationToken getActiveEmailVerificationToken() { return getActiveToken(VerificationToken.VerificationTokenType.emailVerification); } /** * If the user has a VerificationToken of type VerificationTokenType.emailRegistration * that is active return it otherwise return null * * @return verificationToken */ public VerificationToken getActiveEmailRegistrationToken() { return getActiveToken(VerificationToken.VerificationTokenType.emailRegistration); } private VerificationToken getActiveToken(VerificationToken.VerificationTokenType tokenType) { VerificationToken activeToken = null; for (VerificationToken token : getVerificationTokens()) { if (token.getTokenType().equals(tokenType) && !token.hasExpired() && !token.isVerified()) { activeToken = token; break; } } return activeToken; } public boolean isVerified() { return isVerified; } public void setVerified(boolean verified) { isVerified = verified; } /** * Hash the password using salt values * See https://www.owasp.org/index.php/Hashing_Java * * @param passwordToHash * @return hashed password */ public String hashPassword(String passwordToHash) throws Exception { return hashToken(passwordToHash, getUuid().toString() + HASH_SALT ); } private String hashToken(String token, String salt) throws Exception { return HashUtil.byteToBase64(getHash(HASH_ITERATIONS, token, salt.getBytes())); } public byte[] getHash(int numberOfIterations, String password, byte[] salt) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); digest.reset(); digest.update(salt); byte[] input = digest.digest(password.getBytes("UTF-8")); for (int i = 0; i < numberOfIterations; i++) { digest.reset(); input = digest.digest(input); } return input; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/domain/VerificationToken.java ================================================ package com.porterhead.rest.user.domain; import com.porterhead.rest.model.BaseEntity; import org.joda.time.DateTime; import javax.persistence.*; import java.util.Date; import java.util.UUID; /** * A token that gives the user permission to carry out a specific task once within a determined time period. * An example would be a Lost Password token. The user receives the token embedded in a link. * They send the token back to the server by clicking the link and the action is processed * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 10/09/2012 */ @Entity @Table(name = "rest_verification_token") public class VerificationToken extends BaseEntity { private static final int DEFAULT_EXPIRY_TIME_IN_MINS = 60 * 24; //24 hours @Column(length=36) private final String token; private Date expiryDate; @Enumerated(EnumType.STRING) private VerificationTokenType tokenType; private boolean verified; @ManyToOne @JoinColumn(name = "user_id") User user; public VerificationToken() { super(); this.token = UUID.randomUUID().toString(); this.expiryDate = calculateExpiryDate(DEFAULT_EXPIRY_TIME_IN_MINS); } public VerificationToken(User user, VerificationTokenType tokenType, int expirationTimeInMinutes) { this(); this.user = user; this.tokenType = tokenType; this.expiryDate = calculateExpiryDate(expirationTimeInMinutes); } public VerificationTokenType getTokenType() { return tokenType; } public boolean isVerified() { return verified; } public void setVerified(boolean verified) { this.verified = verified; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Date getExpiryDate() { return expiryDate; } public String getToken() { return token; } private Date calculateExpiryDate(int expiryTimeInMinutes) { DateTime now = new DateTime(); return now.plusMinutes(expiryTimeInMinutes).toDate(); } public enum VerificationTokenType { lostPassword, emailVerification, emailRegistration } public boolean hasExpired() { DateTime tokenDate = new DateTime(getExpiryDate()); return tokenDate.isBeforeNow(); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/exception/AlreadyVerifiedException.java ================================================ package com.porterhead.rest.user.exception; import com.porterhead.rest.exception.BaseWebApplicationException; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 14/09/2012 */ public class AlreadyVerifiedException extends BaseWebApplicationException { public AlreadyVerifiedException() { super(409, "40905", "Already verified", "The token has already been verified"); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/exception/AuthenticationException.java ================================================ package com.porterhead.rest.user.exception; import com.porterhead.rest.exception.BaseWebApplicationException; /** * User: porter * Date: 13/03/2012 * Time: 08:58 */ public class AuthenticationException extends BaseWebApplicationException { public AuthenticationException() { super(401, "40102", "Authentication Error", "Authentication Error. The username or password were incorrect"); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/exception/AuthorizationException.java ================================================ package com.porterhead.rest.user.exception; import com.porterhead.rest.exception.BaseWebApplicationException; /** * User: porter * Date: 04/04/2012 * Time: 15:32 */ public class AuthorizationException extends BaseWebApplicationException { public AuthorizationException(String applicationMessage) { super(403, "40301", "Not authorized", applicationMessage); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/exception/DuplicateUserException.java ================================================ package com.porterhead.rest.user.exception; import com.porterhead.rest.exception.BaseWebApplicationException; /** * User: porter * Date: 12/03/2012 * Time: 15:10 */ public class DuplicateUserException extends BaseWebApplicationException { public DuplicateUserException() { super(409, "40901", "User already exists", "An attempt was made to create a user that already exists"); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/exception/TokenHasExpiredException.java ================================================ package com.porterhead.rest.user.exception; import com.porterhead.rest.exception.BaseWebApplicationException; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 14/09/2012 */ public class TokenHasExpiredException extends BaseWebApplicationException { public TokenHasExpiredException() { super(403, "40304", "Token has expired", "An attempt was made to load a token that has expired"); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/exception/TokenNotFoundException.java ================================================ package com.porterhead.rest.user.exception; import com.porterhead.rest.exception.BaseWebApplicationException; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 14/09/2012 */ public class TokenNotFoundException extends BaseWebApplicationException { public TokenNotFoundException() { super(404, "40407", "Token Not Found", "No token could be found for that Id"); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/exception/UserNotFoundException.java ================================================ package com.porterhead.rest.user.exception; import com.porterhead.rest.exception.BaseWebApplicationException; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 12/09/2012 */ public class UserNotFoundException extends BaseWebApplicationException { public UserNotFoundException() { super(404, "40402", "User Not Found", "No User could be found for that Id"); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/mail/MailSenderService.java ================================================ package com.porterhead.rest.user.mail; import com.porterhead.rest.user.EmailServiceTokenModel; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 13/09/2012 */ public interface MailSenderService { public EmailServiceTokenModel sendVerificationEmail(EmailServiceTokenModel emailServiceTokenModel); public EmailServiceTokenModel sendRegistrationEmail(EmailServiceTokenModel emailServiceTokenModel); public EmailServiceTokenModel sendLostPasswordEmail(EmailServiceTokenModel emailServiceTokenModel); } ================================================ FILE: src/main/java/com/porterhead/rest/user/mail/MockJavaMailSender.java ================================================ package com.porterhead.rest.user.mail; import org.springframework.mail.MailException; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessagePreparator; import javax.mail.Session; import javax.mail.internet.MimeMessage; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Properties; /** * @author: Iain Porter */ public class MockJavaMailSender implements JavaMailSender { List messages = new ArrayList(); public MimeMessage createMimeMessage() { MimeMessage message = new MimeMessage(Session.getInstance(new Properties())); return message; } public MimeMessage createMimeMessage(InputStream contentStream) throws MailException { return null; //To change body of implemented methods use File | Settings | File Templates. } public void send(MimeMessage mimeMessage) throws MailException { //To change body of implemented methods use File | Settings | File Templates. } public void send(MimeMessage[] mimeMessages) throws MailException { //To change body of implemented methods use File | Settings | File Templates. } public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException { try { MimeMessage mimeMessage = createMimeMessage(); mimeMessagePreparator.prepare(mimeMessage); messages.add(mimeMessage); } catch(Exception e) { System.out.println("Exception while preparing Mail Message" + e); throw new RuntimeException(e); } } public void send(MimeMessagePreparator[] mimeMessagePreparators) throws MailException { //To change body of implemented methods use File | Settings | File Templates. } public void send(SimpleMailMessage simpleMessage) throws MailException { //To change body of implemented methods use File | Settings | File Templates. } public void send(SimpleMailMessage[] simpleMessages) throws MailException { //To change body of implemented methods use File | Settings | File Templates. } public List getMessages() { return messages; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/mail/impl/MailSenderServiceImpl.java ================================================ package com.porterhead.rest.user.mail.impl; import com.porterhead.rest.config.ApplicationConfig; import com.porterhead.rest.user.EmailServiceTokenModel; import com.porterhead.rest.user.mail.*; import org.apache.velocity.app.VelocityEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.mail.javamail.MimeMessagePreparator; import org.springframework.stereotype.Service; import org.springframework.ui.velocity.VelocityEngineUtils; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.HashMap; import java.util.Map; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 13/09/2012 */ @Service("mailSenderService") public class MailSenderServiceImpl implements MailSenderService { private static Logger LOG = LoggerFactory.getLogger(MailSenderServiceImpl.class); private final JavaMailSender mailSender; private final VelocityEngine velocityEngine; private ApplicationConfig config; @Autowired public MailSenderServiceImpl(JavaMailSender mailSender, VelocityEngine velocityEngine) { this.mailSender = mailSender; this.velocityEngine = velocityEngine; } public EmailServiceTokenModel sendVerificationEmail(final EmailServiceTokenModel emailVerificationModel) { Map resources = new HashMap(); return sendVerificationEmail(emailVerificationModel, config.getEmailVerificationSubjectText(), "META-INF/velocity/VerifyEmail.vm", resources); } public EmailServiceTokenModel sendRegistrationEmail(final EmailServiceTokenModel emailVerificationModel) { Map resources = new HashMap(); return sendVerificationEmail(emailVerificationModel, config.getEmailRegistrationSubjectText(), "META-INF/velocity/RegistrationEmail.vm", resources); } public EmailServiceTokenModel sendLostPasswordEmail(final EmailServiceTokenModel emailServiceTokenModel) { Map resources = new HashMap(); return sendVerificationEmail(emailServiceTokenModel, config.getLostPasswordSubjectText(), "META-INF/velocity/LostPasswordEmail.vm", resources); } private void addInlineResource(MimeMessageHelper messageHelper, String resourcePath, String resourceIdentifier) throws MessagingException { Resource resource = new ClassPathResource(resourcePath); messageHelper.addInline(resourceIdentifier, resource); } private EmailServiceTokenModel sendVerificationEmail(final EmailServiceTokenModel emailVerificationModel, final String emailSubject, final String velocityModel, final Map resources) { MimeMessagePreparator preparator = new MimeMessagePreparator() { public void prepare(MimeMessage mimeMessage) throws Exception { MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, MimeMessageHelper.MULTIPART_MODE_RELATED, "UTF-8"); messageHelper.setTo(emailVerificationModel.getEmailAddress()); messageHelper.setFrom(config.getEmailFromAddress()); messageHelper.setReplyTo(config.getEmailReplyToAddress()); messageHelper.setSubject(emailSubject); Map model = new HashMap(); model.put("model", emailVerificationModel); String text = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, velocityModel, model); messageHelper.setText(new String(text.getBytes(), "UTF-8"), true); for(String resourceIdentifier: resources.keySet()) { addInlineResource(messageHelper, resources.get(resourceIdentifier), resourceIdentifier); } } }; LOG.debug("Sending {} token to : {}",emailVerificationModel.getTokenType().toString(), emailVerificationModel.getEmailAddress()); this.mailSender.send(preparator); return emailVerificationModel; } @Autowired public void setConfig(ApplicationConfig config) { this.config = config; } public ApplicationConfig getConfig() { return this.config; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/resource/PasswordResource.java ================================================ package com.porterhead.rest.user.resource; import com.porterhead.rest.user.VerificationTokenService; import com.porterhead.rest.user.api.LostPasswordRequest; import com.porterhead.rest.user.api.PasswordRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.security.PermitAll; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 28/09/2012 */ @Path("password") @Component @Produces({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON}) public class PasswordResource { @Autowired protected VerificationTokenService verificationTokenService; @PermitAll @Path("tokens") @POST public Response sendEmailToken(LostPasswordRequest request) { verificationTokenService.sendLostPasswordToken(request); return Response.ok().build(); } @PermitAll @Path("tokens/{token}") @POST public Response resetPassword(@PathParam("token") String base64EncodedToken, PasswordRequest request) { verificationTokenService.resetPassword(base64EncodedToken, request); return Response.ok().build(); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/resource/UserResource.java ================================================ package com.porterhead.rest.user.resource; import com.porterhead.rest.config.ApplicationConfig; import com.porterhead.rest.gateway.EmailServicesGateway; import com.porterhead.rest.user.UserService; import com.porterhead.rest.user.VerificationTokenService; import com.porterhead.rest.user.api.*; import com.porterhead.rest.user.domain.Role; import com.porterhead.rest.user.exception.AuthorizationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.support.OAuth2ConnectionFactory; import org.springframework.social.oauth2.AccessGrant; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.annotation.security.PermitAll; import javax.annotation.security.RolesAllowed; import javax.ws.rs.*; import javax.ws.rs.core.*; import java.net.URI; /** * User: porter * Date: 12/03/2012 * Time: 18:57 */ @Path("/user") @Component @Produces({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON}) public class UserResource { private ConnectionFactoryLocator connectionFactoryLocator; @Autowired protected UserService userService; @Autowired protected VerificationTokenService verificationTokenService; @Autowired protected EmailServicesGateway emailServicesGateway; @Context protected UriInfo uriInfo; @Autowired ApplicationConfig config; @Autowired public UserResource(ConnectionFactoryLocator connectionFactoryLocator) { this.connectionFactoryLocator = connectionFactoryLocator; } @PermitAll @POST public Response signupUser(CreateUserRequest request) { AuthenticatedUserToken token = userService.createUser(request, Role.authenticated); verificationTokenService.sendEmailRegistrationToken(token.getUserId()); URI location = uriInfo.getAbsolutePathBuilder().path(token.getUserId()).build(); return Response.created(location).entity(token).build(); } @RolesAllowed("admin") @Path("{userId}") @DELETE public Response deleteUser(@Context SecurityContext sc, @PathParam("userId") String userId) { ExternalUser userMakingRequest = (ExternalUser)sc.getUserPrincipal(); userService.deleteUser(userMakingRequest, userId); return Response.ok().build(); } @PermitAll @Path("login") @POST public Response login(LoginRequest request) { AuthenticatedUserToken token = userService.login(request); return getLoginResponse(token); } @PermitAll @Path("login/{providerId}") @POST public Response socialLogin(@PathParam("providerId") String providerId, OAuth2Request request) { OAuth2ConnectionFactory connectionFactory = (OAuth2ConnectionFactory) connectionFactoryLocator.getConnectionFactory(providerId); Connection connection = connectionFactory.createConnection(new AccessGrant(request.getAccessToken())); AuthenticatedUserToken token = userService.socialLogin(connection); return getLoginResponse(token); } @RolesAllowed({"authenticated"}) @Path("{userId}") @GET public Response getUser(@Context SecurityContext sc, @PathParam("userId") String userId) { ExternalUser userMakingRequest = (ExternalUser)sc.getUserPrincipal(); ExternalUser user = userService.getUser(userMakingRequest, userId); return Response.ok().entity(user).build(); } @RolesAllowed({"authenticated"}) @Path("{userId}") @PUT public Response updateUser(@Context SecurityContext sc, @PathParam("userId") String userId, UpdateUserRequest request) { ExternalUser userMakingRequest = (ExternalUser)sc.getUserPrincipal(); if(!userMakingRequest.getId().equals(userId)) { throw new AuthorizationException("User not authorized to modify this profile"); } boolean sendVerificationToken = StringUtils.hasLength(request.getEmailAddress()) && !request.getEmailAddress().equals(userMakingRequest.getEmailAddress()); ExternalUser savedUser = userService.saveUser(userId, request); if(sendVerificationToken) { verificationTokenService.sendEmailVerificationToken(savedUser.getId()); } return Response.ok().build(); } private Response getLoginResponse(AuthenticatedUserToken token) { URI location = UriBuilder.fromPath(uriInfo.getBaseUri() + "user/" + token.getUserId()).build(); return Response.ok().entity(token).contentLocation(location).build(); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/resource/VerificationResource.java ================================================ package com.porterhead.rest.user.resource; import com.porterhead.rest.user.VerificationTokenService; import com.porterhead.rest.user.api.EmailVerificationRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.security.PermitAll; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * @version 1.0 * @author: Iain Porter iain.porter@porterhead.com * @since 14/09/2012 */ @Path("verify") @Component @Produces({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON}) public class VerificationResource { @Autowired protected VerificationTokenService verificationTokenService; @PermitAll @Path("tokens/{token}") @POST public Response verifyToken(@PathParam("token") String token) { verificationTokenService.verify(token); return Response.ok().build(); } @PermitAll @Path("tokens") @POST public Response sendEmailToken(EmailVerificationRequest request) { verificationTokenService.generateEmailVerificationToken(request.getEmailAddress()); return Response.ok().build(); } } ================================================ FILE: src/main/java/com/porterhead/rest/user/social/JpaConnectionRepository.java ================================================ package com.porterhead.rest.user.social; import com.porterhead.rest.user.SocialUserRepository; import com.porterhead.rest.user.UserRepository; import com.porterhead.rest.user.domain.SocialUser; import com.porterhead.rest.user.domain.SocialUserBuilder; import com.porterhead.rest.user.domain.User; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.social.connect.*; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import java.util.*; /** * User: porter * Date: 15/05/2012 * Time: 16:13 */ public class JpaConnectionRepository implements ConnectionRepository { private final SocialUserRepository socialUserRepository; private final UserRepository userRepository; private final User user; private final ConnectionFactoryLocator connectionFactoryLocator; private final TextEncryptor textEncryptor; public JpaConnectionRepository(SocialUserRepository socialUserRepository, UserRepository userRepository, User user, ConnectionFactoryLocator locator, TextEncryptor encryptor) { this.socialUserRepository = socialUserRepository; this.userRepository = userRepository; this.user = user; this.connectionFactoryLocator = locator; this.textEncryptor = encryptor; } public MultiValueMap> findAllConnections() { List> resultList = connectionMapper.mapEntities(socialUserRepository.findAllByUser(user)); MultiValueMap> connections = new LinkedMultiValueMap>(); Set registeredProviderIds = connectionFactoryLocator.registeredProviderIds(); for (String registeredProviderId : registeredProviderIds) { connections.put(registeredProviderId, Collections.>emptyList()); } for (Connection connection : resultList) { String providerId = connection.getKey().getProviderId(); if (connections.get(providerId).size() == 0) { connections.put(providerId, new LinkedList>()); } connections.add(providerId, connection); } return connections; } public List> findConnections(String providerId) { return connectionMapper.mapEntities(socialUserRepository.findByUserAndProviderId(user, providerId)); } @SuppressWarnings("unchecked") public List> findConnections(Class apiType) { List connections = findConnections(getProviderId(apiType)); return (List>) connections; } public MultiValueMap> findConnectionsToUsers(MultiValueMap providerUsers) { if (providerUsers.isEmpty()) { throw new IllegalArgumentException("Unable to execute find: no providerUsers provided"); } List> resultList = connectionMapper.mapEntities(socialUserRepository.findByUserAndProviderUserId(user, providerUsers)); MultiValueMap> connectionsForUsers = new LinkedMultiValueMap>(); for (Connection connection : resultList) { String providerId = connection.getKey().getProviderId(); List userIds = providerUsers.get(providerId); List> connections = connectionsForUsers.get(providerId); if (connections == null) { connections = new ArrayList>(userIds.size()); for (int i = 0; i < userIds.size(); i++) { connections.add(null); } connectionsForUsers.put(providerId, connections); } String providerUserId = connection.getKey().getProviderUserId(); int connectionIndex = userIds.indexOf(providerUserId); connections.set(connectionIndex, connection); } return connectionsForUsers; } public Connection getConnection(ConnectionKey connectionKey) { try { return connectionMapper.mapEntity(socialUserRepository.findByUserAndProviderIdAndProviderUserId(user, connectionKey.getProviderId(), connectionKey.getProviderUserId())); } catch (EmptyResultDataAccessException e) { throw new NoSuchConnectionException(connectionKey); } } @SuppressWarnings("unchecked") public Connection getConnection(Class apiType, String providerUserId) { String providerId = getProviderId(apiType); return (Connection) getConnection(new ConnectionKey(providerId, providerUserId)); } @SuppressWarnings("unchecked") public Connection getPrimaryConnection(Class apiType) { String providerId = getProviderId(apiType); Connection connection = (Connection) findPrimaryConnection(providerId); if (connection == null) { throw new NotConnectedException(providerId); } return connection; } @SuppressWarnings("unchecked") public Connection findPrimaryConnection(Class apiType) { String providerId = getProviderId(apiType); return (Connection) findPrimaryConnection(providerId); } @Transactional public void addConnection(Connection connection) { try { ConnectionData data = connection.createData(); //TODO: currently only support 1 connection per user per provider (rank = 1) int rank = 1; //create a SocialUser and call save SocialUser socialUser = SocialUserBuilder.create().withUser(user).withProviderId(data.getProviderId()) .withProviderUserId(data.getProviderUserId()).withRank(rank).withDisplayName(data.getDisplayName()) .withProfileUrl(data.getProfileUrl()).withImageUrl(data.getImageUrl()).withAccessToken(encrypt(data.getAccessToken())) .withSecret(encrypt(data.getSecret())).withRefreshToken(encrypt(data.getRefreshToken())) .withExpireTime(data.getExpireTime()).build(); socialUserRepository.save(socialUser); } catch (DuplicateKeyException e) { throw new DuplicateConnectionException(connection.getKey()); } } public void updateConnection(Connection connection) { ConnectionData data = connection.createData(); SocialUser socialUser = socialUserRepository.findByUserAndProviderIdAndProviderUserId(user, data.getProviderId(), data.getProviderUserId()); if(socialUser != null){ socialUser.setDisplayName(data.getDisplayName()); socialUser.setProfileUrl(data.getProfileUrl()); socialUser.setImageUrl(data.getImageUrl()); socialUser.setAccessToken(encrypt(data.getAccessToken())); socialUser.setSecret(encrypt(data.getSecret())); socialUser.setRefreshToken(encrypt(data.getRefreshToken())); socialUser.setExpireTime(data.getExpireTime()); socialUser = socialUserRepository.save(socialUser); } } public void removeConnections(String providerId) { List users = socialUserRepository.findByUserAndProviderId(user, providerId); socialUserRepository.delete(users); } public void removeConnection(ConnectionKey connectionKey) { SocialUser socialUser = socialUserRepository.findByUserAndProviderIdAndProviderUserId(user, connectionKey.getProviderId(), connectionKey.getProviderUserId()); socialUserRepository.delete(socialUser); } private Connection findPrimaryConnection(String providerId) { List> connections = connectionMapper.mapEntities(socialUserRepository.findByUserAndProviderId(user, providerId)); if (connections.size() > 0) { return connections.get(0); } else { return null; } } private final ServiceProviderConnectionMapper connectionMapper = new ServiceProviderConnectionMapper(); private final class ServiceProviderConnectionMapper { public List> mapEntities(List socialUsers){ List> result = new ArrayList>(); for(SocialUser user : socialUsers){ result.add(mapEntity(user)); } return result; } public Connection mapEntity(SocialUser socialUser){ ConnectionData connectionData = mapConnectionData(socialUser); ConnectionFactory connectionFactory = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId()); return connectionFactory.createConnection(connectionData); } private ConnectionData mapConnectionData(SocialUser socialUser){ return new ConnectionData(socialUser.getProviderId(), socialUser.getProviderUserId(), socialUser.getDisplayName(), socialUser.getProfileUrl(), socialUser.getImageUrl(), decrypt(socialUser.getAccessToken()), decrypt(socialUser.getSecret()), decrypt(socialUser.getRefreshToken()), expireTime(socialUser.getExpireTime())); } private String decrypt(String encryptedText) { return encryptedText != null ? textEncryptor.decrypt(encryptedText) : encryptedText; } private Long expireTime(Long expireTime) { return expireTime == null || expireTime == 0 ? null : expireTime; } } private String getProviderId(Class apiType) { return connectionFactoryLocator.getConnectionFactory(apiType).getProviderId(); } private String encrypt(String text) { return text != null ? textEncryptor.encrypt(text) : text; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/social/JpaUsersConnectionRepository.java ================================================ package com.porterhead.rest.user.social; import com.porterhead.rest.user.domain.Role; import com.porterhead.rest.user.domain.SocialUser; import com.porterhead.rest.user.SocialUserRepository; import com.porterhead.rest.user.UserRepository; import com.porterhead.rest.user.UserService; import com.porterhead.rest.user.domain.User; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.social.connect.*; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * User: porter * Date: 15/05/2012 * Time: 15:01 */ public class JpaUsersConnectionRepository implements UsersConnectionRepository { private SocialUserRepository socialUserRepository; private UserService userService; private UserRepository userRepository; private final ConnectionFactoryLocator connectionFactoryLocator; private final TextEncryptor textEncryptor; public JpaUsersConnectionRepository(final SocialUserRepository repository, final UserRepository userRepository, final ConnectionFactoryLocator connectionFactoryLocator, final TextEncryptor textEncryptor) { this.socialUserRepository = repository; this.userRepository = userRepository; this.connectionFactoryLocator = connectionFactoryLocator; this.textEncryptor = textEncryptor; } /** * Find User with the Connection profile (providerId and providerUserId) * If this is the first connection attempt there will be nor User so create one and * persist the Connection information * In reality there will only be one User associated with the Connection * * @param connection * @return List of User Ids (see User.getUuid()) */ public List findUserIdsWithConnection(Connection connection) { List userIds = new ArrayList(); ConnectionKey key = connection.getKey(); List users = socialUserRepository.findByProviderIdAndProviderUserId(key.getProviderId(), key.getProviderUserId()); if (!users.isEmpty()) { for (SocialUser user : users) { userIds.add(user.getUser().getUuid().toString()); } return userIds; } //First time connected so create a User account or find one that is already created with the email address User user = findUserFromSocialProfile(connection); String userId; if(user == null) { userId = userService.createUser(Role.authenticated).getUserId(); } else { userId = user.getUuid().toString(); } //persist the Connection createConnectionRepository(userId).addConnection(connection); userIds.add(userId); return userIds; } public Set findUserIdsConnectedTo(String providerId, Set providerUserIds) { return socialUserRepository.findByProviderIdAndProviderUserId(providerId, providerUserIds); } public ConnectionRepository createConnectionRepository(String userId) { if (userId == null) { throw new IllegalArgumentException("userId cannot be null"); } User user = userRepository.findByUuid(userId); if(user == null) { throw new IllegalArgumentException("User not Found"); } return new JpaConnectionRepository(socialUserRepository, userRepository, user, connectionFactoryLocator, textEncryptor); } private User findUserFromSocialProfile(Connection connection) { User user = null; UserProfile profile = connection.fetchUserProfile(); if(profile != null && StringUtils.hasText(profile.getEmail())) { user = userRepository.findByEmailAddress(profile.getEmail()); } return user; } public UserService getUserService() { return userService; } public void setUserService(UserService userService) { this.userService = userService; } } ================================================ FILE: src/main/java/com/porterhead/rest/user/social/SocialConfig.java ================================================ package com.porterhead.rest.user.social; import com.porterhead.rest.config.ApplicationConfig; import com.porterhead.rest.user.SocialUserRepository; import com.porterhead.rest.user.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.UsersConnectionRepository; import org.springframework.social.connect.support.ConnectionFactoryRegistry; import org.springframework.social.facebook.connect.FacebookConnectionFactory; /** * User: porter * Date: 13/03/2012 * Time: 17:39 */ @Configuration public class SocialConfig { @Autowired ApplicationConfig config; @Autowired SocialUserRepository socialUserRepository; @Autowired UserRepository userRepository; @Autowired TextEncryptor textEncryptor; @Bean public ConnectionFactoryLocator connectionFactoryLocator() { ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry(); registry.addConnectionFactory(new FacebookConnectionFactory( config.getFacebookClientId(), config.getFacebookClientSecret())); return registry; } @Bean public UsersConnectionRepository usersConnectionRepository() { JpaUsersConnectionRepository usersConnectionRepository = new JpaUsersConnectionRepository(socialUserRepository, userRepository, connectionFactoryLocator(), textEncryptor); return usersConnectionRepository; } } ================================================ FILE: src/main/java/com/porterhead/rest/util/DateUtil.java ================================================ package com.porterhead.rest.util; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; import java.util.Date; /** * @author: Iain Porter */ public class DateUtil { private static final DateTimeFormatter ISO8061_FORMATTER = ISODateTimeFormat.dateTimeNoMillis(); public static Date getDateFromIso8061DateString(String dateString) { return ISO8061_FORMATTER.parseDateTime(dateString).toDate(); } public static String getCurrentDateAsIso8061String() { DateTime today = new DateTime(); return ISO8061_FORMATTER.print(today); } public static String getDateDateAsIso8061String(DateTime date) { return ISO8061_FORMATTER.print(date); } } ================================================ FILE: src/main/java/com/porterhead/rest/util/HashUtil.java ================================================ package com.porterhead.rest.util; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.digest.DigestUtils; import java.io.IOException; public class HashUtil { public static void main(String[] args) { if (args.length < 1) { System.out.println("Enter a String to sign"); System.exit(-1); } System.out.println("Signed String: " + signString(args[0])); } /** * From a base 64 representation, returns the corresponding byte[] * * @param data String The base64 representation * @return byte[] * @throws java.io.IOException */ public static byte[] base64ToByte(String data) throws IOException { return Base64.decodeBase64(data); } /** * From a byte[] returns a base 64 representation * * @param data byte[] * @return String * @throws IOException */ public static String byteToBase64(byte[] data) { return new String(Base64.encodeBase64(data)); } private static String signString(String request) { byte[] digest = DigestUtils.sha256(request); return new String(Base64.encodeBase64(digest)); } } ================================================ FILE: src/main/java/com/porterhead/rest/util/StringUtil.java ================================================ package com.porterhead.rest.util; import org.springframework.util.StringUtils; import java.util.regex.Pattern; import static org.springframework.util.Assert.hasText; /** * Author: Iain porter */ public class StringUtil { private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"); public static void minLength(String str, int len) throws IllegalArgumentException { hasText(str); if (str.length() < len) { throw new IllegalArgumentException(); } } public static void maxLength(String str, int len) throws IllegalArgumentException { hasText(str); if (str.length() > len) { throw new IllegalArgumentException(); } } public static void validEmail(String email) throws IllegalArgumentException { minLength(email, 4); maxLength(email, 255); if (!email.contains("@") || StringUtils.containsWhitespace(email)) { throw new IllegalArgumentException(); } } public static boolean isValidUuid(String uuid) { return UUID_PATTERN.matcher(uuid).matches(); } } ================================================ FILE: src/main/resources/META-INF/persistence.xml ================================================ org.hibernate.ejb.HibernatePersistence com.porterhead.rest.user.domain.User com.porterhead.rest.user.domain.SocialUser com.porterhead.rest.user.domain.AuthorizationToken com.porterhead.rest.user.domain.VerificationToken ================================================ FILE: src/main/resources/META-INF/spring/component-scan-context.xml ================================================ ================================================ FILE: src/main/resources/META-INF/spring/data-context.xml ================================================ ================================================ FILE: src/main/resources/META-INF/spring/email-services-context.xml ================================================ ================================================ FILE: src/main/resources/META-INF/spring/email-template-context.xml ================================================ resource.loader=class class.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader false true true ================================================ FILE: src/main/resources/META-INF/spring/root-context.xml ================================================ ================================================ FILE: src/main/resources/META-INF/spring/social-configuration-context.xml ================================================ ================================================ FILE: src/main/resources/META-INF/velocity/LostPasswordEmail.vm ================================================ Sample Rest Application

Reset Password

A request was submitted to reset the password for your account. Click through on the link below to reset your password within the next 24 hours.

Password Reset

Please don't reply to this automatically generated email.

What is this doing in my mailbox?

If you did not intend to reset your password, you do not need to click through on the link in this email. If you did not request a link to reset your password, and are concerned that your account is at risk, then please contact us through customer support at http://XXXXXXXXXX.

Thanks for using the Sample java REST Application!

================================================ FILE: src/main/resources/META-INF/velocity/RegistrationEmail.vm ================================================ Sample Web

Welcome to the java-rest project

Thanks for coming on board to the sample java-rest project.
Your registration is complete when you click here to validate your email address.




Please don't reply to this automatically generated email.

What is this doing in my mailbox?

If you do not wish to sign up for the java-rest project, you do not need to click through on the link above, and we will not contact you again unless you request it. As such, there is no need to unsubscribe.

Thanks for trying the java-rest project!

================================================ FILE: src/main/resources/META-INF/velocity/VerifyEmail.vm ================================================ Verify Your Email

Email Verification

To verify your email address for your XXXXXXXXXXX account click through on the link below within the next 48 hours.

Verify my Email Address

Thanks for using XXXXXXXXX!

Please don't reply to this automatically generated email.

What is this doing in my mailbox?

If you do not wish to sign up for XXXXXXXXX, you do not need to click through on the link above, and we will not contact you again unless you request it. As such, there is no need to unsubscribe.

Thanks for trying XXXXXXXXX!

================================================ FILE: src/main/resources/logback.xml ================================================ logbak: %d{HH:mm:ss.SSS} %logger{36} - %msg%n ================================================ FILE: src/main/resources/properties/app.properties ================================================ #The Facebook app credentials for supporting Facebook login facebook.clientId=133718006790561 facebook.clientSecret=2f489f0978614f958101b3d6b1776990 #encryption keys to use for encrypt/decrypt functions security.encryptPassword=yoursecurepassword!!! security.encryptSalt=45ea2b40ef910eef32 #if true all authorized requests will need to be signed and include the appropriate header fields. #See com.porterheadead.rest.authorization.impl.RequestSigningAuthorizationService security.authorization.requireSignedRequests=true #How long in seconds before authorizationToken expires authorization.timeToLive.inSeconds=2592000 #How long in minutes to allow the request timestamp to differ from the server time session.date.offset.inMinutes=15 #How long in minutes that the email registration token should remain active token.emailRegistration.timeToLive.inMinutes=1440 #How long in minutes that the email verification token should remain active token.emailVerification.timeToLive.inMinutes=1440 #How long in minutes that the lost password token should remain active token.lostPassword.timeToLive.inMinutes=30 #The address that mail sent to users will see as the from email.services.fromAddress=foo@example.com #The address that mail sent to users will see as the replyTo email.services.replyTo=foo@example.com #Email subject text for the various emails sent email.services.emailVerificationSubjectText=[Java-REST sample application] Please verify your email Address email.services.emailRegistrationSubjectText=Welcome to the Java-REST sample application email.services.lostPasswordSubjectText=[Java-REST sample application] Reset Password application.version=1.0.0 ================================================ FILE: src/main/resources/properties/dev-app.properties ================================================ hostNameUrl=http://localhost:8080/java-rest ================================================ FILE: src/main/resources/properties/production-app.properties ================================================ hostNameUrl=http://localhost:8080/java-rest ================================================ FILE: src/main/resources/properties/staging-app.properties ================================================ hostNameUrl=http://localhost:8080/java-rest ================================================ FILE: src/main/resources/schema/indexes.sql ================================================ CREATE UNIQUE INDEX user_uuid_IDX ON rest_user ( uuid); CREATE INDEX user_email_address_IDX on rest_user (email_address); CREATE UNIQUE INDEX verification_token_uuid_IDX ON rest_verification_token (uuid); CREATE UNIQUE INDEX verification_token_token_IDX ON rest_verification_token (token); CREATE UNIQUE INDEX session_token_last_updated_IDX ON rest_session_token (last_updated); CREATE UNIQUE INDEX session_token_token_IDX ON rest_session_token (token); ================================================ FILE: src/main/resources/schema/message_store.sql ================================================ CREATE TABLE INT_MESSAGE ( MESSAGE_ID CHAR(36) NOT NULL PRIMARY KEY, REGION VARCHAR(100), CREATED_DATE DATETIME NOT NULL, MESSAGE_BYTES BLOB ) ENGINE=InnoDB; CREATE INDEX INT_MESSAGE_IX1 ON INT_MESSAGE (CREATED_DATE); CREATE TABLE INT_GROUP_TO_MESSAGE ( GROUP_KEY CHAR(36) NOT NULL, MESSAGE_ID CHAR(36) NOT NULL, constraint MESSAGE_GROUP_PK primary key (GROUP_KEY, MESSAGE_ID) ) ENGINE=InnoDB; CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL PRIMARY KEY, REGION VARCHAR(100), MARKED BIGINT, COMPLETE BIGINT, LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE DATETIME NOT NULL, UPDATED_DATE DATETIME DEFAULT NULL ) ENGINE=InnoDB; alter table INT_MESSAGE modify MESSAGE_BYTES MEDIUMBLOB; ================================================ FILE: src/main/resources/schema/truncate_data.sql ================================================ -- truncate all data TRUNCATE table social_user; TRUNCATE table rest_session_token; TRUNCATE table rest_verification_token; TRUNCATE table rest_user; ================================================ FILE: src/main/webapp/META-INF/MANIFEST.MF ================================================ Manifest-Version: 1.0 Class-Path: ================================================ FILE: src/main/webapp/WEB-INF/spring/appservlet/servlet-context.xml ================================================ ================================================ FILE: src/main/webapp/WEB-INF/web.xml ================================================ Example Java REST Application contextConfigLocation /WEB-INF/spring/appservlet/servlet-context.xml spring.profiles.default dev org.springframework.web.context.ContextLoaderListener jersey-servlet /* jersey-servlet com.sun.jersey.spi.spring.container.servlet.SpringServlet com.sun.jersey.config.property.packages com.porterhead.rest.resource, com.porterhead.rest.user.resource com.sun.jersey.api.json.POJOMappingFeature true com.sun.jersey.spi.container.ResourceFilters com.porterhead.rest.filter.ResourceFilterFactory com.sun.jersey.config.property.JSPTemplatesBasePath /WEB-INF/jsp com.sun.jersey.config.property.WebPageContentRegex /(.*\.jsp|.*\.css|.*\.png|.*\.js|.*\.gif|.*\.jpg|.*\.pdf|.*\.html|(WEB-INF/jsp)) CharacterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 CharacterEncodingFilter /* index.html ================================================ FILE: src/main/webapp/css/styles.css ================================================ /*---------------Site styles------------*/ body { background-color: #5087A3; color: white; font-family: helvetica; margin: auto; padding: 0; -webkit-font-smoothing: antialiased; } /**** sections *****/ #container { margin: 0 auto; margin-top: 20px; width: 310px; padding: 5px; } #logo{ text-align: center; margin-bottom: 40px; } #footer { margin-top: 20px; text-align: center; font-size: 12px; } .section { background-color: #D8E0E0; border-radius: 4px; width: 290px; padding: 10px; color: #333333; margin-bottom: 40px; } .section a { color: #DAFFFF; } /****Components ****/ a{ color: #fff; cursor: pointer; text-decoration: none; } a:hover{ color: #fff; text-decoration: underline; } a img { border: 0; } input[type="password"], input[type="text"], input[type="email"] { width: 100%; border-radius: 0px; -webkit-appearance: none; height: 45px; padding: 0px; text-indent: 10px; box-shadow: inset 0px 1px 3px rgba(0,0,0,.5); border: none; margin-bottom: 15px; font-size: 14px; } button { font-size: 16px; background-color: #E0F0FF; border: none; box-shadow: 0px 1px 3px rgba(0,0,0,.5); height: 45px; width: 100%; margin-bottom: 15px; color: #fff; font-weight: bold; cursor: pointer; border-radius: 3px; background: -webkit-gradient(linear, left top, left bottom, from(#91c814), to(#74a020)); background: -moz-linear-gradient(top, #91c814, #74a020); } button:active{ box-shadow: inset 0px 1px 3px rgba(0,0,0,.5); } button:hover{ background: -webkit-gradient(linear, left top, left bottom, from(#96d319), to(#79a525)); background: -moz-linear-gradient(top, #96d319, #79a525); } .small_button { width: 38%; } .medium_button { width: 50%; } .success, .error { font-size: 1.250em; font-weight: bold; text-align: center; } #error_message { background-color: #f22931; border-radius: 0px; padding: 10px; color: #ffffff; display: none; margin-bottom: 15px; box-shadow: 0px 1px 3px rgba(0,0,0,.5); } #success_message { background-color: #77b300; border-radius: 0px; padding: 10px; color: #ffffff; display: none; margin-bottom: 15px; box-shadow: 0px 1px 3px rgba(0,0,0,.5); } dd { font-size: 12px; } .margin_below { margin-bottom:15px; } /*---------------Nav-----------*/ #nav { margin-bottom: 40px; } .unselected_tab:hover{ background: -webkit-gradient(linear, left top, left bottom, from(#96d319), to(#79a525)); background: -moz-linear-gradient(top, #96d319, #79a525); } .selected_tab{ background-color: #271036; color: #fff; font-size: 16px; box-shadow: inset 0px 1px 3px rgba(0,0,0,.5); } .unselected_tab:active{ box-shadow: inset 0px 1px 3px rgba(0,0,0,.5); } .unselected_tab{ color: #fff; background-color: #77b300; font-size: 16px; background: -webkit-gradient(linear, left top, left bottom, from(#91c814), to(#74a020)); background: -moz-linear-gradient(top, #91c814, #74a020); box-shadow: 0px 1px 3px rgba(0,0,0,.5); cursor: pointer; } #account_tab{ width: 33.3%; height: 45px; line-height: 45px; text-align: center; border-radius: 0px 3px 3px 0px; display:inline-block; font-weight: bold; } a#account_tab:hover{ text-decoration: none; } /*---------------Index Page------------*/ .fb { background-color: #336299; height: 45px; padding: 0px; width: 100%; margin-bottom: 40px; box-shadow: 0px 1px 3px rgba(0,0,0,.5); line-height: 45px; font-weight: bold; font-size: 1.000em; text-align: center; background-image: url("../img/fbline.png"); background-repeat: no-repeat; background-position: 4px center; cursor: pointer; border-radius: 3px; z-index: 30; } .fb:active{ box-shadow: inset 0px 1px 3px rgba(0,0,0,.5); } .fb:hover{ background-color: #3969a3; } #or{ color: #77b300; font-size: 36px; text-align: center; margin-bottom: 40px; border-bottom: 1px solid #77b300; line-height:0.1em; width: 100%; } #or span{ padding:0 10px; background-color: #5087A3; } .divider { color: #77b300; margin-top: 40px; margin-bottom: 20px; border-bottom: 1px solid #77b300; width: 100%; } .right_link{ font-weight: bold; line-height: 45px; padding-left: 55px; } .padding{ padding: 0px 20px; } #forgot{ font-size: .750em text-decoration: underline; } #terms_label, #preferences_label { display: inline-block; padding-left: 10px; } #name { color: #333333; } .profilePhoto { box-shadow: 0px 1px 4px rgba(50,50,50,.8); vertical-align: middle; } ================================================ FILE: src/main/webapp/dashboard.html ================================================ Java Rest sample application
Welcome to the Java Rest Sample Application


================================================ FILE: src/main/webapp/forgot_password.html ================================================ Java REST Sample Application
================================================ FILE: src/main/webapp/index.html ================================================ Sample Java Rest Application
Login with Facebook
Or
Sign Up
Forgot Password
================================================ FILE: src/main/webapp/js/bootstrap.js ================================================ /* =================================================== * bootstrap-transition.js v2.1.1 * http://twitter.github.com/bootstrap/javascript.html#transitions * =================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================== */ !function ($) { $(function () { "use strict"; // jshint ;_; /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) * ======================================================= */ $.support.transition = (function () { var transitionEnd = (function () { var el = document.createElement('bootstrap') , transEndEventNames = { 'WebkitTransition' : 'webkitTransitionEnd' , 'MozTransition' : 'transitionend' , 'OTransition' : 'oTransitionEnd otransitionend' , 'transition' : 'transitionend' } , name for (name in transEndEventNames){ if (el.style[name] !== undefined) { return transEndEventNames[name] } } }()) return transitionEnd && { end: transitionEnd } })() }) }(window.jQuery);/* ========================================================== * bootstrap-alert.js v2.1.1 * http://twitter.github.com/bootstrap/javascript.html#alerts * ========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================== */ !function ($) { "use strict"; // jshint ;_; /* ALERT CLASS DEFINITION * ====================== */ var dismiss = '[data-dismiss="alert"]' , Alert = function (el) { $(el).on('click', dismiss, this.close) } Alert.prototype.close = function (e) { var $this = $(this) , selector = $this.attr('data-target') , $parent if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 } $parent = $(selector) e && e.preventDefault() $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) $parent.trigger(e = $.Event('close')) if (e.isDefaultPrevented()) return $parent.removeClass('in') function removeElement() { $parent .trigger('closed') .remove() } $.support.transition && $parent.hasClass('fade') ? $parent.on($.support.transition.end, removeElement) : removeElement() } /* ALERT PLUGIN DEFINITION * ======================= */ $.fn.alert = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('alert') if (!data) $this.data('alert', (data = new Alert(this))) if (typeof option == 'string') data[option].call($this) }) } $.fn.alert.Constructor = Alert /* ALERT DATA-API * ============== */ $(function () { $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) }) }(window.jQuery);/* ============================================================ * bootstrap-button.js v2.1.1 * http://twitter.github.com/bootstrap/javascript.html#buttons * ============================================================ * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================ */ !function ($) { "use strict"; // jshint ;_; /* BUTTON PUBLIC CLASS DEFINITION * ============================== */ var Button = function (element, options) { this.$element = $(element) this.options = $.extend({}, $.fn.button.defaults, options) } Button.prototype.setState = function (state) { var d = 'disabled' , $el = this.$element , data = $el.data() , val = $el.is('input') ? 'val' : 'html' state = state + 'Text' data.resetText || $el.data('resetText', $el[val]()) $el[val](data[state] || this.options[state]) // push to event loop to allow forms to submit setTimeout(function () { state == 'loadingText' ? $el.addClass(d).attr(d, d) : $el.removeClass(d).removeAttr(d) }, 0) } Button.prototype.toggle = function () { var $parent = this.$element.closest('[data-toggle="buttons-radio"]') $parent && $parent .find('.active') .removeClass('active') this.$element.toggleClass('active') } /* BUTTON PLUGIN DEFINITION * ======================== */ $.fn.button = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('button') , options = typeof option == 'object' && option if (!data) $this.data('button', (data = new Button(this, options))) if (option == 'toggle') data.toggle() else if (option) data.setState(option) }) } $.fn.button.defaults = { loadingText: 'loading...' } $.fn.button.Constructor = Button /* BUTTON DATA-API * =============== */ $(function () { $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { var $btn = $(e.target) if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') $btn.button('toggle') }) }) }(window.jQuery);/* ========================================================== * bootstrap-carousel.js v2.1.1 * http://twitter.github.com/bootstrap/javascript.html#carousel * ========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================== */ !function ($) { "use strict"; // jshint ;_; /* CAROUSEL CLASS DEFINITION * ========================= */ var Carousel = function (element, options) { this.$element = $(element) this.options = options this.options.slide && this.slide(this.options.slide) this.options.pause == 'hover' && this.$element .on('mouseenter', $.proxy(this.pause, this)) .on('mouseleave', $.proxy(this.cycle, this)) } Carousel.prototype = { cycle: function (e) { if (!e) this.paused = false this.options.interval && !this.paused && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) return this } , to: function (pos) { var $active = this.$element.find('.item.active') , children = $active.parent().children() , activePos = children.index($active) , that = this if (pos > (children.length - 1) || pos < 0) return if (this.sliding) { return this.$element.one('slid', function () { that.to(pos) }) } if (activePos == pos) { return this.pause().cycle() } return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) } , pause: function (e) { if (!e) this.paused = true if (this.$element.find('.next, .prev').length && $.support.transition.end) { this.$element.trigger($.support.transition.end) this.cycle() } clearInterval(this.interval) this.interval = null return this } , next: function () { if (this.sliding) return return this.slide('next') } , prev: function () { if (this.sliding) return return this.slide('prev') } , slide: function (type, next) { var $active = this.$element.find('.item.active') , $next = next || $active[type]() , isCycling = this.interval , direction = type == 'next' ? 'left' : 'right' , fallback = type == 'next' ? 'first' : 'last' , that = this , e = $.Event('slide', { relatedTarget: $next[0] }) this.sliding = true isCycling && this.pause() $next = $next.length ? $next : this.$element.find('.item')[fallback]() if ($next.hasClass('active')) return if ($.support.transition && this.$element.hasClass('slide')) { this.$element.trigger(e) if (e.isDefaultPrevented()) return $next.addClass(type) $next[0].offsetWidth // force reflow $active.addClass(direction) $next.addClass(direction) this.$element.one($.support.transition.end, function () { $next.removeClass([type, direction].join(' ')).addClass('active') $active.removeClass(['active', direction].join(' ')) that.sliding = false setTimeout(function () { that.$element.trigger('slid') }, 0) }) } else { this.$element.trigger(e) if (e.isDefaultPrevented()) return $active.removeClass('active') $next.addClass('active') this.sliding = false this.$element.trigger('slid') } isCycling && this.cycle() return this } } /* CAROUSEL PLUGIN DEFINITION * ========================== */ $.fn.carousel = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('carousel') , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) , action = typeof option == 'string' ? option : options.slide if (!data) $this.data('carousel', (data = new Carousel(this, options))) if (typeof option == 'number') data.to(option) else if (action) data[action]() else if (options.interval) data.cycle() }) } $.fn.carousel.defaults = { interval: 5000 , pause: 'hover' } $.fn.carousel.Constructor = Carousel /* CAROUSEL DATA-API * ================= */ $(function () { $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { var $this = $(this), href , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) $target.carousel(options) e.preventDefault() }) }) }(window.jQuery);/* ============================================================= * bootstrap-collapse.js v2.1.1 * http://twitter.github.com/bootstrap/javascript.html#collapse * ============================================================= * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================ */ !function ($) { "use strict"; // jshint ;_; /* COLLAPSE PUBLIC CLASS DEFINITION * ================================ */ var Collapse = function (element, options) { this.$element = $(element) this.options = $.extend({}, $.fn.collapse.defaults, options) if (this.options.parent) { this.$parent = $(this.options.parent) } this.options.toggle && this.toggle() } Collapse.prototype = { constructor: Collapse , dimension: function () { var hasWidth = this.$element.hasClass('width') return hasWidth ? 'width' : 'height' } , show: function () { var dimension , scroll , actives , hasData if (this.transitioning) return dimension = this.dimension() scroll = $.camelCase(['scroll', dimension].join('-')) actives = this.$parent && this.$parent.find('> .accordion-group > .in') if (actives && actives.length) { hasData = actives.data('collapse') if (hasData && hasData.transitioning) return actives.collapse('hide') hasData || actives.data('collapse', null) } this.$element[dimension](0) this.transition('addClass', $.Event('show'), 'shown') $.support.transition && this.$element[dimension](this.$element[0][scroll]) } , hide: function () { var dimension if (this.transitioning) return dimension = this.dimension() this.reset(this.$element[dimension]()) this.transition('removeClass', $.Event('hide'), 'hidden') this.$element[dimension](0) } , reset: function (size) { var dimension = this.dimension() this.$element .removeClass('collapse') [dimension](size || 'auto') [0].offsetWidth this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') return this } , transition: function (method, startEvent, completeEvent) { var that = this , complete = function () { if (startEvent.type == 'show') that.reset() that.transitioning = 0 that.$element.trigger(completeEvent) } this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return this.transitioning = 1 this.$element[method]('in') $.support.transition && this.$element.hasClass('collapse') ? this.$element.one($.support.transition.end, complete) : complete() } , toggle: function () { this[this.$element.hasClass('in') ? 'hide' : 'show']() } } /* COLLAPSIBLE PLUGIN DEFINITION * ============================== */ $.fn.collapse = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('collapse') , options = typeof option == 'object' && option if (!data) $this.data('collapse', (data = new Collapse(this, options))) if (typeof option == 'string') data[option]() }) } $.fn.collapse.defaults = { toggle: true } $.fn.collapse.Constructor = Collapse /* COLLAPSIBLE DATA-API * ==================== */ $(function () { $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { var $this = $(this), href , target = $this.attr('data-target') || e.preventDefault() || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 , option = $(target).data('collapse') ? 'toggle' : $this.data() $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') $(target).collapse(option) }) }) }(window.jQuery);/* ============================================================ * bootstrap-dropdown.js v2.1.1 * http://twitter.github.com/bootstrap/javascript.html#dropdowns * ============================================================ * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================ */ !function ($) { "use strict"; // jshint ;_; /* DROPDOWN CLASS DEFINITION * ========================= */ var toggle = '[data-toggle=dropdown]' , Dropdown = function (element) { var $el = $(element).on('click.dropdown.data-api', this.toggle) $('html').on('click.dropdown.data-api', function () { $el.parent().removeClass('open') }) } Dropdown.prototype = { constructor: Dropdown , toggle: function (e) { var $this = $(this) , $parent , isActive if ($this.is('.disabled, :disabled')) return $parent = getParent($this) isActive = $parent.hasClass('open') clearMenus() if (!isActive) { $parent.toggleClass('open') $this.focus() } return false } , keydown: function (e) { var $this , $items , $active , $parent , isActive , index if (!/(38|40|27)/.test(e.keyCode)) return $this = $(this) e.preventDefault() e.stopPropagation() if ($this.is('.disabled, :disabled')) return $parent = getParent($this) isActive = $parent.hasClass('open') if (!isActive || (isActive && e.keyCode == 27)) return $this.click() $items = $('[role=menu] li:not(.divider) a', $parent) if (!$items.length) return index = $items.index($items.filter(':focus')) if (e.keyCode == 38 && index > 0) index-- // up if (e.keyCode == 40 && index < $items.length - 1) index++ // down if (!~index) index = 0 $items .eq(index) .focus() } } function clearMenus() { getParent($(toggle)) .removeClass('open') } function getParent($this) { var selector = $this.attr('data-target') , $parent if (!selector) { selector = $this.attr('href') selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 } $parent = $(selector) $parent.length || ($parent = $this.parent()) return $parent } /* DROPDOWN PLUGIN DEFINITION * ========================== */ $.fn.dropdown = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('dropdown') if (!data) $this.data('dropdown', (data = new Dropdown(this))) if (typeof option == 'string') data[option].call($this) }) } $.fn.dropdown.Constructor = Dropdown /* APPLY TO STANDARD DROPDOWN ELEMENTS * =================================== */ $(function () { $('html') .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) $('body') .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) }) }(window.jQuery);/* ========================================================= * bootstrap-modal.js v2.1.1 * http://twitter.github.com/bootstrap/javascript.html#modals * ========================================================= * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================= */ !function ($) { "use strict"; // jshint ;_; /* MODAL CLASS DEFINITION * ====================== */ var Modal = function (element, options) { this.options = options this.$element = $(element) .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) this.options.remote && this.$element.find('.modal-body').load(this.options.remote) } Modal.prototype = { constructor: Modal , toggle: function () { return this[!this.isShown ? 'show' : 'hide']() } , show: function () { var that = this , e = $.Event('show') this.$element.trigger(e) if (this.isShown || e.isDefaultPrevented()) return $('body').addClass('modal-open') this.isShown = true this.escape() this.backdrop(function () { var transition = $.support.transition && that.$element.hasClass('fade') if (!that.$element.parent().length) { that.$element.appendTo(document.body) //don't move modals dom position } that.$element .show() if (transition) { that.$element[0].offsetWidth // force reflow } that.$element .addClass('in') .attr('aria-hidden', false) .focus() that.enforceFocus() transition ? that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : that.$element.trigger('shown') }) } , hide: function (e) { e && e.preventDefault() var that = this e = $.Event('hide') this.$element.trigger(e) if (!this.isShown || e.isDefaultPrevented()) return this.isShown = false $('body').removeClass('modal-open') this.escape() $(document).off('focusin.modal') this.$element .removeClass('in') .attr('aria-hidden', true) $.support.transition && this.$element.hasClass('fade') ? this.hideWithTransition() : this.hideModal() } , enforceFocus: function () { var that = this $(document).on('focusin.modal', function (e) { if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { that.$element.focus() } }) } , escape: function () { var that = this if (this.isShown && this.options.keyboard) { this.$element.on('keyup.dismiss.modal', function ( e ) { e.which == 27 && that.hide() }) } else if (!this.isShown) { this.$element.off('keyup.dismiss.modal') } } , hideWithTransition: function () { var that = this , timeout = setTimeout(function () { that.$element.off($.support.transition.end) that.hideModal() }, 500) this.$element.one($.support.transition.end, function () { clearTimeout(timeout) that.hideModal() }) } , hideModal: function (that) { this.$element .hide() .trigger('hidden') this.backdrop() } , removeBackdrop: function () { this.$backdrop.remove() this.$backdrop = null } , backdrop: function (callback) { var that = this , animate = this.$element.hasClass('fade') ? 'fade' : '' if (this.isShown && this.options.backdrop) { var doAnimate = $.support.transition && animate this.$backdrop = $('