commit 123658753e048b64b0ab9aa70f46e2fc97ae844e Author: Greg Burd Date: Tue Apr 11 13:14:16 2017 -0400 Initial import of functional, work in progress, example code. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64b1d59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +*.iws +*.ipr +*.iml +*.log +.DS_Store +*.swp +*.orig +*.xmt +*.jou +*.pyc +*.prefs +*~ +#*# +gradle +.gradle +run-jetty-run/ +license.err +test/client/test-results.xml +.idea/ +node_modules/ +bower_components/ +/projectFilesBackup/* +logs/* +!.gitkeep +coverage/* +/ios +xcuserdata/ +/buildSrc/build/ +services/host.properties +erl_crash.dump +.build_config +/stage/ +.tmp/ +jspm_packages/ +typings/ +/cppGraphics/ +.vscode/ +.metadata/ +RemoteSystemsTempFiles/ +*.mo +.project +.classpath +.settings +project/eclipse/build/ +.eslintcache +.buckconfig +.buckconfig.local +.okbuck +buckw +buck-out +build +out +infer-out +.buckd +.watchmanconfig +*.class +/bin/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7411bcc --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ + +gradlew: + gradle gradlew + +lock: + ./gradlew clean generateLock saveLock + +run: + ./gradlew bootRun + +all: + ./gradlew build + +clean: + gradle clean + +infer: + infer -- ./gradlew build diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..d5e8c93 --- /dev/null +++ b/README.txt @@ -0,0 +1,114 @@ +First, download the latest release of Cockroach Database from http://cockroachlabs.com/ for your platform. I rename the +executable to 'crdb' to keep things crisp. Then start the server on your developer host, I run it in /tmp just because. + +$ cd /tmp +$ crdb start --background --http-port=9090 +CockroachDB node starting at 2017-03-16 15:04:25.280278938 -0400 EDT +build: CCL beta-20170309 @ 2017/03/09 16:34:35 (go1.8) + admin: http://localhost:9090 + sql: postgresql://root@localhost:26257?sslmode=disable + logs: cockroach-data/logs + store[0]: path=cockroach-data + status: restarted pre-existing node + clusterID: 59fcea0b-e206-44e2-9071-f8164a52bb6b + nodeID: 1 + +First: +$ gradle gradlew + +To change jvm arguments: +$ ./gradlew bootRun -PjvmArgs="-Dwhatever1=value1 -Dwhatever2=value2" + +To rebuild the lock file: +$ ./gradlew clean generateLock saveLock + +Otherwise just: +$ ./gradlew bootRun + +Basic tests are in a file 'test-api', first install HTTPie and JasonQuery(jq). +$ brew install httpie jq + +Then run the tests: +$ ./test-api + +Of course you can exercise the API with CuRL: + +$ # Create: +$ curl -i -X POST -H "Content-Type:application/json" -d '{ "firstName" : "Karl", "lastName" : "Penzhorn" }' localhost:8443/persons +HTTP/1.1 201 +Location: http://localhost:8443/persons/215677213022060545 +Content-Type: application/hal+json;charset=UTF-8 +Transfer-Encoding: chunked +Date: Tue, 31 Jan 2017 19:11:02 GMT + +{ + "firstName" : "Karl", + "lastName" : "Penzhorn", + "_links" : { + "self" : { + "href" : "http://localhost:8443/persons/215677213022060545" + }, + "person" : { + "href" : "http://localhost:8443/persons/215677213022060545" + } + } +} +$ # Read: +$ curl localhost:8443/persons/215677213022060545 +{ + "firstName" : "Karl", + "lastName" : "Penzhorn", + "_links" : { + "self" : { + "href" : "http://localhost:8443/persons/215677213022060545" + }, + "person" : { + "href" : "http://localhost:8443/persons/215677213022060545" + } + } +} +$ # Update: +$ curl -i -X PUT -H "Content-Type:application/json" -d '{ "firstName" : "Karl", "lastName" : "Zen" }' localhost:8443/persons/215677213022060545 +HTTP/1.1 200 +Location: http://localhost:8443/persons/215677213022060545 +Content-Type: application/hal+json;charset=UTF-8 +Transfer-Encoding: chunked +Date: Tue, 31 Jan 2017 19:18:22 GMT + +{ + "firstName" : "Karl", + "lastName" : "Zen", + "_links" : { + "self" : { + "href" : "http://localhost:8443/persons/215677213022060545" + }, + "person" : { + "href" : "http://localhost:8443/persons/215677213022060545" + } + } +} +$ +$ # Delete: +$ curl -i -X DELETE localhost:8443/persons/215677213022060545 +HTTP/1.1 204 +Date: Tue, 31 Jan 2017 19:14:51 GMT + +$ curl localhost:8443/persons/215677213022060545 +$ + +You can update the log levels at runtime. + +$ curl http://localhost:8443/loggers + +Then, to change the ROOT configured level to TRACE you'd simply: + +$ curl -i -X POST -H 'Content-Type: application/json' -d '{"configuredLevel": "TRACE"}' http://localhost:8080/loggers/ROOT + +or: + +$ HTTP POST localhost:8080/loggers/ROOT configuredLevel:TRACE + + +Also, you can get the set of REST endpoints mapped (not including model objects) via: + +curl http://localhost:8443/mappings | jq diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9a74300 --- /dev/null +++ b/build.gradle @@ -0,0 +1,215 @@ +// gradle wrapper +// ./gradlew dependencies --configuration compile +// ./gradlew clean generateLock saveLock +// ./gradlew compileJava +// ./gradlew run +// ./gradlew run --debug-jvm + +buildscript { + ext { } + repositories { + jcenter() + mavenLocal() + mavenCentral() + maven { url "https://clojars.org/repo" } + maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://repo.spring.io/milestone" } //maven { url "https://repo.spring.io/snapshot" } + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE") + classpath 'com.netflix.nebula:gradle-dependency-lock-plugin:4.+' +// classpath 'se.transmode.gradle:gradle-docker:1.2' + classpath 'com.uber:okbuck:0.19.0' + } +} + +apply plugin: 'java' +apply plugin: 'idea' +apply plugin: 'eclipse' +apply plugin: 'application' +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" +apply plugin: 'com.uber.okbuck' +apply plugin: 'nebula.dependency-lock' +//TODO(gburd): apply plugin: 'org.flywaydb.flyway', version "4.1.2" +//TODO(gburd): apply plugin: 'docker' + +task wrapper(type: Wrapper) { + gradleVersion = '3.4.1' +} + +applicationDefaultJvmArgs = ["-Dgreeting.language=en"] + +jar { + baseName = 'crud' + version = '0.0.2-SNAPSHOT' +} + +bootRun { + if (project.hasProperty('jvmArgs')) { + jvmArgs = (project.jvmArgs.split("\\s+") as List) + } else { + jvmArgs = ["-server", + "-Xms1g", + "-Xmx1g", + "-XX:NewRatio=3", + "-Xss16m", + "-XX:+UseConcMarkSweepGC", + "-XX:+CMSParallelRemarkEnabled", + "-XX:ConcGCThreads=4", + "-XX:ReservedCodeCacheSize=240m", + "-XX:+AlwaysPreTouch", + "-XX:+TieredCompilation", + "-XX:+UseCompressedOops", + "-XX:SoftRefLRUPolicyMSPerMB=50", + "-Dsun.io.useCanonCaches=false", + "-Djava.net.preferIPv4Stack=true", + "-Djsse.enableSNIExtension=false", + "-ea", + "-XX:+UseAdaptiveGCBoundary", + "-XX:CompileThreshold=10000", + "-XX:+OptimizeStringConcat", + "-XX:+UseFastAccessorMethods", + "-XX:+HeapDumpOnOutOfMemoryError", + "-XX:-OmitStackTraceInFastThrow", + "-XX:MaxJavaStackTraceDepth=-1", + "-XX:+UseCompressedOops"] + } +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + jcenter() + mavenLocal() + mavenCentral() + maven { url "https://clojars.org/repo" } + maven { url "https://repo.spring.io/milestone" } //maven { url "https://repo.spring.io/snapshot" } +} + +configurations { + querydslapt + compile.exclude module: 'spring-boot-starter-tomcat' + compile.exclude module: 'tomcat-jdbc' +} + +configurations.all { + exclude group: 'org.hibernate', module: 'hibernate-entitymanager' + exclude group: 'org.hibernate', module: 'hibernate-core' + exclude group: 'org.slf4j', module: 'slf4j-log4j12' + exclude group: 'log4j', module: 'log4j' +} + +dependencies { + // Some Java Extras + compile group: 'javax.inject', name: 'javax.inject', version: '1' + + // Apache Commons + compile group: 'commons-codec', name: 'commons-codec', version: '1.+' + compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.+' + + // Google Guava, "For all the Goodness(TM)" + compile group: 'com.google.guava', name: 'guava', version: '21.+' + + // Joda Time, "Because time is hard(TM)" + compile group: 'joda-time', name: 'joda-time', version: '2.+' + + // Lombok, "Where less is more(TM)" + compile group: 'org.projectlombok', name: 'lombok' + + // AOP + compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.+' + + // All the Spring, for all the things (e.g. a REST web application with a SQL database) + compile group: 'org.springframework', name: 'spring-core' + compile group: 'org.springframework', name: 'spring-beans' + compile group: 'org.springframework', name: 'spring-context' + compile group: 'org.springframework', name: 'spring-tx' + compile group: 'org.springframework.data', name: 'spring-data-jpa' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-undertow' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-rest' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-cache' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-aop' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-web' + compile group: 'com.querydsl', name: 'querydsl-jpa', version: '4.+' + querydslapt group: 'com.mysema.querydsl', name: 'querydsl-apt', version: '3.+' // QueryDsl Weaving + + // Entity Storage (aka. txn{CRUD}), "Things necessary for data persistence" + // - CockroachDB speaks the PostgreSQL wire protocol + // - EclipseLink (ORM) + // - UUID/Primary Keys (Fast, coordination-free, decentralized, k-ordered unique ID generator) + // - Flake ID generation + // - Flyway, "Change happens(TM)" + compile group: 'org.postgresql', name: 'postgresql' + compile group: 'org.eclipse.persistence', name: 'eclipselink', version: '2.+' + compile group: 'org.eclipse.persistence', name: 'javax.persistence', version: '2.+' + compile group: 'org.eclipse.persistence', name: 'org.eclipse.persistence.jpa', version: '2.+' + compile group: 'com.github.rholder.fauxflake', name: 'fauxflake-core', version: '1.+' +// compile group: 'org.flywaydb', name: 'flyway-core' + + // Dropwizard (aka. CodaHale) Metrics, "Measure all the things!(TM)" + compile group: 'io.dropwizard.metrics', name: 'metrics-core', version: '3.+' + compile group: 'io.dropwizard.metrics', name: 'metrics-jvm', version: '3.+' + compile group: 'io.dropwizard.metrics', name: 'metrics-healthchecks', version: '3.+' + compile group: 'io.dropwizard.metrics', name: 'metrics-servlets', version: '3.+' + compile group: 'io.dropwizard.metrics', name: 'metrics-jetty9', version: '3.+' + compile group: 'io.dropwizard.metrics', name: 'metrics-graphite', version: '3.+' + compile group: 'com.ryantenney.metrics', name: 'metrics-spring', version: '3.+' + compile group: 'io.riemann', name: 'metrics3-riemann-reporter', version: '0.+' + compile group: 'defunkt', name: 'logback-riemann-appender', version: '0.+' + + // Logging for Java, "Visibility proceeds insight(TM)" + compile group: 'ch.qos.logback', name: 'logback-core', version: '1.+' + compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.+' + compile group: 'org.zalando', name: 'logbook-servlet', version: '1.+' + compile group: 'org.zalando', name: 'logbook-httpclient', version: '1.+' + compile group: 'org.zalando', name: 'logbook-spring-boot-starter', version: '1.+' + compile group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.+' + compile group: 'org.slf4j', name: 'log4j-over-slf4j', version: '1.+' + + // Testing + testCompile group: 'junit', name: 'junit', version: '4.+' + testCompile group: 'org.mockito', name: 'mockito-all', version: '1.+' + testCompile group: 'org.assertj', name: 'assertj-core', version: '2.+' + testCompile group: 'com.github.stefanbirkner', name: 'system-rules', version: '1.+' + testCompile group: 'nl.jqno.equalsverifier', name: 'equalsverifier', version: '1.+' + testCompile group: 'org.hamcrest', name: 'hamcrest-core', version: '1.3' + testCompile group: 'org.quicktheories', name: 'quicktheories', version: '0.+' + testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test' + + testCompile group: 'com.github.javafaker', name: 'javafaker', version: '0.+' +} + +dependencyManagement { + imports { mavenBom 'org.springframework.data:spring-data-releasetrain:Ingalls-SR1' } + dependencies { + dependency group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.11.+' + } +} + +// flyway { +// url = 'jdbc:postgresql://127.0.0.1:26257/crud?sslmode=disable' +// user = 'root' +// } + +task performJPAWeaving(type: JavaExec, dependsOn: "compileJava"){ + inputs.dir compileJava.destinationDir + outputs.dir compileJava.destinationDir + main "org.eclipse.persistence.tools.weaving.jpa.StaticWeave" + args "-persistenceinfo", + "src/main/resources", + compileJava.destinationDir.getAbsolutePath(), + compileJava.destinationDir.getAbsolutePath() + classpath = configurations.compile +} + +tasks.withType(JavaCompile){ + doLast{ + tasks.performJPAWeaving.execute() + } +} +// http://stackoverflow.com/questions/6431026/generating-jpa2-metamodel-from-a-gradle-build-script +// http://bsideup.blogspot.com/2015/04/querydsl-with-gradle-and-idea.html diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..08a73fe --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,1516 @@ +{ + "compile": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + }, + "compileClasspath": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + }, + "default": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + }, + "querydslapt": { + "com.mysema.querydsl:querydsl-apt": { + "locked": "3.7.4", + "requested": "3.+" + } + }, + "runtime": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + }, + "runtimeClasspath": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + }, + "testCompile": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.javafaker:javafaker": { + "locked": "0.13", + "requested": "0.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.github.stefanbirkner:system-rules": { + "locked": "1.16.1", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "junit:junit": { + "locked": "4.12", + "requested": "4.+" + }, + "nl.jqno.equalsverifier:equalsverifier": { + "locked": "1.7.8", + "requested": "1.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.assertj:assertj-core": { + "locked": "2.6.0", + "requested": "2.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.hamcrest:hamcrest-core": { + "locked": "1.3", + "requested": "1.3" + }, + "org.mockito:mockito-all": { + "locked": "1.10.19", + "requested": "1.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.quicktheories:quicktheories": { + "locked": "0.13", + "requested": "0.+" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-test": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + }, + "testCompileClasspath": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.javafaker:javafaker": { + "locked": "0.13", + "requested": "0.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.github.stefanbirkner:system-rules": { + "locked": "1.16.1", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "junit:junit": { + "locked": "4.12", + "requested": "4.+" + }, + "nl.jqno.equalsverifier:equalsverifier": { + "locked": "1.7.8", + "requested": "1.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.assertj:assertj-core": { + "locked": "2.6.0", + "requested": "2.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.hamcrest:hamcrest-core": { + "locked": "1.3", + "requested": "1.3" + }, + "org.mockito:mockito-all": { + "locked": "1.10.19", + "requested": "1.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.quicktheories:quicktheories": { + "locked": "0.13", + "requested": "0.+" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-test": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + }, + "testRuntime": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.javafaker:javafaker": { + "locked": "0.13", + "requested": "0.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.github.stefanbirkner:system-rules": { + "locked": "1.16.1", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "junit:junit": { + "locked": "4.12", + "requested": "4.+" + }, + "nl.jqno.equalsverifier:equalsverifier": { + "locked": "1.7.8", + "requested": "1.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.assertj:assertj-core": { + "locked": "2.6.0", + "requested": "2.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.hamcrest:hamcrest-core": { + "locked": "1.3", + "requested": "1.3" + }, + "org.mockito:mockito-all": { + "locked": "1.10.19", + "requested": "1.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.quicktheories:quicktheories": { + "locked": "0.13", + "requested": "0.+" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-test": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + }, + "testRuntimeClasspath": { + "ch.qos.logback:logback-classic": { + "locked": "1.2.3", + "requested": "1.+" + }, + "ch.qos.logback:logback-core": { + "locked": "1.2.3", + "requested": "1.+" + }, + "com.github.javafaker:javafaker": { + "locked": "0.13", + "requested": "0.+" + }, + "com.github.rholder.fauxflake:fauxflake-core": { + "locked": "1.1.0", + "requested": "1.+" + }, + "com.github.stefanbirkner:system-rules": { + "locked": "1.16.1", + "requested": "1.+" + }, + "com.google.guava:guava": { + "locked": "21.0", + "requested": "21.+" + }, + "com.querydsl:querydsl-jpa": { + "locked": "4.1.4", + "requested": "4.+" + }, + "com.ryantenney.metrics:metrics-spring": { + "locked": "3.1.3", + "requested": "3.+" + }, + "commons-codec:commons-codec": { + "locked": "1.10", + "requested": "1.+" + }, + "defunkt:logback-riemann-appender": { + "locked": "0.4.0", + "requested": "0.+" + }, + "io.dropwizard.metrics:metrics-core": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-graphite": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-healthchecks": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jetty9": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-jvm": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.dropwizard.metrics:metrics-servlets": { + "locked": "3.2.2", + "requested": "3.+" + }, + "io.riemann:metrics3-riemann-reporter": { + "locked": "0.4.3", + "requested": "0.+" + }, + "javax.inject:javax.inject": { + "locked": "1", + "requested": "1" + }, + "joda-time:joda-time": { + "locked": "2.9.9", + "requested": "2.+" + }, + "junit:junit": { + "locked": "4.12", + "requested": "4.+" + }, + "nl.jqno.equalsverifier:equalsverifier": { + "locked": "1.7.8", + "requested": "1.+" + }, + "org.apache.commons:commons-lang3": { + "locked": "3.5", + "requested": "3.+" + }, + "org.aspectj:aspectjweaver": { + "locked": "1.9.0.BETA-5", + "requested": "1.+" + }, + "org.assertj:assertj-core": { + "locked": "2.6.0", + "requested": "2.+" + }, + "org.eclipse.persistence:eclipselink": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.eclipse.persistence:javax.persistence": { + "locked": "2.1.1", + "requested": "2.+" + }, + "org.eclipse.persistence:org.eclipse.persistence.jpa": { + "locked": "2.6.4", + "requested": "2.+" + }, + "org.hamcrest:hamcrest-core": { + "locked": "1.3", + "requested": "1.3" + }, + "org.mockito:mockito-all": { + "locked": "1.10.19", + "requested": "1.+" + }, + "org.postgresql:postgresql": { + "locked": "9.4.1212.jre7" + }, + "org.projectlombok:lombok": { + "locked": "1.16.14" + }, + "org.quicktheories:quicktheories": { + "locked": "0.13", + "requested": "0.+" + }, + "org.slf4j:jcl-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.slf4j:log4j-over-slf4j": { + "locked": "1.8.0-alpha0", + "requested": "1.+" + }, + "org.springframework.boot:spring-boot-starter-actuator": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-aop": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-cache": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-jpa": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-data-rest": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-test": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-undertow": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.boot:spring-boot-starter-web": { + "locked": "1.5.2.RELEASE" + }, + "org.springframework.data:spring-data-jpa": { + "locked": "1.11.1.RELEASE" + }, + "org.springframework:spring-beans": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-context": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-core": { + "locked": "4.3.7.RELEASE" + }, + "org.springframework:spring-tx": { + "locked": "4.3.7.RELEASE" + }, + "org.zalando:logbook-httpclient": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-servlet": { + "locked": "1.1.1", + "requested": "1.+" + }, + "org.zalando:logbook-spring-boot-starter": { + "locked": "1.1.1", + "requested": "1.+" + } + } +} \ No newline at end of file diff --git a/ops/Makefile b/ops/Makefile new file mode 100644 index 0000000..f9ba452 --- /dev/null +++ b/ops/Makefile @@ -0,0 +1,7 @@ + +up: + docker-compose -f riemann.yml up --force-recreate + +down: + docker-compose -f riemann.yml down + diff --git a/ops/NOTES b/ops/NOTES new file mode 100644 index 0000000..aeb590b --- /dev/null +++ b/ops/NOTES @@ -0,0 +1,6 @@ +$ docker run -d -p 4567:4567 travix/riemann-dash:latest +$ docker run -d -p 5555:5555 -p 5556:5556 -p 16384:16384 travix/riemann-server:latest +$ open http://127.0.0.1:4567/#Riemann + +java -jar riemann-grid-0.6.4-standalone.jar -l 127.0.0.1 -p 9999 -H 127.0.0.1 -P 5555 + diff --git a/ops/logback.xml b/ops/logback.xml new file mode 100644 index 0000000..57519f6 --- /dev/null +++ b/ops/logback.xml @@ -0,0 +1,14 @@ + + + + Example CRUD Service + 127.0.0.1 + 5555 + graphene.local + application:test-service,datacenter:us-sw + WARN + + + + + diff --git a/ops/riemann-grid-0.6.4-standalone.jar b/ops/riemann-grid-0.6.4-standalone.jar new file mode 100644 index 0000000..a55a6c1 Binary files /dev/null and b/ops/riemann-grid-0.6.4-standalone.jar differ diff --git a/ops/riemann.yml b/ops/riemann.yml new file mode 100644 index 0000000..35dc835 --- /dev/null +++ b/ops/riemann.yml @@ -0,0 +1,32 @@ +version: "3" + +networks: + riemann: + driver: overlay + +services: + riemannserver: + container_name: riemann-server + image: "nathanleclaire/riemann-server:article" + network_mode: "riemann" + ports: + - "127.0.0.1:5556:5556" + restart: always + + riemannhealth: + image: "nathanleclaire/riemann-health:article" + network_mode: "riemann" + pid: host + environment: + - "affinity:container!=*riemannhealth*" + volumes: + - "/etc/hostname:/etc/hostname:ro" + restart: always + + riemanndash: + image: "nathanleclaire/riemann-dash:article" + networks: + - riemann + ports: + - "127.0.0.1:4567:4567" + restart: always diff --git a/src/main/java/com/example/crud/Application.java b/src/main/java/com/example/crud/Application.java new file mode 100644 index 0000000..50d0c5f --- /dev/null +++ b/src/main/java/com/example/crud/Application.java @@ -0,0 +1,147 @@ +package com.example.crud; + +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.env.Environment; +import org.springframework.core.env.SimpleCommandLinePropertySource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect; +import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.zalando.logbook.Logbook; + +import javax.inject.Inject; +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.zalando.logbook.Conditions.*; + +@ComponentScan +@EnableCaching +@EnableJpaRepositories("com.example.crud.db.dao") +@EnableAutoConfiguration(exclude = { MetricFilterAutoConfiguration.class, MetricRepositoryAutoConfiguration.class }) +@EnableTransactionManagement +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + @Inject private Environment env; + + + public static void main(String[] args) { + SpringApplication app = new SpringApplication(Application.class); + app.setBannerMode(Banner.Mode.OFF); + SimpleCommandLinePropertySource source = new SimpleCommandLinePropertySource(args); + addDefaultProfile(app, source); + logbookSetup(); + Environment env = app.run(args).getEnvironment(); + } + + private static void logbookSetup() { + Logbook logbook = Logbook.builder() + .condition(exclude( + requestTo("/health"), + requestTo("/admin/**"), + contentType("application/octet-stream"), + header("X-Secret", Sets.newHashSet("1", "true")::contains))) + .build(); + } + + /** + * If no profile has been configured, set by default the "dev" profile. + */ + private static void addDefaultProfile(SpringApplication app, SimpleCommandLinePropertySource source) { + if (!source.containsProperty("spring.profiles.active") && + !System.getenv().containsKey("SPRING_PROFILES_ACTIVE")) { + + app.setAdditionalProfiles(Constants.SPRING_PROFILE_DEVELOPMENT); + } + } + + /** + * EclipseLink JPA setup. + */ + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean ret = new LocalContainerEntityManagerFactoryBean(); + ret.setDataSource(dataSource()); + ret.setJpaVendorAdapter(jpaVendorAdapter()); + ret.setJpaDialect(eclipseLinkJpaDialect()); + ret.setJpaPropertyMap(jpaProperties()); + ret.setPackagesToScan("com.example.crud.db"); + return ret; + } + + + @Bean + public EclipseLinkJpaDialect eclipseLinkJpaDialect() { + return new EclipseLinkJpaDialect(); + } + + /* + * Set this property to disable LoadTimeWeaver (i.e. Dynamic Weaving) for EclipseLink. + * Otherwise, you'll get: Cannot apply class transformer without LoadTimeWeaver specified + */ + @Bean + public Map jpaProperties() { + Map props = new HashMap<>(); + props.put("eclipselink.weaving", "static"); //TODO(gburd): enable + return props; + } + + @Bean + public JpaVendorAdapter jpaVendorAdapter() { + EclipseLinkJpaVendorAdapter jpaVendorAdapter = new EclipseLinkJpaVendorAdapter(); + jpaVendorAdapter.setDatabase(Database.POSTGRESQL); + jpaVendorAdapter.setGenerateDdl(true); + return jpaVendorAdapter; + } + + @Bean + public DataSource dataSource() { + final DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.postgresql.Driver"); + dataSource.setUrl("jdbc:postgresql://127.0.0.1:26257/crud"); + dataSource.setUsername("root"); + dataSource.setPassword(""); + return dataSource; + } + + @Bean + public CacheManager cacheManager() { + //TODO(gburd): is there an eclipselink cache manager? or caffeine? or...? + Cache cache = new ConcurrentMapCache("name"); + + SimpleCacheManager manager = new SimpleCacheManager(); + manager.setCaches(Arrays.asList(cache)); + + return manager; + } + +} diff --git a/src/main/java/com/example/crud/Constants.java b/src/main/java/com/example/crud/Constants.java new file mode 100644 index 0000000..d5f803e --- /dev/null +++ b/src/main/java/com/example/crud/Constants.java @@ -0,0 +1,21 @@ +package com.example.crud; + +/** + * Application constants. + */ +public final class Constants { + + // Spring profile for development, production and "fast", see http://jhipster.github.io/profiles.html + public static final String SPRING_PROFILE_DEVELOPMENT = "dev"; + public static final String SPRING_PROFILE_PRODUCTION = "prod"; + public static final String SPRING_PROFILE_FAST = "fast"; + // Spring profile used when deploying with Spring Cloud (used when deploying to CloudFoundry) + public static final String SPRING_PROFILE_CLOUD = "cloud"; + // Spring profile used when deploying to Heroku + public static final String SPRING_PROFILE_HEROKU = "heroku"; + + public static final String SYSTEM_ACCOUNT = "system"; + + private Constants() { + } +} diff --git a/src/main/java/com/example/crud/TransactionManagersConfig.java b/src/main/java/com/example/crud/TransactionManagersConfig.java new file mode 100644 index 0000000..b0e7d63 --- /dev/null +++ b/src/main/java/com/example/crud/TransactionManagersConfig.java @@ -0,0 +1,27 @@ +package com.example.crud; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +@Configuration +@EnableTransactionManagement +public class TransactionManagersConfig { + @Autowired EntityManagerFactory emf; + @Autowired private DataSource dataSource; + + @Bean(name = "transactionManager") + public PlatformTransactionManager transactionManager() { + JpaTransactionManager tm = + new JpaTransactionManager(); + tm.setEntityManagerFactory(emf); + tm.setDataSource(dataSource); + return tm; + } +} diff --git a/src/main/java/com/example/crud/controllers/EmployeeController.java b/src/main/java/com/example/crud/controllers/EmployeeController.java new file mode 100644 index 0000000..94623bd --- /dev/null +++ b/src/main/java/com/example/crud/controllers/EmployeeController.java @@ -0,0 +1,132 @@ +package com.example.crud.controllers; + +import com.example.crud.db.annotations.Retry; +import com.example.crud.db.models.Employee; +import com.example.crud.services.EmployeeService; +import lombok.val; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static org.springframework.web.bind.annotation.RequestMethod.*; + +@Controller +@Transactional(propagation = Propagation.MANDATORY) +public class EmployeeController { + private static final Logger log = LoggerFactory.getLogger(EmployeeService.class); + + @Autowired private EmployeeService employeeService; + + @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) + @RequestMapping(value = "/api/v1/employees", method = GET, produces = "application/json") + public ResponseEntity> index() { + log.debug("Getting all employees..."); + @SuppressWarnings("unchecked") ResponseEntity response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + try { + List employees = employeeService.findAll(); + if (employees != null) { + response = ResponseEntity.status(HttpStatus.OK).body(employees); + } else { + response = ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } catch (Exception e) { + response = ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + return response; + } + + @Retry(times = 3, on = org.springframework.dao.OptimisticLockingFailureException.class) + @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ) + @RequestMapping(value = "/api/v1/employee", method = POST, consumes = "application/json") + public ResponseEntity addEmployee(@RequestBody Employee employee) { + log.debug("Inserting employee"); + @SuppressWarnings("unchecked") ResponseEntity response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + try { + val e = employeeService.saveEmployee(employee); + if (e != null) { + response = ResponseEntity.status(HttpStatus.CREATED).body(e.getId()); + } else { + response = ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); + } + } catch (Exception e) { + response = ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + return response; + } + + @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE) + @RequestMapping(value = "/api/v1/employee/{id:[\\d]+}", method = DELETE) + public ResponseEntity deleteEmployee(@PathVariable Long id) { + log.debug("Deleting employee"); + @SuppressWarnings("unchecked") ResponseEntity response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + try { + employeeService.delete(id); + response = ResponseEntity.status(HttpStatus.OK).body(id); + } catch (Exception e) { + response = ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + return response; + } + + @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) + //@RequestMapping(value = "/api/v1/employee/{id:[\\d]+}", method = GET) + @RequestMapping(value = "/api/v1/employee/{id}", method = GET) + public ResponseEntity find(@PathVariable Long id) { + log.debug("Getting a specific employee by id {}", id); + @SuppressWarnings("unchecked") ResponseEntity response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + try { + Employee employee = employeeService.getOne(id); + if (employee != null) { + response = ResponseEntity.status(HttpStatus.OK).body(employee); + } else { + response = ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } catch (Exception e) { + response = ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + return response; + } + + @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) + @RequestMapping(value = "/api/v1/employee/named", method = GET) + public ResponseEntity> find(@RequestParam("last") String name) { + log.debug("Getting a specific employee by name: {}", name); + @SuppressWarnings("unchecked") ResponseEntity response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + try { + List employees = employeeService.findByLastName(name); + if (employees != null) { + response = ResponseEntity.status(HttpStatus.OK).body(employees); + } else { + response = ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } catch (Exception e) { + response = ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + return response; + } + + @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) + @RequestMapping(value = "/api/v1/employee/{id:[\\d]+}", method = DELETE) + public @ResponseBody Long delete(@PathVariable Long id) { + log.debug("Remove an employee by id {}", id); + employeeService.delete(id); + return id; + } + + /* + @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) + @RequestMapping("/api/v1/employee/lastNameLength") + public List fetchByLength(Long length) { + return employeeService.fetchByLastNameLength(length); + } + */ +} diff --git a/src/main/java/com/example/crud/crypto/MersenneTwisterFast.java b/src/main/java/com/example/crud/crypto/MersenneTwisterFast.java new file mode 100644 index 0000000..f069f7f --- /dev/null +++ b/src/main/java/com/example/crud/crypto/MersenneTwisterFast.java @@ -0,0 +1,1439 @@ +package com.example.crud.crypto; +import java.io.*; +import java.util.*; + +/** + *

MersenneTwister and MersenneTwisterFast

+ *

Version 22, based on version MT199937(99/10/29) + * of the Mersenne Twister algorithm found at + * + * The Mersenne Twister Home Page, with the initialization + * improved using the new 2002/1/26 initialization algorithm + * By Sean Luke, October 2004. + * + *

MersenneTwister is a drop-in subclass replacement + * for java.util.Random. It is properly synchronized and + * can be used in a multithreaded environment. On modern VMs such + * as HotSpot, it is approximately 1/3 slower than java.util.Random. + * + *

MersenneTwisterFast is not a subclass of java.util.Random. It has + * the same public methods as Random does, however, and it is + * algorithmically identical to MersenneTwister. MersenneTwisterFast + * has hard-code inlined all of its methods directly, and made all of them + * final (well, the ones of consequence anyway). Further, these + * methods are not synchronized, so the same MersenneTwisterFast + * instance cannot be shared by multiple threads. But all this helps + * MersenneTwisterFast achieve well over twice the speed of MersenneTwister. + * java.util.Random is about 1/3 slower than MersenneTwisterFast. + * + *

About the Mersenne Twister

+ *

This is a Java version of the C-program for MT19937: Integer version. + * The MT19937 algorithm was created by Makoto Matsumoto and Takuji Nishimura, + * who ask: "When you use this, send an email to: matumoto@math.keio.ac.jp + * with an appropriate reference to your work". Indicate that this + * is a translation of their algorithm into Java. + * + *

Reference. + * Makato Matsumoto and Takuji Nishimura, + * "Mersenne Twister: A 623-Dimensionally Equidistributed Uniform + * Pseudo-Random Number Generator", + * ACM Transactions on Modeling and. Computer Simulation, + * Vol. 8, No. 1, January 1998, pp 3--30. + * + *

About this Version

+ * + *

Changes since V21: Minor documentation HTML fixes. + * + *

Changes since V20: Added clearGuassian(). Modified stateEquals() + * to be synchronizd on both objects for MersenneTwister, and changed its + * documentation. Added synchronization to both setSeed() methods, to + * writeState(), and to readState() in MersenneTwister. Removed synchronization + * from readObject() in MersenneTwister. + * + *

Changes since V19: nextFloat(boolean, boolean) now returns float, + * not double. + * + *

Changes since V18: Removed old final declarations, which used to + * potentially speed up the code, but no longer. + * + *

Changes since V17: Removed vestigial references to &= 0xffffffff + * which stemmed from the original C code. The C code could not guarantee that + * ints were 32 bit, hence the masks. The vestigial references in the Java + * code were likely optimized out anyway. + * + *

Changes since V16: Added nextDouble(includeZero, includeOne) and + * nextFloat(includeZero, includeOne) to allow for half-open, fully-closed, and + * fully-open intervals. + * + *

Changes Since V15: Added serialVersionUID to quiet compiler warnings + * from Sun's overly verbose compilers as of JDK 1.5. + * + *

Changes Since V14: made strictfp, with StrictMath.log and StrictMath.sqrt + * in nextGaussian instead of Math.log and Math.sqrt. This is largely just to be safe, + * as it presently makes no difference in the speed, correctness, or results of the + * algorithm. + * + *

Changes Since V13: clone() method CloneNotSupportedException removed. + * + *

Changes Since V12: clone() method added. + * + *

Changes Since V11: stateEquals(...) method added. MersenneTwisterFast + * is equal to other MersenneTwisterFasts with identical state; likewise + * MersenneTwister is equal to other MersenneTwister with identical state. + * This isn't equals(...) because that requires a contract of immutability + * to compare by value. + * + *

Changes Since V10: A documentation error suggested that + * setSeed(int[]) required an int[] array 624 long. In fact, the array + * can be any non-zero length. The new version also checks for this fact. + * + *

Changes Since V9: readState(stream) and writeState(stream) + * provided. + * + *

Changes Since V8: setSeed(int) was only using the first 28 bits + * of the seed; it should have been 32 bits. For small-number seeds the + * behavior is identical. + * + *

Changes Since V7: A documentation error in MersenneTwisterFast + * (but not MersenneTwister) stated that nextDouble selects uniformly from + * the full-open interval [0,1]. It does not. nextDouble's contract is + * identical across MersenneTwisterFast, MersenneTwister, and java.util.Random, + * namely, selection in the half-open interval [0,1). That is, 1.0 should + * not be returned. A similar contract exists in nextFloat. + * + *

Changes Since V6: License has changed from LGPL to BSD. + * New timing information to compare against + * java.util.Random. Recent versions of HotSpot have helped Random increase + * in speed to the point where it is faster than MersenneTwister but slower + * than MersenneTwisterFast (which should be the case, as it's a less complex + * algorithm but is synchronized). + * + *

Changes Since V5: New empty constructor made to work the same + * as java.util.Random -- namely, it seeds based on the current time in + * milliseconds. + * + *

Changes Since V4: New initialization algorithms. See + * (see + * http://www.math.keio.ac.jp/matumoto/MT2002/emt19937ar.html) + * + *

The MersenneTwister code is based on standard MT19937 C/C++ + * code by Takuji Nishimura, + * with suggestions from Topher Cooper and Marc Rieffel, July 1997. + * The code was originally translated into Java by Michael Lecuyer, + * January 1999, and the original code is Copyright (c) 1999 by Michael Lecuyer. + * + *

Java notes

+ * + *

This implementation implements the bug fixes made + * in Java 1.2's version of Random, which means it can be used with + * earlier versions of Java. See + * + * the JDK 1.2 java.util.Random documentation for further documentation + * on the random-number generation contracts made. Additionally, there's + * an undocumented bug in the JDK java.util.Random.nextBytes() method, + * which this code fixes. + * + *

Just like java.util.Random, this + * generator accepts a long seed but doesn't use all of it. java.util.Random + * uses 48 bits. The Mersenne Twister instead uses 32 bits (int size). + * So it's best if your seed does not exceed the int range. + * + *

MersenneTwister can be used reliably + * on JDK version 1.1.5 or above. Earlier Java versions have serious bugs in + * java.util.Random; only MersenneTwisterFast (and not MersenneTwister nor + * java.util.Random) should be used with them. + * + *

License

+ * + * Copyright (c) 2003 by Sean Luke.
+ * Portions copyright (c) 1993 by Michael Lecuyer.
+ * All rights reserved.
+ * + *

Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + *

    + *
  • Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + *
  • Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *
  • Neither the name of the copyright owners, their employers, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + *
+ *

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + @version 22 +*/ + + +// Note: this class is hard-inlined in all of its methods. This makes some of +// the methods well-nigh unreadable in their complexity. In fact, the Mersenne +// Twister is fairly easy code to understand: if you're trying to get a handle +// on the code, I strongly suggest looking at MersenneTwister.java first. +// -- Sean + +public strictfp class MersenneTwisterFast implements Serializable, Cloneable + { + // Serialization + private static final long serialVersionUID = -8219700664442619525L; // locked as of Version 15 + + // Period parameters + private static final int N = 624; + private static final int M = 397; + private static final int MATRIX_A = 0x9908b0df; // private static final * constant vector a + private static final int UPPER_MASK = 0x80000000; // most significant w-r bits + private static final int LOWER_MASK = 0x7fffffff; // least significant r bits + + + // Tempering parameters + private static final int TEMPERING_MASK_B = 0x9d2c5680; + private static final int TEMPERING_MASK_C = 0xefc60000; + + private int mt[]; // the array for the state vector + private int mti; // mti==N+1 means mt[N] is not initialized + private int mag01[]; + + // a good initial seed (of int size, though stored in a long) + //private static final long GOOD_SEED = 4357; + + private double __nextNextGaussian; + private boolean __haveNextNextGaussian; + + /* We're overriding all internal data, to my knowledge, so this should be okay */ + public Object clone() + { + try + { + MersenneTwisterFast f = (MersenneTwisterFast)(super.clone()); + f.mt = (int[])(mt.clone()); + f.mag01 = (int[])(mag01.clone()); + return f; + } + catch (CloneNotSupportedException e) { throw new InternalError(); } // should never happen + } + + /** Returns true if the MersenneTwisterFast's current internal state is equal to another MersenneTwisterFast. + This is roughly the same as equals(other), except that it compares based on value but does not + guarantee the contract of immutability (obviously random number generators are immutable). + Note that this does NOT check to see if the internal gaussian storage is the same + for both. You can guarantee that the internal gaussian storage is the same (and so the + nextGaussian() methods will return the same values) by calling clearGaussian() on both + objects. */ + public boolean stateEquals(MersenneTwisterFast other) + { + if (other == this) return true; + if (other == null)return false; + + if (mti != other.mti) return false; + for(int x=0;x>> 30)) + mti); + /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ + /* In the previous versions, MSBs of the seed affect */ + /* only MSBs of the array mt[]. */ + /* 2002/01/09 modified by Makoto Matsumoto */ + // mt[mti] &= 0xffffffff; + /* for >32 bit machines */ + } + } + + + /** + * Sets the seed of the MersenneTwister using an array of integers. + * Your array must have a non-zero length. Only the first 624 integers + * in the array are used; if the array is shorter than this then + * integers are repeatedly used in a wrap-around fashion. + */ + + public void setSeed(int[] array) + { + if (array.length == 0) + throw new IllegalArgumentException("Array length must be greater than zero"); + int i, j, k; + setSeed(19650218); + i=1; j=0; + k = (N>array.length ? N : array.length); + for (; k!=0; k--) + { + mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >>> 30)) * 1664525)) + array[j] + j; /* non linear */ + // mt[i] &= 0xffffffff; /* for WORDSIZE > 32 machines */ + i++; + j++; + if (i>=N) { mt[0] = mt[N-1]; i=1; } + if (j>=array.length) j=0; + } + for (k=N-1; k!=0; k--) + { + mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >>> 30)) * 1566083941)) - i; /* non linear */ + // mt[i] &= 0xffffffff; /* for WORDSIZE > 32 machines */ + i++; + if (i>=N) + { + mt[0] = mt[N-1]; i=1; + } + } + mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */ + } + + + public int nextInt() + { + int y; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + return y; + } + + + + public short nextShort() + { + int y; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + return (short)(y >>> 16); + } + + + + public char nextChar() + { + int y; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + return (char)(y >>> 16); + } + + + public boolean nextBoolean() + { + int y; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + return (boolean)((y >>> 31) != 0); + } + + + + /** This generates a coin flip with a probability probability + of returning true, else returning false. probability must + be between 0.0 and 1.0, inclusive. Not as precise a random real + event as nextBoolean(double), but twice as fast. To explicitly + use this, remember you may need to cast to float first. */ + + public boolean nextBoolean(float probability) + { + int y; + + if (probability < 0.0f || probability > 1.0f) + throw new IllegalArgumentException ("probability must be between 0.0 and 1.0 inclusive."); + if (probability==0.0f) return false; // fix half-open issues + else if (probability==1.0f) return true; // fix half-open issues + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + return (y >>> 8) / ((float)(1 << 24)) < probability; + } + + + /** This generates a coin flip with a probability probability + of returning true, else returning false. probability must + be between 0.0 and 1.0, inclusive. */ + + public boolean nextBoolean(double probability) + { + int y; + int z; + + if (probability < 0.0 || probability > 1.0) + throw new IllegalArgumentException ("probability must be between 0.0 and 1.0 inclusive."); + if (probability==0.0) return false; // fix half-open issues + else if (probability==1.0) return true; // fix half-open issues + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + for (; kk < N-1; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + z = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (z >>> 1) ^ mag01[z & 0x1]; + + mti = 0; + } + + z = mt[mti++]; + z ^= z >>> 11; // TEMPERING_SHIFT_U(z) + z ^= (z << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(z) + z ^= (z << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(z) + z ^= (z >>> 18); // TEMPERING_SHIFT_L(z) + + /* derived from nextDouble documentation in jdk 1.2 docs, see top */ + return ((((long)(y >>> 6)) << 27) + (z >>> 5)) / (double)(1L << 53) < probability; + } + + + public byte nextByte() + { + int y; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + return (byte)(y >>> 24); + } + + + public void nextBytes(byte[] bytes) + { + int y; + + for (int x=0;x= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + bytes[x] = (byte)(y >>> 24); + } + } + + + /** Returns a long drawn uniformly from 0 to n-1. Suffice it to say, + n must be greater than 0, or an IllegalArgumentException is raised. */ + + public long nextLong() + { + int y; + int z; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + for (; kk < N-1; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + z = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (z >>> 1) ^ mag01[z & 0x1]; + + mti = 0; + } + + z = mt[mti++]; + z ^= z >>> 11; // TEMPERING_SHIFT_U(z) + z ^= (z << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(z) + z ^= (z << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(z) + z ^= (z >>> 18); // TEMPERING_SHIFT_L(z) + + return (((long)y) << 32) + (long)z; + } + + + + /** Returns a long drawn uniformly from 0 to n-1. Suffice it to say, + n must be > 0, or an IllegalArgumentException is raised. */ + public long nextLong(long n) + { + if (n<=0) + throw new IllegalArgumentException("n must be positive, got: " + n); + + long bits, val; + do + { + int y; + int z; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + for (; kk < N-1; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + z = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (z >>> 1) ^ mag01[z & 0x1]; + + mti = 0; + } + + z = mt[mti++]; + z ^= z >>> 11; // TEMPERING_SHIFT_U(z) + z ^= (z << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(z) + z ^= (z << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(z) + z ^= (z >>> 18); // TEMPERING_SHIFT_L(z) + + bits = (((((long)y) << 32) + (long)z) >>> 1); + val = bits % n; + } while (bits - val + (n-1) < 0); + return val; + } + + /** Returns a random double in the half-open range from [0.0,1.0). Thus 0.0 is a valid + result but 1.0 is not. */ + public double nextDouble() + { + int y; + int z; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + for (; kk < N-1; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + z = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (z >>> 1) ^ mag01[z & 0x1]; + + mti = 0; + } + + z = mt[mti++]; + z ^= z >>> 11; // TEMPERING_SHIFT_U(z) + z ^= (z << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(z) + z ^= (z << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(z) + z ^= (z >>> 18); // TEMPERING_SHIFT_L(z) + + /* derived from nextDouble documentation in jdk 1.2 docs, see top */ + return ((((long)(y >>> 6)) << 27) + (z >>> 5)) / (double)(1L << 53); + } + + + + /** Returns a double in the range from 0.0 to 1.0, possibly inclusive of 0.0 and 1.0 themselves. Thus: + + + + + + + + +
ExpressionInterval
nextDouble(false, false)(0.0, 1.0)
nextDouble(true, false)[0.0, 1.0)
nextDouble(false, true)(0.0, 1.0]
nextDouble(true, true)[0.0, 1.0]
Table of intervals
+ +

This version preserves all possible random values in the double range. + */ + public double nextDouble(boolean includeZero, boolean includeOne) + { + double d = 0.0; + do + { + d = nextDouble(); // grab a value, initially from half-open [0.0, 1.0) + if (includeOne && nextBoolean()) d += 1.0; // if includeOne, with 1/2 probability, push to [1.0, 2.0) + } + while ( (d > 1.0) || // everything above 1.0 is always invalid + (!includeZero && d == 0.0)); // if we're not including zero, 0.0 is invalid + return d; + } + + + /** + Clears the internal gaussian variable from the RNG. You only need to do this + in the rare case that you need to guarantee that two RNGs have identical internal + state. Otherwise, disregard this method. See stateEquals(other). + */ + public void clearGaussian() { __haveNextNextGaussian = false; } + + + public double nextGaussian() + { + if (__haveNextNextGaussian) + { + __haveNextNextGaussian = false; + return __nextNextGaussian; + } + else + { + double v1, v2, s; + do + { + int y; + int z; + int a; + int b; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + for (; kk < N-1; kk++) + { + z = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (z >>> 1) ^ mag01[z & 0x1]; + } + z = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (z >>> 1) ^ mag01[z & 0x1]; + + mti = 0; + } + + z = mt[mti++]; + z ^= z >>> 11; // TEMPERING_SHIFT_U(z) + z ^= (z << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(z) + z ^= (z << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(z) + z ^= (z >>> 18); // TEMPERING_SHIFT_L(z) + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + a = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (a >>> 1) ^ mag01[a & 0x1]; + } + for (; kk < N-1; kk++) + { + a = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (a >>> 1) ^ mag01[a & 0x1]; + } + a = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (a >>> 1) ^ mag01[a & 0x1]; + + mti = 0; + } + + a = mt[mti++]; + a ^= a >>> 11; // TEMPERING_SHIFT_U(a) + a ^= (a << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(a) + a ^= (a << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(a) + a ^= (a >>> 18); // TEMPERING_SHIFT_L(a) + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + b = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (b >>> 1) ^ mag01[b & 0x1]; + } + for (; kk < N-1; kk++) + { + b = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (b >>> 1) ^ mag01[b & 0x1]; + } + b = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (b >>> 1) ^ mag01[b & 0x1]; + + mti = 0; + } + + b = mt[mti++]; + b ^= b >>> 11; // TEMPERING_SHIFT_U(b) + b ^= (b << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(b) + b ^= (b << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(b) + b ^= (b >>> 18); // TEMPERING_SHIFT_L(b) + + /* derived from nextDouble documentation in jdk 1.2 docs, see top */ + v1 = 2 * + (((((long)(y >>> 6)) << 27) + (z >>> 5)) / (double)(1L << 53)) + - 1; + v2 = 2 * (((((long)(a >>> 6)) << 27) + (b >>> 5)) / (double)(1L << 53)) + - 1; + s = v1 * v1 + v2 * v2; + } while (s >= 1 || s==0); + double multiplier = StrictMath.sqrt(-2 * StrictMath.log(s)/s); + __nextNextGaussian = v2 * multiplier; + __haveNextNextGaussian = true; + return v1 * multiplier; + } + } + + + + + + /** Returns a random float in the half-open range from [0.0f,1.0f). Thus 0.0f is a valid + result but 1.0f is not. */ + public float nextFloat() + { + int y; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + return (y >>> 8) / ((float)(1 << 24)); + } + + + /** Returns a float in the range from 0.0f to 1.0f, possibly inclusive of 0.0f and 1.0f themselves. Thus: + + + + + + + + +
ExpressionInterval
nextFloat(false, false)(0.0f, 1.0f)
nextFloat(true, false)[0.0f, 1.0f)
nextFloat(false, true)(0.0f, 1.0f]
nextFloat(true, true)[0.0f, 1.0f]
Table of intervals
+ +

This version preserves all possible random values in the float range. + */ + public float nextFloat(boolean includeZero, boolean includeOne) + { + float d = 0.0f; + do + { + d = nextFloat(); // grab a value, initially from half-open [0.0f, 1.0f) + if (includeOne && nextBoolean()) d += 1.0f; // if includeOne, with 1/2 probability, push to [1.0f, 2.0f) + } + while ( (d > 1.0f) || // everything above 1.0f is always invalid + (!includeZero && d == 0.0f)); // if we're not including zero, 0.0f is invalid + return d; + } + + + + /** Returns an integer drawn uniformly from 0 to n-1. Suffice it to say, + n must be > 0, or an IllegalArgumentException is raised. */ + public int nextInt(int n) + { + if (n<=0) + throw new IllegalArgumentException("n must be positive, got: " + n); + + if ((n & -n) == n) // i.e., n is a power of 2 + { + int y; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + return (int)((n * (long) (y >>> 1) ) >> 31); + } + + int bits, val; + do + { + int y; + + if (mti >= N) // generate N words at one time + { + int kk; + final int[] mt = this.mt; // locals are slightly faster + final int[] mag01 = this.mag01; // locals are slightly faster + + for (kk = 0; kk < N - M; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < N-1; kk++) + { + y = (mt[kk] & UPPER_MASK) | (mt[kk+1] & LOWER_MASK); + mt[kk] = mt[kk+(M-N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (mt[N-1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N-1] = mt[M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + mti = 0; + } + + y = mt[mti++]; + y ^= y >>> 11; // TEMPERING_SHIFT_U(y) + y ^= (y << 7) & TEMPERING_MASK_B; // TEMPERING_SHIFT_S(y) + y ^= (y << 15) & TEMPERING_MASK_C; // TEMPERING_SHIFT_T(y) + y ^= (y >>> 18); // TEMPERING_SHIFT_L(y) + + bits = (y >>> 1); + val = bits % n; + } while(bits - val + (n-1) < 0); + return val; + } + + + /** + * Tests the code. + */ + public static void main(String args[]) + { + int j; + + MersenneTwisterFast r; + + // CORRECTNESS TEST + // COMPARE WITH http://www.math.keio.ac.jp/matumoto/CODES/MT2002/mt19937ar.out + + r = new MersenneTwisterFast(new int[]{0x123, 0x234, 0x345, 0x456}); + System.out.println("Output of MersenneTwisterFast with new (2002/1/26) seeding mechanism"); + for (j=0;j<1000;j++) + { + // first, convert the int from signed to "unsigned" + long l = (long)r.nextInt(); + if (l < 0 ) l += 4294967296L; // max int value + String s = String.valueOf(l); + while(s.length() < 10) s = " " + s; // buffer + System.out.print(s + " "); + if (j%5==4) System.out.println(); + } + + // SPEED TEST + + final long SEED = 4357; + + int xx; long ms; + System.out.println("\nTime to test grabbing 100000000 ints"); + + Random rr = new Random(SEED); + xx = 0; + ms = System.currentTimeMillis(); + for (j = 0; j < 100000000; j++) + xx += rr.nextInt(); + System.out.println("java.util.Random: " + (System.currentTimeMillis()-ms) + " Ignore this: " + xx); + + r = new MersenneTwisterFast(SEED); + ms = System.currentTimeMillis(); + xx=0; + for (j = 0; j < 100000000; j++) + xx += r.nextInt(); + System.out.println("Mersenne Twister Fast: " + (System.currentTimeMillis()-ms) + " Ignore this: " + xx); + + // TEST TO COMPARE TYPE CONVERSION BETWEEN + // MersenneTwisterFast.java AND MersenneTwister.java + + System.out.println("\nGrab the first 1000 booleans"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextBoolean() + " "); + if (j%8==7) System.out.println(); + } + if (!(j%8==7)) System.out.println(); + + System.out.println("\nGrab 1000 booleans of increasing probability using nextBoolean(double)"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextBoolean((double)(j/999.0)) + " "); + if (j%8==7) System.out.println(); + } + if (!(j%8==7)) System.out.println(); + + System.out.println("\nGrab 1000 booleans of increasing probability using nextBoolean(float)"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextBoolean((float)(j/999.0f)) + " "); + if (j%8==7) System.out.println(); + } + if (!(j%8==7)) System.out.println(); + + byte[] bytes = new byte[1000]; + System.out.println("\nGrab the first 1000 bytes using nextBytes"); + r = new MersenneTwisterFast(SEED); + r.nextBytes(bytes); + for (j = 0; j < 1000; j++) + { + System.out.print(bytes[j] + " "); + if (j%16==15) System.out.println(); + } + if (!(j%16==15)) System.out.println(); + + byte b; + System.out.println("\nGrab the first 1000 bytes -- must be same as nextBytes"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print((b = r.nextByte()) + " "); + if (b!=bytes[j]) System.out.print("BAD "); + if (j%16==15) System.out.println(); + } + if (!(j%16==15)) System.out.println(); + + System.out.println("\nGrab the first 1000 shorts"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextShort() + " "); + if (j%8==7) System.out.println(); + } + if (!(j%8==7)) System.out.println(); + + System.out.println("\nGrab the first 1000 ints"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextInt() + " "); + if (j%4==3) System.out.println(); + } + if (!(j%4==3)) System.out.println(); + + System.out.println("\nGrab the first 1000 ints of different sizes"); + r = new MersenneTwisterFast(SEED); + int max = 1; + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextInt(max) + " "); + max *= 2; + if (max <= 0) max = 1; + if (j%4==3) System.out.println(); + } + if (!(j%4==3)) System.out.println(); + + System.out.println("\nGrab the first 1000 longs"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextLong() + " "); + if (j%3==2) System.out.println(); + } + if (!(j%3==2)) System.out.println(); + + System.out.println("\nGrab the first 1000 longs of different sizes"); + r = new MersenneTwisterFast(SEED); + long max2 = 1; + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextLong(max2) + " "); + max2 *= 2; + if (max2 <= 0) max2 = 1; + if (j%4==3) System.out.println(); + } + if (!(j%4==3)) System.out.println(); + + System.out.println("\nGrab the first 1000 floats"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextFloat() + " "); + if (j%4==3) System.out.println(); + } + if (!(j%4==3)) System.out.println(); + + System.out.println("\nGrab the first 1000 doubles"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextDouble() + " "); + if (j%3==2) System.out.println(); + } + if (!(j%3==2)) System.out.println(); + + System.out.println("\nGrab the first 1000 gaussian doubles"); + r = new MersenneTwisterFast(SEED); + for (j = 0; j < 1000; j++) + { + System.out.print(r.nextGaussian() + " "); + if (j%3==2) System.out.println(); + } + if (!(j%3==2)) System.out.println(); + + } + } diff --git a/src/main/java/com/example/crud/crypto/Password.java b/src/main/java/com/example/crud/crypto/Password.java new file mode 100644 index 0000000..2b24f91 --- /dev/null +++ b/src/main/java/com/example/crud/crypto/Password.java @@ -0,0 +1,52 @@ +//http://stackoverflow.com/a/11038230/366692 +package com.example.crud.crypto; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.SecureRandom; +import org.apache.commons.codec.binary.Base64; + +public class Password { + // The higher the number of iterations the more expensive computing the hash is for us and also for an attacker. + private static final int iterations = 20*1000; + private static final int saltLen = 32; + private static final int desiredKeyLen = 256; + + /** + * Computes a salted PBKDF2 hash of given plaintext password suitable for storing in a database. + * Empty passwords are not supported. + */ + public static String getSaltedHash(String password) throws Exception { + byte[] salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen); + // combine the salt with the password + return Base64.encodeBase64String(salt) + "$" + hash(password, salt); + } + + /** + * Checks whether given plaintext password corresponds to a stored salted hash of the password. + */ + public static boolean check(String password, String stored) throws Exception{ + String[] saltAndPass = stored.split("\\$"); + if (saltAndPass.length != 2) { + throw new IllegalStateException( + "The stored password have the form 'salt$hash'"); + } + String hashOfInput = hash(password, Base64.decodeBase64(saltAndPass[0])); + return hashOfInput.equals(saltAndPass[1]); + } + + /** + * NOTE: using PBKDF2 from Sun, an alternative is https://github.com/wg/scrypt + * cf. http://www.unlimitednovelty.com/2012/03/dont-use-bcrypt.html + */ + private static String hash(String password, byte[] salt) throws Exception { + if (password == null || password.length() == 0) + throw new IllegalArgumentException("Empty passwords are not supported."); + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + SecretKey key = f.generateSecret(new PBEKeySpec( + password.toCharArray(), salt, iterations, desiredKeyLen) + ); + return Base64.encodeBase64String(key.getEncoded()); + } +} diff --git a/src/main/java/com/example/crud/crypto/Randomness.java b/src/main/java/com/example/crud/crypto/Randomness.java new file mode 100644 index 0000000..384c8d7 --- /dev/null +++ b/src/main/java/com/example/crud/crypto/Randomness.java @@ -0,0 +1,190 @@ +package com.example.crud.crypto; + +import java.security.SecureRandom; +import java.util.regex.Pattern; + +public final class Randomness { + private static final int NUM_SEEDS = 16; + private static final SecureRandom SEED_PROVIDER = new SecureRandom(); + private static final Pattern DB_OBJECT_ID_PATTERN = Pattern.compile("[0-9a-f]{24}", Pattern.CASE_INSENSITIVE); + private static final String BASE64_CHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + private static final String OBJECT_ID_CHAR = "0123456789abcdef"; + private static final int[] BASE64_NUM = new int[256]; //maps base64 characters to 0-63, others to 0. + + static { + for(int i = 0; i < 64; ++i) { + BASE64_NUM[BASE64_CHAR.charAt(i)] = i; + } + } + + private static final ThreadLocal SECURE_RANDOM = new ThreadLocal() { + @Override + protected ReseedingSecureRandom initialValue() { + return new ReseedingSecureRandom(); + } + }; + + // Use the MersenneTwisterFast implementation for its performance and long period. The implementation is + // not thread-safe so we keep an instance in thread-local storage. + private static final ThreadLocal MERSENNE_TWISTER = new ThreadLocal() { + @Override + protected MersenneTwisterFast initialValue() { + int[] seedInts = new int[NUM_SEEDS]; + for (int i = 0; i < NUM_SEEDS; i++) { + seedInts[i] = SEED_PROVIDER.nextInt(); + } + + return new MersenneTwisterFast(seedInts); + } + }; + + private Randomness() {} + + /** Unsecure! */ + public static MersenneTwisterFast getMersenneTwister() { + return MERSENNE_TWISTER.get(); + } + + public static int randomIntSecure(int n) { + if (n < 1) { + throw new IllegalArgumentException(); + } + + // NOTE: This is completely different from how java.util.Random#nextInt(int) does it + return Math.abs(SECURE_RANDOM.get().nextInt()) % n; + } + + public static int[] randomIntsSecure(int count) { + int[] ints = new int[count]; + for (int i = 0; i < ints.length; ++i) { + ints[i] = SECURE_RANDOM.get().nextInt(); + } + return ints; + } + + public static int[] randomIntsUnsecure(int count) { + MersenneTwisterFast mt = getMersenneTwister(); + int[] ints = new int[count]; + for (int i = 0; i < ints.length; ++i) { + ints[i] = mt.nextInt(); + } + return ints; + } + + public static byte[] randomBytesSecure(int size /* in bytes */) { + byte[] bytes = new byte[size]; + SECURE_RANDOM.get().nextBytes(bytes); + + return bytes; + } + + public static byte[] randomBytesUnsecure(int size /* in bytes */) { + byte[] bytes = new byte[size]; + getMersenneTwister().nextBytes(bytes); + + return bytes; + } + + public static String randomHexStringSecure(int lengthBytes) { + StringBuilder str = new StringBuilder(); + ReseedingSecureRandom sr = SECURE_RANDOM.get(); + for (int i = 0; i < lengthBytes; i++) { + str.append(Integer.toHexString(sr.nextInt(16))); + } + + return str.toString(); + } + + public static boolean isValidDBObjectId(String stringId) { + return stringId != null && DB_OBJECT_ID_PATTERN.matcher(stringId).matches() + && stringId.toLowerCase().equals(stringId); + } + + public static String toValidDBObjectId(String stringId) { + if (isValidDBObjectId(stringId)) { + return stringId; + } + if (stringId.length() < 24) { + throw new AssertionError("Can't convert to valid DBObjectId " + stringId); + } + StringBuilder hexString = new StringBuilder(); + for (int i = 0; i < 24; ++i) { + byte bt = (byte) stringId.charAt(i); + String hs = Integer.toHexString(bt & 0x0F).toLowerCase(); + hexString.append(hs); + } + return hexString.toString(); + } + + public static String treeNodeId() { + return treeNodeId('\u0000'); + } + + public static String treeNodeId(char prefix) { + return treeNodeId(prefix, getMersenneTwister()); + } + + public static String treeNodeId(char prefix, MersenneTwisterFast twister) { + final int prefixLength = prefix == '\u0000' ? 0 : 1; + final int numChars = prefixLength + 16; // 16 * 6 is 96 bits of entropy + char[] chars = new char[numChars]; + if (prefixLength == 1) { + chars[0] = prefix; + } + for (int i = 0; i < 16; ++i) { + chars[prefixLength + i] = BASE64_CHAR.charAt(twister.nextInt(64)); + } + + return new String(chars); + } + + /** Not secure! */ + public static String alphaString(int length) { + MersenneTwisterFast mt = getMersenneTwister(); + char[] chars = new char[length]; + for (int i = 0; i < length; ++i) { + chars[i] = BASE64_CHAR.charAt(mt.nextInt(52)); + } + return new String(chars); + } + + public static String alphaNumericStringSecure(int length) { + ReseedingSecureRandom sr = SECURE_RANDOM.get(); + char[] chars = new char[length]; + for (int i = 0; i < length; ++i) { + chars[i] = BASE64_CHAR.charAt(sr.nextInt(62)); + } + return new String(chars); + } + + public static String elementId() { + MersenneTwisterFast mt = getMersenneTwister(); + char[] chars = new char[24]; + for (int i = 0; i < 24; ++i) { + chars[i] = OBJECT_ID_CHAR.charAt(mt.nextInt(16)); + } + return new String(chars); + } + + public static String alphaNumericString(int length) { + MersenneTwisterFast mt = getMersenneTwister(); + char[] chars = new char[length]; + for (int i = 0; i < length; ++i) { + chars[i] = BASE64_CHAR.charAt(mt.nextInt(62)); + } + return new String(chars); + } + + /** Returns a feature id without the ordinal */ + public static String featureId() { + return "F" + alphaNumericString(14); + } + + public static char toBase64Char(int position) { + return BASE64_CHAR.charAt(position % 64); + } + + public static int fromBase64Char(char character) { + return BASE64_NUM[character]; + } +} diff --git a/src/main/java/com/example/crud/crypto/ReseedingSecureRandom.java b/src/main/java/com/example/crud/crypto/ReseedingSecureRandom.java new file mode 100644 index 0000000..872b5c3 --- /dev/null +++ b/src/main/java/com/example/crud/crypto/ReseedingSecureRandom.java @@ -0,0 +1,41 @@ +package com.example.crud.crypto; + +import java.security.SecureRandom; +import java.util.concurrent.atomic.AtomicInteger; + +final class ReseedingSecureRandom { + /** How frequently does reseeding happen. This is not strict */ + private static final int RESEED_AT = 100000; + + /** Atomic integer that keeps track of number of calls */ + private final AtomicInteger count_ = new AtomicInteger(0); + /** Secure random */ + private volatile SecureRandom secureRandom_; + + ReseedingSecureRandom() { + secureRandom_ = new SecureRandom(); + } + + void nextBytes(byte[] bytes) { + getSecureRandom().nextBytes(bytes); + } + + int nextInt() { + return getSecureRandom().nextInt(); + } + + int nextInt(int n) { + return getSecureRandom().nextInt(n); + } + + private SecureRandom getSecureRandom() { + int currentCount = count_.incrementAndGet(); + if ((currentCount % RESEED_AT) == 0) { + // Reset to 0, next caller should not come into this "if" and might use the old secure random which is fine + count_.set(0); + secureRandom_ = new SecureRandom(); + } + + return secureRandom_; + } +} diff --git a/src/main/java/com/example/crud/db/annotations/Retry.java b/src/main/java/com/example/crud/db/annotations/Retry.java new file mode 100644 index 0000000..c6a476c --- /dev/null +++ b/src/main/java/com/example/crud/db/annotations/Retry.java @@ -0,0 +1,15 @@ +package com.example.crud.db.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Retry { + + Class[] on(); + + int times() default 1; +} diff --git a/src/main/java/com/example/crud/db/aspects/OptimisticConcurrencyControlAspect.java b/src/main/java/com/example/crud/db/aspects/OptimisticConcurrencyControlAspect.java new file mode 100644 index 0000000..2f0c77a --- /dev/null +++ b/src/main/java/com/example/crud/db/aspects/OptimisticConcurrencyControlAspect.java @@ -0,0 +1,81 @@ +package com.example.crud.db.aspects; + +import com.example.crud.db.annotations.Retry; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; + +import java.lang.reflect.Method; +import java.util.Arrays; + +@Aspect +public class OptimisticConcurrencyControlAspect { + + private static final Logger log = LoggerFactory.getLogger(OptimisticConcurrencyControlAspect.class); + + @Around("@annotation(com.example.crud.db.annotations.Retry)") + public Object retry(ProceedingJoinPoint pjp) throws Throwable { + Retry retryAnnotation = getRetryAnnotation(pjp); + return (retryAnnotation != null) ? proceed(pjp, retryAnnotation) : proceed(pjp); + } + + private Object proceed(ProceedingJoinPoint pjp) throws Throwable { + return pjp.proceed(); + } + + private Object proceed(ProceedingJoinPoint pjp, Retry retryAnnotation) throws Throwable { + int times = retryAnnotation.times(); + Class[] retryOn = retryAnnotation.on(); + Assert.isTrue(times > 0, "@Retry{times} should be greater than 0!"); + Assert.isTrue(retryOn.length > 0, "@Retry{on} should have at least one Throwable!"); + log.info("Proceed with {} retries on {}", times, Arrays.toString(retryOn)); + return tryProceeding(pjp, times, retryOn); + } + + private Object tryProceeding(ProceedingJoinPoint pjp, int times, Class[] retryOn) throws Throwable { + try { + return proceed(pjp); + } catch (Throwable throwable) { + if(isRetryThrowable(throwable, retryOn) && times-- > 0) { + log.info("Optimistic locking detected, {} remaining retries on {}", times, Arrays.toString(retryOn)); + return tryProceeding(pjp, times, retryOn); + } + throw throwable; + } + } + + private boolean isRetryThrowable(Throwable throwable, Class[] retryOn) { + Throwable[] causes = ExceptionUtils.getThrowables(throwable); + for(Throwable cause : causes) { + for(Class retryThrowable : retryOn) { + if(retryThrowable.isAssignableFrom(cause.getClass())) { + return true; + } + } + } + return false; + } + + private Retry getRetryAnnotation(ProceedingJoinPoint pjp) throws NoSuchMethodException { + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + Retry retryAnnotation = AnnotationUtils.findAnnotation(method, Retry.class); + + if(retryAnnotation != null) { + return retryAnnotation; + } + + Class[] argClasses = new Class[pjp.getArgs().length]; + for (int i = 0; i < pjp.getArgs().length; i++) { + argClasses[i] = pjp.getArgs()[i].getClass(); + } + method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argClasses); + return AnnotationUtils.findAnnotation(method, Retry.class); + } +} diff --git a/src/main/java/com/example/crud/db/dao/BaseRepository.java b/src/main/java/com/example/crud/db/dao/BaseRepository.java new file mode 100644 index 0000000..bda56b1 --- /dev/null +++ b/src/main/java/com/example/crud/db/dao/BaseRepository.java @@ -0,0 +1,94 @@ +package com.example.crud.db.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.QueryByExampleExecutor; + +import java.io.Serializable; +import java.util.List; + +@NoRepositoryBean +public interface BaseRepository extends JpaRepository, + PagingAndSortingRepository, QueryByExampleExecutor { //TODO(gburd): this requires gradle to pre-build classes like '@Employee' etc. }, QueryDslPredicateExecutor { + + /** + * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the + * entity instance completely. + * + * @param entity + * @return the saved entity + */ + S save(S entity); + + /** + * Saves all given models. + * + * @param entities + * @return the saved models + * @throws IllegalArgumentException in case the given entity is {@literal null}. + */ + List save(Iterable entities); + + /** + * Retrieves an entity by its id. + * + * @param id must not be {@literal null}. + * @return the entity with the given id or {@literal null} if none found + * @throws IllegalArgumentException if {@code id} is {@literal null} + */ + T findOne(ID id); + + /** + * Returns all instances of the type. + * + * @return all models + */ + List findAll(); + + /** + * Returns all instances of the type with the given IDs. + */ + List findAll(Iterable ids); + + /** + * Returns whether an entity with the given id exists. + * + * @param id must not be {@literal null}. + * @return true if an entity with the given id exists, {@literal false} otherwise + * @throws IllegalArgumentException if {@code id} is {@literal null} + */ + boolean exists(ID id); + + /** + * Returns the number of models available. + * + * @return the number of models + */ + long count(); + + /** + * Deletes a given entity. + * + * @throws IllegalArgumentException in case the given entity is {@literal null}. + */ + // Optional... + void delete(ID id); + + /** + * Deletes the given models. + * + * @throws IllegalArgumentException in case the given {@link Iterable} is {@literal null}. + */ + void delete(Iterable entities); + + /** + * Deletes all models managed by the models. + */ + void deleteAll(); + + /** + * Flush cache. + */ + default void refresh() {} //TODO(gburd): implement cache flush... ? +} diff --git a/src/main/java/com/example/crud/db/dao/EmployeeRepository.java b/src/main/java/com/example/crud/db/dao/EmployeeRepository.java new file mode 100644 index 0000000..837ff3d --- /dev/null +++ b/src/main/java/com/example/crud/db/dao/EmployeeRepository.java @@ -0,0 +1,18 @@ +package com.example.crud.db.dao; + +import com.example.crud.db.models.Employee; +import com.example.crud.db.models.Gender; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RepositoryRestResource +public interface EmployeeRepository extends BaseRepository { + List findByLastName(String lastName); + List findByGender(Gender gender); + List findByAgeLessThan(int age); + List findByAgeBetween(int age1, int age2); +// List fetchByLastNameLength(@Param("length") Long length); +} diff --git a/src/main/java/com/example/crud/db/models/AbstractModel.java b/src/main/java/com/example/crud/db/models/AbstractModel.java new file mode 100644 index 0000000..07a31a0 --- /dev/null +++ b/src/main/java/com/example/crud/db/models/AbstractModel.java @@ -0,0 +1,53 @@ +package com.example.crud.db.models; + +import com.example.crud.util.SerialVersionUID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.eclipse.persistence.annotations.*; +import org.eclipse.persistence.descriptors.changetracking.ChangeTracker; +import org.eclipse.persistence.sessions.Session; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import javax.persistence.*; +import java.beans.PropertyChangeListener; +import java.io.Serializable; +import java.util.Date; + +import static org.eclipse.persistence.annotations.ChangeTrackingType.AUTO; + +@EqualsAndHashCode +@ChangeTracking(AUTO) // uses the persistence property change listeners +/* NOTE: @Cache is ignored on mapped superclasses (@MappedSuperclass), this is here purely for reference sake. +@Cache(type = CacheType.SOFT_WEAK // let the GC remove cached data under memory pressure + ,expiryTimeOfDay=@TimeOfDay(hour=1) // cached data will expire after 1hr even if there is enough memory to keep the data around longer + ,disableHits=false // use the cache (when true the cache is only used for identity) + ,isolation=ISOLATED // cache within UnitOfWork (db/txn), or ".SHARED" for Session scope + ,alwaysRefresh=true // update cache on reads... + ,refreshOnlyIfNewer=true // only when read value is newer than one in cache + ,databaseChangeNotificationType=INVALIDATE // when DCN is enabled, invalidate cached objects when DB says they've mutated + ,coordinationType=SEND_NEW_OBJECTS_WITH_CHANGES) //.INVALIDATE_CHANGED_OBJECTS */ +@MappedSuperclass +public abstract class AbstractModel implements Model, ChangeTracker { + private static final long serialVersionUID = SerialVersionUID.compute(AbstractModel.class); + + // Instance variables *not* mapped to database fields must be marked as @Transient + @Transient private PropertyChangeListener listener; + + @Getter @CreatedDate @Temporal(TemporalType.TIMESTAMP) @Column(name="CREATED") Date createdDate; + @Getter @LastModifiedDate @Temporal(TemporalType.TIMESTAMP) @Column(name="MODIFIED") Date modifiedDate; + + // OPTIMISTIC CONCURRENCY CONTROL + @Version @Column(name="VERSION") private long version; + + // TODO(gburd): what must these two methods do? + public PropertyChangeListener _persistence_getPropertyChangeListener() { + return listener; + } + + public void _persistence_setPropertyChangeListener(PropertyChangeListener listener) { + this.listener = listener; + } +} diff --git a/src/main/java/com/example/crud/db/models/Address.java b/src/main/java/com/example/crud/db/models/Address.java new file mode 100644 index 0000000..a47b662 --- /dev/null +++ b/src/main/java/com/example/crud/db/models/Address.java @@ -0,0 +1,55 @@ +package com.example.crud.db.models; + +import com.example.crud.util.SerialVersionUID; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.eclipse.persistence.annotations.Cache; +import org.eclipse.persistence.annotations.CacheType; +import org.eclipse.persistence.annotations.TimeOfDay; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import javax.persistence.*; + +import static javax.persistence.FetchType.LAZY; +import static org.eclipse.persistence.annotations.CacheCoordinationType.SEND_NEW_OBJECTS_WITH_CHANGES; +import static org.eclipse.persistence.annotations.DatabaseChangeNotificationType.INVALIDATE; +import static org.eclipse.persistence.config.CacheIsolationType.ISOLATED; + +@EnableJpaAuditing +@EntityListeners(AuditingEntityListener.class) +@EqualsAndHashCode(callSuper=true) +@Cache(type = CacheType.SOFT_WEAK + ,expiryTimeOfDay=@TimeOfDay(hour=1) + ,disableHits=false + ,isolation=ISOLATED + ,alwaysRefresh=true + ,refreshOnlyIfNewer=true + ,databaseChangeNotificationType=INVALIDATE + ,coordinationType=SEND_NEW_OBJECTS_WITH_CHANGES) +@Table(name = "ADDRESS") +public @Data @Entity class Address extends AbstractModel { + private static final long serialVersionUID = SerialVersionUID.compute(Address.class); + + // PRIMARY KEY + @Id @Column @GeneratedValue(generator = "flake-seq") + private Long id; + + // FIELDS + private String city; + private String country; + @Basic(fetch=LAZY) private String province; + private String postalCode; + private String street; + + public Address() { + } + + public Address(String city, String country, String province, String postalCode, String street) { + this.city = city; + this.country = country; + this.province = province; + this.postalCode = postalCode; + this.street = street; + } +} diff --git a/src/main/java/com/example/crud/db/models/Employee.java b/src/main/java/com/example/crud/db/models/Employee.java new file mode 100644 index 0000000..f55b489 --- /dev/null +++ b/src/main/java/com/example/crud/db/models/Employee.java @@ -0,0 +1,148 @@ +package com.example.crud.db.models; + +import com.example.crud.util.SerialVersionUID; +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.eclipse.persistence.annotations.*; +import org.eclipse.persistence.annotations.Cache; +import org.eclipse.persistence.config.HintValues; +import org.eclipse.persistence.config.QueryHints; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import javax.persistence.*; +import javax.persistence.CollectionTable; +import javax.persistence.Convert; +import javax.persistence.Index; +import javax.swing.*; +import javax.validation.constraints.*; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import static javax.persistence.TemporalType.DATE; +import static org.eclipse.persistence.annotations.CacheCoordinationType.SEND_NEW_OBJECTS_WITH_CHANGES; +import static org.eclipse.persistence.annotations.DatabaseChangeNotificationType.INVALIDATE; +import static org.eclipse.persistence.config.CacheIsolationType.ISOLATED; + +@EqualsAndHashCode(callSuper=true) +@EnableJpaAuditing +@EntityListeners(AuditingEntityListener.class) +@SecondaryTable(name = "SALARY") +@NamedQueries({ + @NamedQuery(name = "Employee.findAll", query = "SELECT e FROM Employee e ORDER BY e.id"), + @NamedQuery(name = "Employee.findByName", query = "SELECT e FROM Employee e WHERE e.firstName LIKE :firstName AND e.lastName LIKE :lastName"), + @NamedQuery(name = "Employee.count", query = "SELECT COUNT(e) FROM Employee e"), + @NamedQuery(name = "Employee.countByName", query = "SELECT COUNT(e) FROM Employee e WHERE e.firstName LIKE :firstName AND e.lastName LIKE :lastName"), + // Query used in {@link IdInPaging} + @NamedQuery(name = "Employee.idsIn", query = "SELECT e FROM Employee e WHERE e.id IN :IDS ORDER BY e.id", + hints = { @QueryHint(name = QueryHints.QUERY_RESULTS_CACHE, value = HintValues.TRUE) }) }) +@Cache(type = CacheType.SOFT_WEAK + ,expiryTimeOfDay=@TimeOfDay(hour=1) + ,disableHits=false + ,isolation=ISOLATED + ,alwaysRefresh=true + ,refreshOnlyIfNewer=true + ,databaseChangeNotificationType=INVALIDATE + ,coordinationType=SEND_NEW_OBJECTS_WITH_CHANGES) +@Table(name = "EMPLOYEE", + indexes={ + @Index(name="EMP_SSN_INDEX", unique=true, columnList="SSN"), + @Index(name="EMP_EMAIL_INDEX", columnList="EMAIL"), + @Index(name="EMP_F_NAME_INDEX", columnList="FIRST_NAME"), + @Index(name="EMP_L_NAME_INDEX", columnList="LAST_NAME"), + @Index(name="EMP_NAME_INDEX", columnList="FIRST_NAME LAST_NAME") }) +public @Data @Entity class Employee extends AbstractModel { + private static final long serialVersionUID = SerialVersionUID.compute(AbstractModel.class); + + // PRIMARY KEY + @Id @Column @GeneratedValue(generator = "flake-seq") + private Long id; + + // FIELDS + @Column(name="SSN") @CacheIndex @NotNull private String socialSecurityNumber; + @Column private String honorific; + @Column(name="FIRST_NAME") private String firstName; + @Column(name="LAST_NAME") private String lastName; + @Column private String suffix; + @Column(name="EMAIL") private String emailAddress; + @Column @Past @Temporal(DATE) private Calendar birthdate; + @Column @Digits(integer=3, fraction=0) @NotNull @Min(0) @Max(125) private int age = 0; + @Basic(fetch=FetchType.LAZY) @Lob private ImageIcon picture; + + /* NOTE: Gender mapped using Basic with an ObjectTypeConverter to map between + * single char code value in database to enum. JPA only supports mapping to + * the full name of the enum or its ordinal value. */ + @Basic @Column(name = "GENDER") @Convert(converter = GenderConverter.class) private Gender gender = Gender.Male; + + @Column(table = "SALARY") + private double salary; + + // RELATIONS + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "MANAGER_ID") + @JsonBackReference + private Employee manager; + + @OneToMany(mappedBy = "manager") + private List managedEmployees = new ArrayList(); + + @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true) + @PrivateOwned + @JsonManagedReference + private List phoneNumbers = new ArrayList(); + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + @JoinColumn(name = "ADDRESS_ID") + private Address address; + + @Embedded + @AttributeOverrides({ @AttributeOverride(name = "startDate", column = @Column(name = "START_DATE")), + @AttributeOverride(name = "endDate", column = @Column(name = "END_DATE")) }) + private EmploymentPeriod period; + + @ElementCollection + @CollectionTable(name = "RESPONS") + private List responsibilities = new ArrayList(); + + + public Employee addManagedEmployee(Employee employee) { + getManagedEmployees().add(employee); + employee.setManager(this); + return employee; + } + + public Employee removeManagedEmployee(Employee employee) { + getManagedEmployees().remove(employee); + employee.setManager(null); + return employee; + } + + public PhoneNumber addPhoneNumber(PhoneNumber phoneNumber) { + getPhoneNumbers().add(phoneNumber); + phoneNumber.setOwner(this); + return phoneNumber; + } + + public PhoneNumber addPhoneNumber(String type, String number) { + PhoneNumber phoneNumber = new PhoneNumber(type, number); + return addPhoneNumber(phoneNumber); + } + + public PhoneNumber removePhoneNumber(PhoneNumber phoneNumber) { + getPhoneNumbers().remove(phoneNumber); + phoneNumber.setOwner(null); + return phoneNumber; + } + + public void addResponsibility(String responsibility) { + getResponsibilities().add(responsibility); + } + + public void removeResponsibility(String responsibility) { + getResponsibilities().remove(responsibility); + } + +} diff --git a/src/main/java/com/example/crud/db/models/EmploymentPeriod.java b/src/main/java/com/example/crud/db/models/EmploymentPeriod.java new file mode 100644 index 0000000..50d6dc5 --- /dev/null +++ b/src/main/java/com/example/crud/db/models/EmploymentPeriod.java @@ -0,0 +1,29 @@ +package com.example.crud.db.models; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import static javax.persistence.TemporalType.TIMESTAMP; + +import java.util.Calendar; + +import javax.persistence.Embeddable; +import javax.persistence.Temporal; + +/** + * Represents the period of time an employee has worked for the company. A null + * endDate indicates that the employee is current. + */ +public @Data @Embeddable class EmploymentPeriod { + + @Temporal(TIMESTAMP) private Calendar startDate; + @Temporal(TIMESTAMP) private Calendar endDate; + + public void setStartDate(int year, int month, int date) { + getStartDate().set(year, month, date); + } + + public void setEndDate(int year, int month, int date) { + getEndDate().set(year, month, date); + } +} diff --git a/src/main/java/com/example/crud/db/models/Gender.java b/src/main/java/com/example/crud/db/models/Gender.java new file mode 100644 index 0000000..95a13dd --- /dev/null +++ b/src/main/java/com/example/crud/db/models/Gender.java @@ -0,0 +1,5 @@ +package com.example.crud.db.models; + +public enum Gender { + Female, Male, ; +} diff --git a/src/main/java/com/example/crud/db/models/GenderConverter.java b/src/main/java/com/example/crud/db/models/GenderConverter.java new file mode 100644 index 0000000..61cceea --- /dev/null +++ b/src/main/java/com/example/crud/db/models/GenderConverter.java @@ -0,0 +1,36 @@ +package com.example.crud.db.models; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter(autoApply = true) +public class GenderConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Gender gender) { + switch (gender) { + case Male: + return "M"; + + case Female: + return "F"; + + default: + throw new IllegalArgumentException("Invalid gender: " + gender); + } + } + + @Override + public Gender convertToEntityAttribute(String gender) { + switch (gender) { + case "M": + return Gender.Male; + + case "F": + return Gender.Female; + + default: + throw new IllegalArgumentException("Invalid gender code: " + gender); + } + } +} diff --git a/src/main/java/com/example/crud/db/models/Login.java b/src/main/java/com/example/crud/db/models/Login.java new file mode 100644 index 0000000..0dc9566 --- /dev/null +++ b/src/main/java/com/example/crud/db/models/Login.java @@ -0,0 +1,54 @@ +package com.example.crud.db.models; + +import com.example.crud.crypto.Password; +import com.example.crud.util.SerialVersionUID; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.eclipse.persistence.annotations.Cache; +import org.eclipse.persistence.annotations.CacheType; +import org.eclipse.persistence.annotations.TimeOfDay; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import javax.persistence.*; + +import static org.eclipse.persistence.annotations.CacheCoordinationType.SEND_NEW_OBJECTS_WITH_CHANGES; +import static org.eclipse.persistence.annotations.DatabaseChangeNotificationType.INVALIDATE; +import static org.eclipse.persistence.config.CacheIsolationType.ISOLATED; + +@EnableJpaAuditing +@EntityListeners(AuditingEntityListener.class) +@EqualsAndHashCode(callSuper=true) +@Cache(type = CacheType.SOFT_WEAK + ,expiryTimeOfDay=@TimeOfDay(hour=1) + ,disableHits=false + ,isolation=ISOLATED + ,alwaysRefresh=true + ,refreshOnlyIfNewer=true + ,databaseChangeNotificationType=INVALIDATE + ,coordinationType=SEND_NEW_OBJECTS_WITH_CHANGES) +public @Data @Entity class Login extends AbstractModel { + private static final long serialVersionUID = SerialVersionUID.compute(AbstractModel.class); + + // PRIMARY KEY + @Id @Column @GeneratedValue(generator = "flake-seq") + private Long id; + + // FIELDS + private String username; + private String password; + + public void setPassword(String password) { + try { password = Password.getSaltedHash(password); } + catch (Exception e) { + e.printStackTrace(); + } + } + + public boolean check(String password) { + try { return Password.check(password, this.password); } + catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/example/crud/db/models/Model.java b/src/main/java/com/example/crud/db/models/Model.java new file mode 100644 index 0000000..5652472 --- /dev/null +++ b/src/main/java/com/example/crud/db/models/Model.java @@ -0,0 +1,9 @@ +package com.example.crud.db.models; + +import java.io.Serializable; +import java.util.Date; + +public interface Model { + Date getCreatedDate(); + Date getModifiedDate(); +} diff --git a/src/main/java/com/example/crud/db/models/PhoneNumber.java b/src/main/java/com/example/crud/db/models/PhoneNumber.java new file mode 100644 index 0000000..0f368ca --- /dev/null +++ b/src/main/java/com/example/crud/db/models/PhoneNumber.java @@ -0,0 +1,75 @@ +package com.example.crud.db.models; + +import com.example.crud.util.SerialVersionUID; +import com.fasterxml.jackson.annotation.JsonBackReference; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.eclipse.persistence.annotations.Cache; +import org.eclipse.persistence.annotations.CacheType; +import org.eclipse.persistence.annotations.TimeOfDay; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import java.io.Serializable; + +import javax.persistence.*; + +import static org.eclipse.persistence.annotations.CacheCoordinationType.SEND_NEW_OBJECTS_WITH_CHANGES; +import static org.eclipse.persistence.annotations.DatabaseChangeNotificationType.INVALIDATE; +import static org.eclipse.persistence.config.CacheIsolationType.ISOLATED; + +@ToString(exclude="owner") @EqualsAndHashCode(exclude="owner", callSuper=true) +@EnableJpaAuditing +@EntityListeners(AuditingEntityListener.class) +@Table(name = "PHONE") +@IdClass(PhoneNumber.ID.class) +@Cache(type = CacheType.SOFT_WEAK + ,expiryTimeOfDay=@TimeOfDay(hour=1) + ,disableHits=false + ,isolation=ISOLATED + ,alwaysRefresh=true + ,refreshOnlyIfNewer=true + ,databaseChangeNotificationType=INVALIDATE + ,coordinationType=SEND_NEW_OBJECTS_WITH_CHANGES) +public @Data @Entity class PhoneNumber extends AbstractModel { + private static final long serialVersionUID = SerialVersionUID.compute(PhoneNumber.class); + + @Id @Column(name = "EMP_ID", updatable = false, insertable = false) + private Long id; + @Id @Column(updatable = false) + private String type; + + private String number; + + @ManyToOne @JoinColumn(name = "EMP_ID") + @JsonBackReference + private Employee owner; + + public PhoneNumber() { + } + + public PhoneNumber(String type, String number) { + this(); + setType(type); + setNumber(number); + } + + protected void setOwner(Employee employee) { + this.owner = employee; + if (employee != null) { + this.id = employee.getId(); + } + } + + /** + * Inner-class to manage the compound primary key for phone numbers. + */ + public @Data static class ID implements Serializable { + private static final long serialVersionUID = SerialVersionUID.compute(PhoneNumber.ID.class); + + public Long id; + public String type; + } + +} diff --git a/src/main/java/com/example/crud/db/monitoring/EclipseLinkCache.java b/src/main/java/com/example/crud/db/monitoring/EclipseLinkCache.java new file mode 100644 index 0000000..8afecb0 --- /dev/null +++ b/src/main/java/com/example/crud/db/monitoring/EclipseLinkCache.java @@ -0,0 +1,39 @@ +package com.example.crud.db.monitoring; + +import org.eclipse.persistence.internal.sessions.IdentityMapAccessor; +import org.eclipse.persistence.jpa.JpaEntityManager; +import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl; +import org.eclipse.persistence.sessions.Session; +import org.eclipse.persistence.sessions.server.ServerSession; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; + +public class EclipseLinkCache { + EntityManagerFactory emf = Persistence.createEntityManagerFactory("default"); + EntityManager em = emf.createEntityManager(); + Session session = em.unwrap(Session.class); + JpaEntityManager jem = em.unwrap(JpaEntityManager.class); + UnitOfWorkImpl ouw = jem.unwrap(UnitOfWorkImpl.class); + ServerSession ss = jem.unwrap(ServerSession.class); + IdentityMapAccessor ima = (IdentityMapAccessor) ss.getIdentityMapAccessor(); + + + /** + * long count = countCachedEntitiesL1(clazz); + */ + public long countCachedEntitiesL1(Class clazz) { + return ouw.getCloneMapping().keySet().stream() + .filter(entity -> entity.getClass().equals(clazz)) + .count(); + } + + /** + * int count = countCachedEntitiesL2(clazz); + */ + public int countCachedEntitiesL2(Class clazz) { + return ima.getIdentityMap(clazz).getSize(); + } + +} diff --git a/src/main/java/com/example/crud/db/monitoring/EclipseLinkGaugeSet.java b/src/main/java/com/example/crud/db/monitoring/EclipseLinkGaugeSet.java new file mode 100644 index 0000000..e6ab1ad --- /dev/null +++ b/src/main/java/com/example/crud/db/monitoring/EclipseLinkGaugeSet.java @@ -0,0 +1,118 @@ +package com.example.crud.db.monitoring; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricSet; + +import java.lang.management.ManagementFactory; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * A set of gauges for operating system settings. + */ +public class EclipseLinkGaugeSet implements MetricSet { + + private final Optional level1CacheObjectCount; + private final Optional level1CacheMemorySize; + private final Optional level1CacheHitMissRatio; + private final Optional level2CacheObjectCount; + private final Optional level2CacheHitMissRatio; + + /** + * Creates new gauges using the platform OS bean. + */ + public EclipseLinkGaugeSet() { + this(ManagementFactory.getEclipseLinkMXBean()); + } + + /** + * Creates a new gauges using the given OS bean. + * + * @param mxBean an {@link EclipseLinkMXBean} + */ + public EclipseLinkGaugeSet(EclipseLinkMXBean mxBean) { + this.mxBean = mxBean; + + committedVirtualMemorySize = getMethod("getCommittedVirtualMemorySize"); + totalSwapSpaceSize = getMethod("getTotalSwapSpaceSize"); + freeSwapSpaceSize = getMethod("getFreeSwapSpaceSize"); + processCpuTime = getMethod("getProcessCpuTime"); + freePhysicalMemorySize = getMethod("getFreePhysicalMemorySize"); + totalPhysicalMemorySize = getMethod("getTotalPhysicalMemorySize"); + openFileDescriptorCount = getMethod("getOpenFileDescriptorCount"); + maxFileDescriptorCount = getMethod("getMaxFileDescriptorCount"); + systemCpuLoad = getMethod("getSystemCpuLoad"); + processCpuLoad = getMethod("getProcessCpuLoad"); + } + + + @Override + public Map getMetrics() { + final Map gauges = new HashMap<>(); + + gauges.put("committedVirtualMemorySize", (Gauge) () -> invokeLong(committedVirtualMemorySize)); + gauges.put("totalSwapSpaceSize", (Gauge) () -> invokeLong(totalSwapSpaceSize)); + gauges.put("freeSwapSpaceSize", (Gauge) () -> invokeLong(freeSwapSpaceSize)); + gauges.put("processCpuTime", (Gauge) () -> invokeLong(processCpuTime)); + gauges.put("freePhysicalMemorySize", (Gauge) () -> invokeLong(freePhysicalMemorySize)); + gauges.put("totalPhysicalMemorySize", (Gauge) () -> invokeLong(totalPhysicalMemorySize)); + gauges.put("fd.usage", (Gauge) () -> invokeRatio(openFileDescriptorCount, maxFileDescriptorCount)); + gauges.put("systemCpuLoad", (Gauge) () -> invokeDouble(systemCpuLoad)); + gauges.put("processCpuLoad", (Gauge) () -> invokeDouble(processCpuLoad)); + + return gauges; + } + + private Optional getMethod(String name) { + try { + final Method method = mxBean.getClass().getDeclaredMethod(name); + method.setAccessible(true); + return Optional.of(method); + } catch (NoSuchMethodException e) { + return Optional.empty(); + } + } + + private long invokeLong(Optional method) { + if (method.isPresent()) { + try { + return (long) method.get().invoke(mxBean); + } catch (IllegalAccessException | InvocationTargetException ite) { + return 0L; + } + } + return 0L; + } + + private double invokeDouble(Optional method) { + if (method.isPresent()) { + try { + return (double) method.get().invoke(mxBean); + } catch (IllegalAccessException | InvocationTargetException ite) { + return 0.0; + } + } + return 0.0; + } + + private double invokeRatio(Optional numeratorMethod, Optional denominatorMethod) { + if (numeratorMethod.isPresent() && denominatorMethod.isPresent()) { + try { + long numerator = (long) numeratorMethod.get().invoke(mxBean); + long denominator = (long) denominatorMethod.get().invoke(mxBean); + if (0 == denominator) { + return Double.NaN; + } + return 1.0 * numerator / denominator; + } catch (IllegalAccessException | InvocationTargetException ite) { + return Double.NaN; + } + } + return Double.NaN; + } + +} diff --git a/src/main/java/com/example/crud/db/sequence/FlakeSequence.java b/src/main/java/com/example/crud/db/sequence/FlakeSequence.java new file mode 100644 index 0000000..7fd40bb --- /dev/null +++ b/src/main/java/com/example/crud/db/sequence/FlakeSequence.java @@ -0,0 +1,77 @@ +package com.example.crud.db.sequence; + +import com.example.crud.crypto.Randomness; +import com.github.rholder.fauxflake.DefaultIdGenerator; +import com.github.rholder.fauxflake.api.IdGenerator; +import com.github.rholder.fauxflake.provider.SystemTimeProvider; +import com.github.rholder.fauxflake.provider.twitter.SnowflakeEncodingProvider; +import org.eclipse.persistence.internal.databaseaccess.Accessor; +import org.eclipse.persistence.internal.sessions.AbstractSession; +import org.eclipse.persistence.sequencing.Sequence; + +import java.util.Vector; + +/** + * FlakeSequence is a decentralized, k-ordered unique ID generator that produces 64bit integers (Long). + * + * This is configured to mimic Twitter's Snowflake pattern. "k-ordered" means that they sort in timestamp + * order based on when they were created within the level of precision available and of course with all the + * standard NTP-based cavaets. + */ + +public class FlakeSequence extends Sequence { + IdGenerator idGenerator; + + public FlakeSequence(String name) { + super(name); + long mid = Randomness.randomIntSecure(1024); // TODO(gburd): use a hash of the hostname? + idGenerator = new DefaultIdGenerator(new SystemTimeProvider(), new SnowflakeEncodingProvider(mid)); + } + + @Override + public Object getGeneratedValue(Accessor accessor, AbstractSession writeSession, String seqName) { + while (true) { + try { + return idGenerator.generateId(10).asLong(); + } + catch (InterruptedException e) { + // We waited more than 10ms to generate an Id, try again. This could be due to NTP + // drift, leap seconds, GC pause, who knows. + } + } + } + + @Override + public Vector getGeneratedVector(Accessor accessor, AbstractSession writeSession, String seqName, int size) { + return null; + } + + @Override + public void onConnect() { + } + + @Override + public void onDisconnect() { + } + + @Override + public boolean shouldAcquireValueAfterInsert() { + return false; + } + + public boolean shouldAlwaysOverrideExistingValue(String seqName, Object existingValue) { + return ((String) existingValue).isEmpty(); + } + + @Override + public boolean shouldUseTransaction() { + return false; + } + + @Override + public boolean shouldUsePreallocation() { + // NOTE: never pre-allocate, that would defeat the time-ordered nature of these IDs + return false; + } + +} diff --git a/src/main/java/com/example/crud/db/sequence/PSRNSequence.java b/src/main/java/com/example/crud/db/sequence/PSRNSequence.java new file mode 100644 index 0000000..3f97df6 --- /dev/null +++ b/src/main/java/com/example/crud/db/sequence/PSRNSequence.java @@ -0,0 +1,51 @@ +package com.example.crud.db.sequence; + +import com.example.crud.crypto.Randomness; +import org.eclipse.persistence.internal.databaseaccess.Accessor; +import org.eclipse.persistence.internal.sessions.AbstractSession; +import org.eclipse.persistence.sequencing.Sequence; + +import java.util.Vector; + +public class PSRNSequence extends Sequence { + + public PSRNSequence(String name) { + super(name); + } + + @Override + public Object getGeneratedValue(Accessor accessor, AbstractSession writeSession, String seqName) { + return Randomness.randomHexStringSecure(36); + } + + @Override + public Vector getGeneratedVector(Accessor accessor, AbstractSession writeSession, String seqName, int size) { + return null; + } + + @Override + public void onConnect() { } + + @Override + public void onDisconnect() { } + + @Override + public boolean shouldAcquireValueAfterInsert() { + return false; + } + + public boolean shouldAlwaysOverrideExistingValue(String seqName, Object existingValue) { + return ((String) existingValue).isEmpty(); + } + + @Override + public boolean shouldUseTransaction() { + return false; + } + + @Override + public boolean shouldUsePreallocation() { + return false; + } + +} diff --git a/src/main/java/com/example/crud/db/sequence/UUIDSequence.java b/src/main/java/com/example/crud/db/sequence/UUIDSequence.java new file mode 100644 index 0000000..15ddd9c --- /dev/null +++ b/src/main/java/com/example/crud/db/sequence/UUIDSequence.java @@ -0,0 +1,55 @@ +package com.example.crud.db.sequence; + +import java.util.UUID; +import java.util.Vector; + +import org.eclipse.persistence.config.SessionCustomizer; +import org.eclipse.persistence.internal.databaseaccess.Accessor; +import org.eclipse.persistence.internal.sessions.AbstractSession; +import org.eclipse.persistence.sequencing.Sequence; +import org.eclipse.persistence.sessions.Session; + +public class UUIDSequence extends Sequence { + + public UUIDSequence(String name) { + super(name); + } + + @Override + public Object getGeneratedValue(Accessor accessor, AbstractSession writeSession, String seqName) { + return UUID.randomUUID().toString().toUpperCase(); + } + + @Override + public Vector getGeneratedVector(Accessor accessor, AbstractSession writeSession, String seqName, int size) { + return null; + } + + @Override + public void onConnect() { + } + + @Override + public void onDisconnect() { + } + + @Override + public boolean shouldAcquireValueAfterInsert() { + return false; + } + + public boolean shouldAlwaysOverrideExistingValue(String seqName, Object existingValue) { + return ((String) existingValue).isEmpty(); + } + + @Override + public boolean shouldUseTransaction() { + return false; + } + + @Override + public boolean shouldUsePreallocation() { + return false; + } + +} diff --git a/src/main/java/com/example/crud/db/util/AuditingConfiguration.java b/src/main/java/com/example/crud/db/util/AuditingConfiguration.java new file mode 100644 index 0000000..06d578e --- /dev/null +++ b/src/main/java/com/example/crud/db/util/AuditingConfiguration.java @@ -0,0 +1,34 @@ +package com.example.crud.db.util; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; + +@EnableAsync +@SpringBootApplication +@EnableJpaAuditing +class AuditingConfiguration { + + @Bean + public AuditorAware createAuditorProvider() { + return new SecurityAuditor(); + } + + @Bean + public AuditingEntityListener createAuditingListener() { + return new AuditingEntityListener(); + } + + public static class SecurityAuditor implements AuditorAware { + @Override + public String getCurrentAuditor() { + //Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + //String username = auth.getName(); + //return username; + return "joeblow"; + } + } +} diff --git a/src/main/java/com/example/crud/db/util/EclipseLinkSessionCustomizer.java b/src/main/java/com/example/crud/db/util/EclipseLinkSessionCustomizer.java new file mode 100644 index 0000000..13a727b --- /dev/null +++ b/src/main/java/com/example/crud/db/util/EclipseLinkSessionCustomizer.java @@ -0,0 +1,57 @@ +package com.example.crud.db.util; + +import com.example.crud.db.sequence.FlakeSequence; +import com.example.crud.db.sequence.PSRNSequence; +import com.example.crud.db.sequence.UUIDSequence; +import com.google.common.collect.Lists; +import org.eclipse.persistence.config.SessionCustomizer; +import org.eclipse.persistence.descriptors.ClassDescriptor; +import org.eclipse.persistence.internal.sessions.AbstractSession; +import org.eclipse.persistence.sessions.Session; +import org.eclipse.persistence.tools.schemaframework.IndexDefinition; + +import java.sql.SQLException; + +public class EclipseLinkSessionCustomizer implements SessionCustomizer { + + @Override + public void customize(Session session) throws SQLException { + + // Enable concurrent processing of result sets and concurrent loading of load groups. + ((AbstractSession) session).setIsConcurrent(true); + + // Add sequence generators. + Lists.newArrayList( + new UUIDSequence("uuid-seq"), + new PSRNSequence("psrn-seq"), + new FlakeSequence("flake-seq")) + .forEach(sequence -> { + session.getLogin().addSequence(sequence); + }); + + // Convert class names to table names by adding '_' characters when case changes. + for (ClassDescriptor descriptor : session.getDescriptors().values()) { + // Only change the table name for non-embeddable entities with no @Table already + if (!descriptor.getTables().isEmpty() && + descriptor.getAlias().equalsIgnoreCase(descriptor.getTableName())) { + String tableName = convertCaseToUnderscores(descriptor.getTableName()); + descriptor.setTableName(tableName); + for (IndexDefinition index : descriptor.getTables().get(0).getIndexes()) { + index.setTargetTable(tableName); + } + } + } + } + + private static String convertCaseToUnderscores(String name) { + StringBuffer buf = new StringBuffer(name.replace('.', '_')); + for (int i = 1; i < buf.length() - 1; i++) { + if (Character.isLowerCase(buf.charAt(i - 1)) && + Character.isUpperCase(buf.charAt(i)) && + Character.isLowerCase(buf.charAt(i + 1))) { + buf.insert(i++, '_'); + } + } + return buf.toString().toLowerCase(); + } +} diff --git a/src/main/java/com/example/crud/metrics/CoalescingReporter.java b/src/main/java/com/example/crud/metrics/CoalescingReporter.java new file mode 100644 index 0000000..dfbdc7a --- /dev/null +++ b/src/main/java/com/example/crud/metrics/CoalescingReporter.java @@ -0,0 +1,229 @@ +package com.example.crud.metrics; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricFilter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.ScheduledReporter; +import com.codahale.metrics.Snapshot; +import com.codahale.metrics.Timer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; + +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.concurrent.TimeUnit; + +/** + * A reporter class for logging metrics values to a {@link Logger} periodically, similar to + * {@link com.codahale.metrics.ConsoleReporter} or {@link com.codahale.metrics.CsvReporter}, but using + * the logging framework instead. It also supports specifying a {@link Marker} instance that can be used + * by custom appenders and filters for the bound logging toolkit to further process metrics reports. + */ +public final class CoalescingReporter extends ScheduledReporter { + + private final Logger logger; + private final Marker marker; + + /** + * Returns a new {@link Builder} for {@link CoalescingReporter}. + * + * @param registry the registry to report + * @return a {@link Builder} instance for a {@link CoalescingReporter} + */ + public static Builder forRegistry(MetricRegistry registry) { + return new Builder(registry); + } + + private CoalescingReporter(MetricRegistry registry, + Logger logger, + Marker marker, + TimeUnit rateUnit, + TimeUnit durationUnit, + MetricFilter filter) { + super(registry, "logger-reporter", filter, rateUnit, durationUnit); + this.logger = logger; + this.marker = marker; + } + + @Override + public void report(SortedMap gauges, + SortedMap counters, + SortedMap histograms, + SortedMap meters, + SortedMap timers) { + StringBuilder data = new StringBuilder(); + for (Entry entry : gauges.entrySet()) { + addGauge(data, entry.getKey(), entry.getValue()); + } + + for (Entry entry : counters.entrySet()) { + addCounter(data, entry.getKey(), entry.getValue()); + } + + for (Entry entry : histograms.entrySet()) { + addHistogram(data, entry.getKey(), entry.getValue()); + } + + for (Entry entry : meters.entrySet()) { + addMeter(data, entry.getKey(), entry.getValue()); + } + + for (Entry entry : timers.entrySet()) { + addTimer(data, entry.getKey(), entry.getValue()); + } + logger.info(marker, data.toString()); + } + + private void addTimer(StringBuilder data, String name, Timer timer) { + final Snapshot snapshot = timer.getSnapshot(); + data.append(" type=timer.").append(name).append(":"); + data.append(" count=").append(timer.getCount()); + data.append(", min=").append(convertDuration(snapshot.getMin())); + data.append(", max=").append(convertDuration(snapshot.getMax())); + data.append(", mean=").append(convertDuration(snapshot.getMean())); + data.append(", stdDev=").append(convertDuration(snapshot.getStdDev())); + data.append(", median=").append(convertDuration(snapshot.getMedian())); + data.append(", p75=").append(convertDuration(snapshot.get75thPercentile())); + data.append(", p95=").append(convertDuration(snapshot.get95thPercentile())); + data.append(", p98=").append(convertDuration(snapshot.get98thPercentile())); + data.append(", p99=").append(convertDuration(snapshot.get99thPercentile())); + data.append(", 999=").append(convertDuration(snapshot.get999thPercentile())); + data.append(", mean_rate=").append(convertRate(timer.getMeanRate())); + data.append(", m1=").append(convertRate(timer.getMeanRate())); + data.append(", m5=").append(convertRate(timer.getMeanRate())); + data.append(", m15=").append(convertRate(timer.getMeanRate())); + data.append(", rate_unit=").append(getRateUnit()); + data.append(", duration_unit=").append(getDurationUnit()); + } + + private void addMeter(StringBuilder data, String name, Meter meter) { + data.append(" type=meter.").append(name).append(":"); + data.append(" count=").append(meter.getCount()); + data.append(", mean_rate=").append(convertRate(meter.getMeanRate())); + data.append(", m1=").append(convertRate(meter.getOneMinuteRate())); + data.append(", m5=").append(convertRate(meter.getFiveMinuteRate())); + data.append(", m15=").append(convertRate(meter.getFifteenMinuteRate())); + data.append(", rate_unit=").append(getRateUnit()); + } + + private void addHistogram(StringBuilder data, String name, Histogram histogram) { + final Snapshot snapshot = histogram.getSnapshot(); + data.append(" type=histogram.").append(name).append(":"); + data.append(" count=").append(histogram.getCount()); + data.append(", min=").append(snapshot.getMin()); + data.append(", max=").append(snapshot.getMax()); + data.append(", mean=").append(snapshot.getMean()); + data.append(", stdDev=").append(snapshot.getStdDev()); + data.append(", median=").append(snapshot.getMedian()); + data.append(", p75=").append(snapshot.get75thPercentile()); + data.append(", p95=").append(snapshot.get95thPercentile()); + data.append(", p98=").append(snapshot.get98thPercentile()); + data.append(", p99=").append(snapshot.get99thPercentile()); + data.append(", 999=").append(snapshot.get999thPercentile()); + } + + private void addCounter(StringBuilder data, String name, Counter counter) { + data.append(" counter.").append(name).append(": ").append(counter.getCount()); + } + + private void addGauge(StringBuilder data, String name, Gauge gauge) { + data.append(" gauge.").append(name).append(": ").append(gauge.getValue()); + } + + @Override + protected String getRateUnit() { + return "events/" + super.getRateUnit(); + } + + /** + * A builder for {@link com.codahale.metrics.CsvReporter} instances. Defaults to logging to {@code metrics}, not + * using a marker, converting rates to events/second, converting durations to milliseconds, and + * not filtering metrics. + */ + public static final class Builder { + private final MetricRegistry registry; + private Logger logger; + private Marker marker; + private TimeUnit rateUnit; + private TimeUnit durationUnit; + private MetricFilter filter; + + private Builder(MetricRegistry registry) { + this.registry = registry; + this.logger = LoggerFactory.getLogger("metrics"); + this.marker = null; + this.rateUnit = TimeUnit.SECONDS; + this.durationUnit = TimeUnit.MILLISECONDS; + this.filter = MetricFilter.ALL; + } + + /** + * Log metrics to the given logger. + * + * @param logger a {@link Logger} + * @return {@code this} + */ + public Builder outputTo(Logger logger) { + this.logger = logger; + return this; + } + + /** + * Mark all logged metrics with the given marker. + * + * @param marker a {@link Marker} + * @return {@code this} + */ + public Builder markWith(Marker marker) { + this.marker = marker; + return this; + } + + /** + * Convert rates to the given time unit. + * + * @param rateUnit a unit of time + * @return {@code this} + */ + public Builder convertRatesTo(TimeUnit rateUnit) { + this.rateUnit = rateUnit; + return this; + } + + /** + * Convert durations to the given time unit. + * + * @param durationUnit a unit of time + * @return {@code this} + */ + public Builder convertDurationsTo(TimeUnit durationUnit) { + this.durationUnit = durationUnit; + return this; + } + + /** + * Only report metrics which match the given filter. + * + * @param filter a {@link MetricFilter} + * @return {@code this} + */ + public Builder filter(MetricFilter filter) { + this.filter = filter; + return this; + } + + /** + * Builds a {@link CoalescingReporter} with the given properties. + * + * @return a {@link CoalescingReporter} + */ + public CoalescingReporter build() { + return new CoalescingReporter(registry, logger, marker, rateUnit, durationUnit, filter); + } + } + +} diff --git a/src/main/java/com/example/crud/metrics/CoalescingReporterElementParser.java b/src/main/java/com/example/crud/metrics/CoalescingReporterElementParser.java new file mode 100644 index 0000000..fdf136d --- /dev/null +++ b/src/main/java/com/example/crud/metrics/CoalescingReporterElementParser.java @@ -0,0 +1,38 @@ +package com.example.crud.metrics; + +import com.ryantenney.metrics.spring.reporter.AbstractReporterElementParser; + +/** + * Reporter for metrics-spring which logs more compact, all in one line instead of one line for each metric. + */ +public class CoalescingReporterElementParser extends AbstractReporterElementParser { + + private static final String FILTER_REF = "filter-ref"; + private static final String FILTER_PATTERN = "filter"; + + @Override + public String getType() { + return "compact-slf4j"; + } + + @Override + protected Class getBeanClass() { + return CoalescingReporterFactoryBean.class; + } + + @Override + protected void validate(ValidationContext c) { + c.require(CoalescingReporterFactoryBean.PERIOD, DURATION_STRING_REGEX, "Period is required and must be in the form '\\d+(ns|us|ms|s|m|h|d)'"); + c.optional(CoalescingReporterFactoryBean.MARKER); + c.optional(CoalescingReporterFactoryBean.LOGGER); + c.optional(CoalescingReporterFactoryBean.RATE_UNIT, TIMEUNIT_STRING_REGEX, "Rate unit must be one of the enum constants from java.util.concurrent.TimeUnit"); + c.optional(CoalescingReporterFactoryBean.DURATION_UNIT, TIMEUNIT_STRING_REGEX, "Duration unit must be one of the enum constants from java.util.concurrent.TimeUnit"); + c.optional(FILTER_PATTERN); + c.optional(FILTER_REF); + if (c.has(FILTER_PATTERN) && c.has(FILTER_REF)) { + c.reject(FILTER_REF, "Reporter element must not specify both the 'filter' and 'filter-ref' attributes"); + } + c.rejectUnmatchedProperties(); + } + +} diff --git a/src/main/java/com/example/crud/metrics/CoalescingReporterFactoryBean.java b/src/main/java/com/example/crud/metrics/CoalescingReporterFactoryBean.java new file mode 100644 index 0000000..7d31e1a --- /dev/null +++ b/src/main/java/com/example/crud/metrics/CoalescingReporterFactoryBean.java @@ -0,0 +1,54 @@ +package com.example.crud.metrics; + +import com.ryantenney.metrics.spring.reporter.AbstractScheduledReporterFactoryBean; +import org.slf4j.LoggerFactory; +import org.slf4j.MarkerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * CoalescingReporterFactoryBean. + */ +public class CoalescingReporterFactoryBean extends AbstractScheduledReporterFactoryBean { + + /** Period attribute. */ + public static final String PERIOD = "period"; + /** Duration unit. */ + public static final String DURATION_UNIT = "duration-unit"; + /** Rate unit. */ + public static final String RATE_UNIT = "rate-unit"; + /** Marker. */ + public static final String MARKER = "marker"; + /** Logger. */ + public static final String LOGGER = "logger"; + + @Override + public Class getObjectType() { + return CoalescingReporter.class; + } + + @Override + protected CoalescingReporter createInstance() { + final CoalescingReporter.Builder reporter = CoalescingReporter.forRegistry(getMetricRegistry()); + if (hasProperty(DURATION_UNIT)) { + reporter.convertDurationsTo(getProperty(DURATION_UNIT, TimeUnit.class)); + } + if (hasProperty(RATE_UNIT)) { + reporter.convertRatesTo(getProperty(RATE_UNIT, TimeUnit.class)); + } + reporter.filter(getMetricFilter()); + if (hasProperty(MARKER)) { + reporter.markWith(MarkerFactory.getMarker(getProperty(MARKER))); + } + if (hasProperty(LOGGER)) { + reporter.outputTo(LoggerFactory.getLogger(getProperty(LOGGER))); + } + return reporter.build(); + } + + @Override + protected long getPeriod() { + return convertDurationString(getProperty(PERIOD)); + } + +} diff --git a/src/main/java/com/example/crud/metrics/MetricsConfiguration.java b/src/main/java/com/example/crud/metrics/MetricsConfiguration.java new file mode 100644 index 0000000..a80abd8 --- /dev/null +++ b/src/main/java/com/example/crud/metrics/MetricsConfiguration.java @@ -0,0 +1,179 @@ +package com.example.crud.metrics; + +import com.codahale.metrics.JmxReporter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.graphite.Graphite; +import com.codahale.metrics.graphite.GraphiteReporter; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.codahale.metrics.jvm.*; +import com.codahale.metrics.riemann.Riemann; +import com.codahale.metrics.riemann.RiemannReporter; +import com.example.crud.Constants; +import com.ryantenney.metrics.spring.config.annotation.EnableMetrics; +import com.ryantenney.metrics.spring.config.annotation.MetricsConfigurerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +@Configuration +@EnableMetrics(proxyTargetClass = true) +@Profile("!" + Constants.SPRING_PROFILE_FAST) +public class MetricsConfiguration extends MetricsConfigurerAdapter implements EnvironmentAware { + + private static final String ENV_METRICS = "metrics."; + private static final String ENV_METRICS_GRAPHITE = "metrics.graphite."; + private static final String ENV_METRICS_RIEMANN = "metrics.riemann."; + private static final String PROP_JMX_ENABLED = "jmx.enabled"; + private static final String PROP_RIEMANN_ENABLED = "enabled"; + private static final String PROP_GRAPHITE_ENABLED = "enabled"; + private static final String PROP_GRAPHITE_PREFIX = ""; + private static final String PROP_RIEMANN_PREFIX = ""; + + private static final String PROP_PORT = "port"; + private static final String PROP_HOST = "host"; + private static final String PROP_METRIC_REG_JVM_MEMORY = "jvm.memory"; + private static final String PROP_METRIC_REG_JVM_GARBAGE = "jvm.garbage"; + private static final String PROP_METRIC_REG_JVM_THREADS = "jvm.threads"; + private static final String PROP_METRIC_REG_JVM_FILES = "jvm.files"; + private static final String PROP_METRIC_REG_JVM_BUFFERS = "jvm.buffers"; + + private final Logger log = LoggerFactory.getLogger(MetricsConfiguration.class); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private HealthCheckRegistry healthCheckRegistry = new HealthCheckRegistry(); + + private RelaxedPropertyResolver propertyResolver; + + @Override + public void setEnvironment(Environment environment) { + this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_METRICS); + } + + @Override + @Bean + public MetricRegistry getMetricRegistry() { + return metricRegistry; + } + + @Override + @Bean + public HealthCheckRegistry getHealthCheckRegistry() { + return healthCheckRegistry; + } + + @PostConstruct + public void init() { + log.debug("Registering JVM gauges"); + metricRegistry.registerAll(new OperatingSystemGaugeSet()); + metricRegistry.register(PROP_METRIC_REG_JVM_MEMORY, new MemoryUsageGaugeSet()); + metricRegistry.register(PROP_METRIC_REG_JVM_GARBAGE, new GarbageCollectorMetricSet()); + metricRegistry.register(PROP_METRIC_REG_JVM_THREADS, new ThreadStatesGaugeSet()); + metricRegistry.register(PROP_METRIC_REG_JVM_FILES, new FileDescriptorRatioGauge()); + metricRegistry.register(PROP_METRIC_REG_JVM_BUFFERS, new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer())); + if (propertyResolver.getProperty(PROP_JMX_ENABLED, Boolean.class, false)) { + log.info("Initializing Metrics JMX reporting"); + JmxReporter jmxReporter = JmxReporter.forRegistry(metricRegistry).build(); + jmxReporter.start(); + } + CoalescingReporter reporter = CoalescingReporter.forRegistry(metricRegistry) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build(); + reporter.start(1, TimeUnit.MINUTES); + } + + @Configuration + @ConditionalOnClass(Graphite.class) + @Profile("!" + Constants.SPRING_PROFILE_FAST) + public static class GraphiteRegistry implements EnvironmentAware { + + private final Logger log = LoggerFactory.getLogger(GraphiteRegistry.class); + + @Inject + private MetricRegistry metricRegistry; + + private RelaxedPropertyResolver propertyResolver; + + @Override + public void setEnvironment(Environment environment) { + this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_METRICS_GRAPHITE); + } + + @PostConstruct + private void init() { + //TODO(gburd): why isn't this picking up the application property? + Boolean enabled = propertyResolver.getProperty(PROP_GRAPHITE_ENABLED, Boolean.class, false); + if (enabled) { + log.info("Initializing Metrics Graphite reporting"); + String host = propertyResolver.getRequiredProperty(PROP_HOST); + Integer port = propertyResolver.getRequiredProperty(PROP_PORT, Integer.class); + String prefix = propertyResolver.getProperty(PROP_GRAPHITE_PREFIX, String.class, ""); + + Graphite graphite = new Graphite(new InetSocketAddress(host, port)); + GraphiteReporter reporter = GraphiteReporter.forRegistry(metricRegistry) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .prefixedWith(prefix) + .build(graphite); + reporter.start(5, TimeUnit.MINUTES); + } + } + } + + + @Configuration + @ConditionalOnClass(Riemann.class) + @Profile("!" + Constants.SPRING_PROFILE_FAST) + public static class RiemannRegistry implements EnvironmentAware { + + private final Logger log = LoggerFactory.getLogger(RiemannRegistry.class); + + @Inject + private MetricRegistry metricRegistry; + + private RelaxedPropertyResolver propertyResolver; + + @Override + public void setEnvironment(Environment environment) { + this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_METRICS_RIEMANN); + } + + @PostConstruct + private void init() { + //TODO(gburd): why isn't this picking up the application property? + Boolean enabled = propertyResolver.getProperty(PROP_RIEMANN_ENABLED, Boolean.class, false); + if (enabled) { + log.info("Initializing Metrics Riemann reporting"); + String host = propertyResolver.getRequiredProperty(PROP_HOST); + Integer port = propertyResolver.getRequiredProperty(PROP_PORT, Integer.class); + String prefix = propertyResolver.getProperty(PROP_RIEMANN_PREFIX, String.class, ""); + try { + Riemann riemann = new Riemann(host, port); + RiemannReporter reporter = RiemannReporter.forRegistry(metricRegistry) + .convertRatesTo(TimeUnit.MILLISECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .prefixedWith(prefix) + .build(riemann); + reporter.start(1, TimeUnit.SECONDS); + } catch (IOException e) { + log.error(e.toString()); + } + } + } + } + +} diff --git a/src/main/java/com/example/crud/metrics/OperatingSystemGaugeSet.java b/src/main/java/com/example/crud/metrics/OperatingSystemGaugeSet.java new file mode 100644 index 0000000..8149876 --- /dev/null +++ b/src/main/java/com/example/crud/metrics/OperatingSystemGaugeSet.java @@ -0,0 +1,125 @@ +package com.example.crud.metrics; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricSet; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * A set of gauges for operating system settings. + */ +public class OperatingSystemGaugeSet implements MetricSet { + + private final OperatingSystemMXBean mxBean; + private final Optional committedVirtualMemorySize; + private final Optional totalSwapSpaceSize; + private final Optional freeSwapSpaceSize; + private final Optional processCpuTime; + private final Optional freePhysicalMemorySize; + private final Optional totalPhysicalMemorySize; + private final Optional openFileDescriptorCount; + private final Optional maxFileDescriptorCount; + private final Optional systemCpuLoad; + private final Optional processCpuLoad; + + /** + * Creates new gauges using the platform OS bean. + */ + public OperatingSystemGaugeSet() { + this(ManagementFactory.getOperatingSystemMXBean()); + } + + /** + * Creates a new gauges using the given OS bean. + * + * @param mxBean an {@link OperatingSystemMXBean} + */ + public OperatingSystemGaugeSet(OperatingSystemMXBean mxBean) { + this.mxBean = mxBean; + + committedVirtualMemorySize = getMethod("getCommittedVirtualMemorySize"); + totalSwapSpaceSize = getMethod("getTotalSwapSpaceSize"); + freeSwapSpaceSize = getMethod("getFreeSwapSpaceSize"); + processCpuTime = getMethod("getProcessCpuTime"); + freePhysicalMemorySize = getMethod("getFreePhysicalMemorySize"); + totalPhysicalMemorySize = getMethod("getTotalPhysicalMemorySize"); + openFileDescriptorCount = getMethod("getOpenFileDescriptorCount"); + maxFileDescriptorCount = getMethod("getMaxFileDescriptorCount"); + systemCpuLoad = getMethod("getSystemCpuLoad"); + processCpuLoad = getMethod("getProcessCpuLoad"); + } + + + @Override + public Map getMetrics() { + final Map gauges = new HashMap<>(); + + gauges.put("committedVirtualMemorySize", (Gauge) () -> invokeLong(committedVirtualMemorySize)); + gauges.put("totalSwapSpaceSize", (Gauge) () -> invokeLong(totalSwapSpaceSize)); + gauges.put("freeSwapSpaceSize", (Gauge) () -> invokeLong(freeSwapSpaceSize)); + gauges.put("processCpuTime", (Gauge) () -> invokeLong(processCpuTime)); + gauges.put("freePhysicalMemorySize", (Gauge) () -> invokeLong(freePhysicalMemorySize)); + gauges.put("totalPhysicalMemorySize", (Gauge) () -> invokeLong(totalPhysicalMemorySize)); + gauges.put("fd.usage", (Gauge) () -> invokeRatio(openFileDescriptorCount, maxFileDescriptorCount)); + gauges.put("systemCpuLoad", (Gauge) () -> invokeDouble(systemCpuLoad)); + gauges.put("processCpuLoad", (Gauge) () -> invokeDouble(processCpuLoad)); + + return gauges; + } + + private Optional getMethod(String name) { + try { + final Method method = mxBean.getClass().getDeclaredMethod(name); + method.setAccessible(true); + return Optional.of(method); + } catch (NoSuchMethodException e) { + return Optional.empty(); + } + } + + private long invokeLong(Optional method) { + if (method.isPresent()) { + try { + return (long) method.get().invoke(mxBean); + } catch (IllegalAccessException | InvocationTargetException ite) { + return 0L; + } + } + return 0L; + } + + private double invokeDouble(Optional method) { + if (method.isPresent()) { + try { + return (double) method.get().invoke(mxBean); + } catch (IllegalAccessException | InvocationTargetException ite) { + return 0.0; + } + } + return 0.0; + } + + private double invokeRatio(Optional numeratorMethod, Optional denominatorMethod) { + if (numeratorMethod.isPresent() && denominatorMethod.isPresent()) { + try { + long numerator = (long) numeratorMethod.get().invoke(mxBean); + long denominator = (long) denominatorMethod.get().invoke(mxBean); + if (0 == denominator) { + return Double.NaN; + } + return 1.0 * numerator / denominator; + } catch (IllegalAccessException | InvocationTargetException ite) { + return Double.NaN; + } + } + return Double.NaN; + } + +} diff --git a/src/main/java/com/example/crud/metrics/SpringConfiguringClass.java b/src/main/java/com/example/crud/metrics/SpringConfiguringClass.java new file mode 100644 index 0000000..ff84ff4 --- /dev/null +++ b/src/main/java/com/example/crud/metrics/SpringConfiguringClass.java @@ -0,0 +1,22 @@ +package com.example.crud.metrics; + +import java.util.concurrent.TimeUnit; +import org.springframework.context.annotation.Configuration; +import com.codahale.metrics.ConsoleReporter; +import com.codahale.metrics.MetricRegistry; +import com.ryantenney.metrics.spring.config.annotation.EnableMetrics; +import com.ryantenney.metrics.spring.config.annotation.MetricsConfigurerAdapter; + +@Configuration +@EnableMetrics +public class SpringConfiguringClass extends MetricsConfigurerAdapter { + + @Override + public void configureReporters(MetricRegistry metricRegistry) { + // registerReporter allows the MetricsConfigurerAdapter to + // shut down the reporter when the Spring context is closed + registerReporter(ConsoleReporter.forRegistry(metricRegistry).build()) + .start(1, TimeUnit.MINUTES); + } + +} diff --git a/src/main/java/com/example/crud/services/EmployeeService.java b/src/main/java/com/example/crud/services/EmployeeService.java new file mode 100644 index 0000000..deb58f3 --- /dev/null +++ b/src/main/java/com/example/crud/services/EmployeeService.java @@ -0,0 +1,45 @@ +package com.example.crud.services; + +import com.example.crud.db.models.Employee; +import com.example.crud.db.dao.EmployeeRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@EnableJpaRepositories("com.example.crud.db.dao") +public class EmployeeService { + private static final Logger log = LoggerFactory.getLogger(EmployeeService.class); + + @Autowired private EmployeeRepository repository; + + public List findAll() { + return repository.findAll(); + } + + public Employee saveEmployee(Employee employee) { + return repository.saveAndFlush(employee); + } + + public Employee getOne(Long id) { + return repository.getOne(id); + } + + public List findByLastName(String name) { + return repository.findByLastName(name); + } + + public void delete(Long id) { + repository.delete(id); + } +/* + public List fetchByLastNameLength(Long length) { + return repository.fetchByLastNameLength(length); + } +*/ + +} diff --git a/src/main/java/com/example/crud/util/ReflectionUtils.java b/src/main/java/com/example/crud/util/ReflectionUtils.java new file mode 100644 index 0000000..8dd6e3b --- /dev/null +++ b/src/main/java/com/example/crud/util/ReflectionUtils.java @@ -0,0 +1,28 @@ +package com.example.crud.util; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +public class ReflectionUtils { + + public static T getAnnotation(ProceedingJoinPoint pjp, Class annotationClass) throws NoSuchMethodException { + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + T annotation = AnnotationUtils.findAnnotation(method, annotationClass); + + if (annotation != null) { + return annotation; + } + + Class[] argClasses = new Class[pjp.getArgs().length]; + for (int i = 0; i < pjp.getArgs().length; i++) { + argClasses[i] = pjp.getArgs()[i].getClass(); + } + method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argClasses); + return AnnotationUtils.findAnnotation(method, annotationClass); + } +} diff --git a/src/main/java/com/example/crud/util/SerialVersionUID.java b/src/main/java/com/example/crud/util/SerialVersionUID.java new file mode 100644 index 0000000..6feed84 --- /dev/null +++ b/src/main/java/com/example/crud/util/SerialVersionUID.java @@ -0,0 +1,104 @@ +package com.example.crud.util; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class SerialVersionUID { + + /** + * Compute a UID for the serialization version of a class. + * + * The Java object serialization standard defines an algorithm for computing the default serialVersionUID of a + * class: http://docs.oracle.com/javase/6/docs/platform/serialization/spec/class.html#4100. This method computes a + * serialVersionUID value models classes. For example, the com.example.models.User class should specify the following: + * + *

+     *   private static final long serialVersionUID = SerialVersionUID.computeUID(User.class);
+     * 
+ * + * Java provides a way to access and possibly compute a default value for serialVersionUID using + * java.io.ObjectStreamClass. If a class has no serialVersionID, the + * {@link java.io.ObjectStreamClass#getSerialVersionUID()} computes a default value according to the algorithm + * defined by the object serialization standard. However, we do not use this method because there is no clean way to + * access the private method computeDefaultSUID that performs the computation. + * + * Therefore, this method is based on, but different from the implementation of + * java.io.ObjectStreamClass#computeDefaultSIUD(Class). This implementation does not factor various elements of + * the default computation into the resulting hash because the goal is to characterize versions of the class which + * are compatible for serialization and deserialization of their fields even if method signatures change. We don't + * want the addition of a method or a constructor to change the computed value. Therefore, this method uses only + * some of the internal logic of the default algorithm: it implements steps 1, 4, 8 and 9 of the standard algorithm + * and omits steps 2, 3, 5, 6 and 8. + * + * Like the standard algorithm, this implementation writes various elements of the class definition to a + * DataOutputStream and then computes a SHA-1 digest of the stream and returns a hash based on the digest. Unlike + * the standard algorithm, this implementation does not include interface, method and constructor signatures. + * + * @param clazz + * The class + * @return A version UID. + */ + public static long compute(final Class clazz) { + try { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(bytesOut); + + // #1 + dataOut.writeUTF(clazz.getName()); + + // #4 + List sorted = Arrays.asList(clazz.getDeclaredFields()); + sorted.sort(new Comparator() { + + @Override + public int compare(Field field1, Field field2) { + return field1.getName().compareTo(field2.getName()); + } + + }); + for (final Field field : sorted) { + int mods = field.getModifiers() & + (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED | + Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE | + Modifier.TRANSIENT); + if (((mods & Modifier.PRIVATE) == 0) || + ((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0)) + // Don't include private static or + // private transient fields + { + dataOut.writeUTF(field.getName()); + dataOut.writeInt(mods); + dataOut.writeUTF(field.getType().getName()); + } + } + dataOut.flush(); + + // #8 + MessageDigest md = MessageDigest.getInstance("SHA"); + byte[] hashBytes = md.digest(bytesOut.toByteArray()); + + // #9 + long hash = 0L; + for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) { + hash = (hash << 8) | (hashBytes[i] & 0xFF); + } + return hash; + + } + catch (IOException ex) { + throw new InternalError(ex); + } + catch (NoSuchAlgorithmException ex) { + throw new SecurityException(ex.getMessage()); + } + } + +} diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..fe72e2e --- /dev/null +++ b/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,79 @@ + + + + + + + org.eclipse.persistence.jpa.PersistenceProvider + true + com.example.crud.db.models.Address + com.example.crud.db.models.Employee + com.example.crud.db.models.EmploymentPeriod + com.example.crud.db.models.Gender + com.example.crud.db.models.GenderConverter + com.example.crud.db.models.PhoneNumber + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..13c534a --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,28 @@ +management: + port: 8484 + security: + enabled: false + ssl: + enabled: false + key-store: classpath:management.jks + key-password: super*53cr37!pa33w0rd, +server: + port: ${port:8443} + ssl: + enabled: false + key-store: classpath:main.jks + key-password: super*53cr37!pa33w0rd, + undertow: + accesslog: + enabled: true + pattern: '%t %a "%r" %s (%D ms)' +spring: + aop: + proxy-target-class: true + datasource: + driver-class-name: org.postgresql.Driver + password: '' + url: jdbc:postgresql://127.0.0.1:26257/crud?sslmode=disable + username: root + jpa: + show-sql: true diff --git a/src/main/resources/applicationContext.xml b/src/main/resources/applicationContext.xml new file mode 100644 index 0000000..1c57c06 --- /dev/null +++ b/src/main/resources/applicationContext.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/com/example/crud/test/BasicTest.java b/src/test/java/com/example/crud/test/BasicTest.java new file mode 100644 index 0000000..73b1728 --- /dev/null +++ b/src/test/java/com/example/crud/test/BasicTest.java @@ -0,0 +1,118 @@ +package com.example.crud.test; + +import com.example.crud.db.annotations.Retry; +import com.example.crud.db.models.Employee; +import com.example.crud.db.models.Gender; +import org.junit.Test; + +import javax.persistence.*; + +import org.eclipse.persistence.sessions.Session; + +import java.util.List; + +public class BasicTest { + + EntityManagerFactory emf; + EntityManager em; + // Connection connection = em.unwrap(java.sql.Connection.class); + + @Test + public void testMain() throws Exception { + //emf = PersistenceTesting.createEMF(true); + emf = Persistence.createEntityManagerFactory("default"); + em = emf.createEntityManager(); + Session session = em.unwrap(Session.class); + session.getLogin().setQueryRetryAttemptCount(3); + + try { + + + hire(100); + + // Add employee with 555 area code to satisfy a test query + em.getTransaction().begin(); + Employee e = new Employee(); + e.setFirstName("John"); + e.setLastName("Doe"); + e.setGender(Gender.Male); + e.setSocialSecurityNumber("111-22-3333"); + e.addPhoneNumber("HOME", "555-555-2222"); + em.persist(e); + Long id = e.getId(); + + em.getTransaction().commit(); + em.clear(); + + queryAllEmployees(em); + em.clear(); + + queryEmployeeLikeAreaCode55(em); + em.clear(); + + modifyEmployee(em, id); + em.clear(); + + deleteEmployee(em, id); + em.clear(); + + em.close(); + + } finally { + emf.close(); + } + } + + @Retry(times = 3, on = org.springframework.dao.OptimisticLockingFailureException.class) + public int hire(int n) { + em.getTransaction().begin(); + new SamplePopulation().createNewEmployees(em, n); + em.getTransaction().commit(); + em.clear(); + return n; + } + + public void queryAllEmployees(EntityManager em) { + List results = em.createQuery("SELECT e FROM Employee e", Employee.class).getResultList(); + + System.out.println("Query All Results: " + results.size()); + + results.forEach(e -> System.out.println("\t>" + e)); + } + + public void queryEmployeeLikeAreaCode55(EntityManager em) { + System.out.println("\n\n --- Query Employee.phoneNumbers.areaCode LIKE '55%' ---"); + + TypedQuery query = em.createQuery("SELECT e FROM Employee e JOIN e.phoneNumbers phones WHERE phones.number LIKE '55%'", Employee.class); + List emps = query.getResultList(); + + emps.forEach(e -> System.out.println("> " + e)); + } + + public void modifyEmployee(EntityManager em, Long id) { + System.out.println("\n\n --- Modify Employee ---"); + em.getTransaction().begin(); + + Employee emp = em.find(Employee.class, id); + emp.setSalary(1); + + TypedQuery query = em.createQuery("SELECT e FROM Employee e WHERE e.id = :ID AND e.firstName = :FNAME", Employee.class); + query.setParameter("ID", id); + query.setParameter("FNAME", emp.getFirstName()); + emp = query.getSingleResult(); + + em.getTransaction().commit(); + + } + + public void deleteEmployee(EntityManager em, Long id) { + em.getTransaction().begin(); + + em.remove(em.find(Employee.class, id)); + em.flush(); + + //em.getTransaction().rollback(); + em.getTransaction().commit(); + } + +} diff --git a/src/test/java/com/example/crud/test/ConfigTest.java b/src/test/java/com/example/crud/test/ConfigTest.java new file mode 100644 index 0000000..3a29993 --- /dev/null +++ b/src/test/java/com/example/crud/test/ConfigTest.java @@ -0,0 +1,38 @@ +package com.example.crud.test; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ConfigTest { + + @Test + public void bootstrap() { + EntityManager em = getEmf().createEntityManager(); + + em.close(); + } + + private static EntityManagerFactory emf; + + public static EntityManagerFactory getEmf() { + return emf; + } + + @BeforeClass + public static void createEMF() { + emf = PersistenceTesting.createEMF(true); + } + + @AfterClass + public static void closeEMF() { + if (emf != null && emf.isOpen()) { + emf.close(); + } + emf = null; + } + +} diff --git a/src/test/java/com/example/crud/test/PersistenceTesting.java b/src/test/java/com/example/crud/test/PersistenceTesting.java new file mode 100644 index 0000000..aebdb9a --- /dev/null +++ b/src/test/java/com/example/crud/test/PersistenceTesting.java @@ -0,0 +1,54 @@ +package com.example.crud.test; + +import java.util.HashMap; +import java.util.Map; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; + +import org.eclipse.persistence.config.PersistenceUnitProperties; + +/** + * Persistence testing helper which creates an EMF providing testing overrides + * to use direct JDBC instead of a data source + */ +public class PersistenceTesting { + + public static EntityManagerFactory createEMF(boolean replaceTables) { + Map props = new HashMap(); + + // Ensure the persistence.xml provided data source are ignored during test runs + props.put(PersistenceUnitProperties.NON_JTA_DATASOURCE, ""); + props.put(PersistenceUnitProperties.JTA_DATASOURCE, ""); + props.put(PersistenceUnitProperties.TRANSACTION_TYPE, "RESOURCE_LOCAL"); + + // Configure the use of embedded derby for the tests allowing system properties of the same name to override + setProperty(props, PersistenceUnitProperties.JDBC_DRIVER, "org.postgresql.Driver"); + setProperty(props, PersistenceUnitProperties.JDBC_URL, "jdbc:postgresql://127.0.0.1:26257/crud"); + setProperty(props, PersistenceUnitProperties.JDBC_USER, "root"); + setProperty(props, PersistenceUnitProperties.JDBC_PASSWORD, ""); + + // Ensure weaving is not used during testing + props.put(PersistenceUnitProperties.WEAVING, "false"); + + if (replaceTables) { + props.put(PersistenceUnitProperties.DDL_GENERATION, PersistenceUnitProperties.DROP_AND_CREATE); + props.put(PersistenceUnitProperties.DDL_GENERATION_MODE, PersistenceUnitProperties.DDL_DATABASE_GENERATION); + } + + return Persistence.createEntityManagerFactory("default", props); + } + + /** + * Add the system property value if it exists, otherwise use the default + * value. + */ + private static void setProperty(Map props, String key, String defaultValue) { + String value = defaultValue; + if (System.getProperties().containsKey(key)) { + value = System.getProperty(key); + } + props.put(key, value); + } + +} diff --git a/src/test/java/com/example/crud/test/SamplePopulation.java b/src/test/java/com/example/crud/test/SamplePopulation.java new file mode 100644 index 0000000..2023826 --- /dev/null +++ b/src/test/java/com/example/crud/test/SamplePopulation.java @@ -0,0 +1,52 @@ +package com.example.crud.test; + +import com.example.crud.db.models.Address; +import com.example.crud.db.models.Employee; +import com.example.crud.db.models.Gender; +import com.github.javafaker.Faker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; +import java.util.Random; + +import static java.lang.String.format; + +/** + * Examples illustrating the use of JPA with the employee domain + * com.example.crud.models. + * + * @see BasicTest + */ +public class SamplePopulation { + + private static final Logger log = LoggerFactory.getLogger(SamplePopulation.class); + + Faker fake = new Faker(); //TODO(gburd): https://github.com/joselufo/RandomUserApi https://randomuser.me/ + + /** + * Create the specified number of random sample employees. + */ + public void createNewEmployees(EntityManager em, int quantity) { + for (int index = 0; index < quantity; index++) { + em.persist(createRandomEmployee()); + } + } + + public Employee createRandomEmployee() { + Random r = new Random(); + + Employee emp = new Employee(); + emp.setSocialSecurityNumber(format("%d-%d-%d", r.nextInt(999), r.nextInt(99), r.nextInt(9999))); + emp.setGender(Gender.values()[r.nextInt(2)]); + emp.setFirstName(fake.name().firstName()); + emp.setLastName(fake.name().lastName()); + emp.addPhoneNumber("HOME", fake.phoneNumber().phoneNumber().toString()); + emp.addPhoneNumber("WORK", fake.phoneNumber().phoneNumber().toString()); + emp.addPhoneNumber("MOBILE", fake.phoneNumber().cellPhone().toString()); + + emp.setAddress(new Address(fake.address().city(), fake.address().country(), "", fake.address().zipCode(), fake.address().streetAddress())); + + return emp; + } +} diff --git a/src/test/java/com/example/crud/test/rest/CrudConfigurationTest.java b/src/test/java/com/example/crud/test/rest/CrudConfigurationTest.java new file mode 100644 index 0000000..2ff8e97 --- /dev/null +++ b/src/test/java/com/example/crud/test/rest/CrudConfigurationTest.java @@ -0,0 +1,54 @@ +package com.example.crud.test.rest; + +import java.util.Map; + +import com.example.crud.Application; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * Basic integration tests for service example application. + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { "management.port=0" }) +public class CrudConfigurationTest { + + @LocalServerPort + private int port; + + @Value("${local.management.port}") + private int mgt; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + public void shouldReturn200WhenSendingRequestToController() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = this.testRestTemplate.getForEntity( + "http://localhost:" + this.port + "/hello-world", Map.class); + + then(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void shouldReturn200WhenSendingRequestToManagementEndpoint() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = this.testRestTemplate.getForEntity( + "http://localhost:" + this.mgt + "/info", Map.class); + + then(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } +}