diff --git a/pom.xml b/pom.xml index 0a99788..7f75e54 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 io.zipcoder @@ -43,12 +43,20 @@ hsqldb runtime - javax.inject javax.inject 1 + + com.h2database + h2 + 1.4.194 + + + org.springframework.boot + spring-boot-starter-security + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/build.gradle b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/build.gradle new file mode 100644 index 0000000..a543cc9 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'java' +apply plugin: 'maven' + +group = 'io.pivotal.workshop' +version = '1.0-SNAPSHOT' + +description = """simple-spring-webapp Maven Webapp""" + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + + + +repositories { + + maven { url "http://repo.maven.apache.org/maven2" } +} +dependencies { + compile group: 'org.springframework', name: 'spring-webmvc', version:'5.0.1.RELEASE' + compile group: 'org.springframework', name: 'spring-context', version:'5.0.1.RELEASE' + compile group: 'org.springframework', name: 'spring-aop', version:'5.0.1.RELEASE' + compile group: 'org.springframework', name: 'spring-web', version:'5.0.1.RELEASE' + + compile group: 'org.thymeleaf', name: 'thymeleaf', version:'3.0.9.RELEASE' + compile group: 'org.thymeleaf', name: 'thymeleaf-spring5', version:'3.0.9.RELEASE' + + compile group: 'org.slf4j', name: 'slf4j-api', version:'1.7.25' + compile group: 'ch.qos.logback', name: 'logback-classic', version:'1.2.3' + + providedCompile group: 'javax.servlet', name: 'servlet-api', version:'3.1.0' +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/pom.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/pom.xml new file mode 100644 index 0000000..5416151 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/pom.xml @@ -0,0 +1,94 @@ + + 4.0.0 + io.pivotal.workshop + simple-spring-webapp + war + 1.0-SNAPSHOT + simple-spring-webapp Maven Webapp + http://maven.apache.org + + io.pivotal.education.boot + parentProject + 1.0.a.RELEASE + ../../ + + + + + 1.8 + + + 3.1.0 + + + 5.0.1.RELEASE + + + 3.0.9.RELEASE + + + 1.7.25 + 1.2.3 + + + + + + + org.springframework + spring-context + ${spring-framework.version} + + + org.springframework + spring-aop + ${spring-framework.version} + + + org.springframework + spring-webmvc + ${spring-framework.version} + + + org.springframework + spring-web + ${spring-framework.version} + + + + org.thymeleaf + thymeleaf + ${thymeleaf.version} + + + org.thymeleaf + thymeleaf-spring5 + ${thymeleaf.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + javax.servlet + javax.servlet-api + ${servlet.version} + provided + + + + + simple-spring-webapp + + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/settings.gradle b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/settings.gradle new file mode 100644 index 0000000..2c26010 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'simple-spring-webapp' diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/java/io/pivotal/workshop/web/SimpleController.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/java/io/pivotal/workshop/web/SimpleController.java new file mode 100644 index 0000000..ca56838 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/java/io/pivotal/workshop/web/SimpleController.java @@ -0,0 +1,29 @@ +package io.pivotal.workshop.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public class SimpleController{ + + Logger log = LoggerFactory.getLogger(SimpleController.class); + + @RequestMapping("/") + public ModelAndView index(){ + log.debug("About to redirect..."); + return new ModelAndView("redirect:/showMessage"); + } + + @RequestMapping(value="/showMessage",method = RequestMethod.GET) + public ModelAndView helloWorld(){ + final String message = "Simple Spring MVC Web App with Thymeleaf and Spring 5"; + log.debug("Showing the Message: " + message); + ModelAndView model = new ModelAndView("showMessage"); + model.addObject("message", message); + return model; + } +} \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/resources/logback.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/resources/logback.xml new file mode 100644 index 0000000..8e1ce0e --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/applicationContext.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/applicationContext.xml new file mode 100644 index 0000000..364977e --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/applicationContext.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/dispatcherServlet-servlet.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/dispatcherServlet-servlet.xml new file mode 100644 index 0000000..a3ac749 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/dispatcherServlet-servlet.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/view/index.html b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/view/index.html new file mode 100644 index 0000000..40f6c29 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/view/index.html @@ -0,0 +1,11 @@ + + + + + + Welcome + + +

This page never will show up

+ + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/view/showMessage.html b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/view/showMessage.html new file mode 100644 index 0000000..ac1c69b --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/view/showMessage.html @@ -0,0 +1,11 @@ + + + + + + Welcome + + +

+ + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/web.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..40c2f7d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/01-spring-framework/simple-spring-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,45 @@ + + + + Simple Spring Web Application + + + + + + + + dispatcherServlet + org.springframework.web.servlet.DispatcherServlet + + 1 + + + + dispatcherServlet + / + + + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/.mvn/wrapper/maven-wrapper.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c315043 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/build.gradle b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/build.gradle new file mode 100644 index 0000000..73e2539 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/build.gradle @@ -0,0 +1,37 @@ +buildscript { + ext { + springBootVersion = '2.0.0.M7' + } + repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + + +dependencies { + compile('org.springframework.boot:spring-boot-starter-web') + + // SOLUTION: XML response + // compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml') + + testCompile('org.springframework.boot:spring-boot-starter-test') +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradle/wrapper/gradle-wrapper.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..90a06ce --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradlew b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradlew.bat b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/pom.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/pom.xml new file mode 100644 index 0000000..f0be831 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + io.pivotal.workshop + directory-web + 0.0.1-SNAPSHOT + jar + + directory-web + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.M7 + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/DirectoryWebApplication.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/DirectoryWebApplication.java new file mode 100644 index 0000000..4a9dac3 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/DirectoryWebApplication.java @@ -0,0 +1,12 @@ +package io.pivotal.workshop.directory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DirectoryWebApplication { + + public static void main(String[] args) { + SpringApplication.run(DirectoryWebApplication.class, args); + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java new file mode 100644 index 0000000..a73fffb --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java @@ -0,0 +1,116 @@ +package io.pivotal.workshop.directory.controller; + +import java.net.URI; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import io.pivotal.workshop.directory.domain.Person; +import io.pivotal.workshop.directory.repository.DirectoryRepository; + +@RestController +public class DirectoryController { + + private DirectoryRepository repo; + + @Autowired + public DirectoryController(DirectoryRepository repo) { + this.repo = repo; + } + + @RequestMapping("/directory") + public ResponseEntity> findAll() { + return ResponseEntity.ok(this.repo.findAll()); + } + + + //SOLUTION: Challenges + @RequestMapping("/directory/{id}") + public ResponseEntity findById(@PathVariable String id) { + Optional result = this.repo.findById(id); + + if (result.isPresent()) { + return ResponseEntity.ok(result.get()); + } + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("{}"); + } + + @RequestMapping("/directory/search") + public ResponseEntity searchByEmail(@RequestParam String email) { + Optional result = this.repo.findByEmail(email); + + if (result.isPresent()) { + return ResponseEntity.ok(result.get()); + } + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("{}"); + } + + @RequestMapping(value = "/directory/{id}", method = { RequestMethod.DELETE }) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deletePerson(@PathVariable String id) { + this.repo.delete(id); + } + + + @RequestMapping(value = "/directory", method = { RequestMethod.POST }) + public ResponseEntity addPerson(@RequestBody Person person) { + + return this.save(person); + } + + @RequestMapping(value = "/directory", method = { RequestMethod.PUT }) + public ResponseEntity updatePerson(@RequestBody Person person) { + return this.save(person); + } + + private ResponseEntity save(Person person) { + + Person result = this.repo.save(person); + + final URI location = ServletUriComponentsBuilder.fromCurrentServletMapping().path("/directory/{id}").build() + .expand(person.getId()).toUri(); + + return ResponseEntity.created(location).body(result); + } + + + //SOLUTION: Homework + /* + @RequestMapping(value = "/directory", method = { RequestMethod.POST }) + public ResponseEntity addPerson(@Valid @RequestBody Person person, BindingResult bindingResult) { + + return this.save(person, bindingResult); + } + + @RequestMapping(value = "/directory", method = { RequestMethod.PUT }) + public ResponseEntity updatePerson(@Valid @RequestBody Person person, BindingResult bindingResult) { + return this.save(person, bindingResult); + } + + private ResponseEntity save(Person person, BindingResult bindingResult) { + + if (bindingResult.hasErrors()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new PersonError(bindingResult.getAllErrors())); + } + + Person result = this.repo.save(person); + + final URI location = ServletUriComponentsBuilder.fromCurrentServletMapping().path("/directory/{id}").build() + .expand(person.getId()).toUri(); + + return ResponseEntity.created(location).body(result); + } + */ +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/domain/Person.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/domain/Person.java new file mode 100644 index 0000000..97aeba2 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/domain/Person.java @@ -0,0 +1,109 @@ +package io.pivotal.workshop.directory.domain; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +import javax.validation.constraints.NotNull; + +public class Person { + + private SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd"); + + private String id; + + @NotNull + private String email; + @NotNull + private String name; + @NotNull + private String password; + private Date birthday; + private Date created; + private Date modified; + + public Person(){ + this.id = UUID.randomUUID().toString().replaceAll("-", ""); + this.created = new Date(); + this.modified = new Date(); + } + + public Person(String email, String name, String password, String birthday) { + this(); + this.email = email; + this.name = name; + this.password = password; + + try { + this.birthday = date.parse(birthday); + } catch (ParseException e) { + this.birthday = null; + } + } + + public Person(String email, String name, String password, Date birthday) { + this(); + this.email = email; + this.name = name; + this.password = password; + this.birthday = birthday; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getBirthday() { + return birthday; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/error/PersonError.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/error/PersonError.java new file mode 100644 index 0000000..5b0611e --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/error/PersonError.java @@ -0,0 +1,40 @@ +package io.pivotal.workshop.directory.error; + +import java.util.Date; +import java.util.List; + +import org.springframework.validation.ObjectError; + +public class PersonError { + + private List errors; + private Date date; + + public PersonError(){ + this.date = new Date(); + } + + public PersonError(List errors){ + this(); + this.errors = errors; + } + + public List getErrors() { + return errors; + } + + public void setErrors(List errors) { + this.errors = errors; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java new file mode 100644 index 0000000..fe92f7c --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java @@ -0,0 +1,77 @@ +package io.pivotal.workshop.directory.repository; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; + +import org.springframework.stereotype.Repository; + +import io.pivotal.workshop.directory.domain.Person; + +@Repository +public class DirectoryRepository { + + + @SuppressWarnings("serial") + private List directory = new ArrayList(){{ + add(new Person("john@email.com","John S","password","1985-11-10")); + add(new Person("mike@email.com","Mike H","password","1984-12-02")); + add(new Person("dan@email.com","Dan B","password","1983-03-07")); + add(new Person("bill@email.com","Bill G","password","1983-06-12")); + add(new Person("mark@email.com","Mark S","password","1986-02-22")); + }}; + + public Iterable findAll(){ + return this.directory; + } + + // SOLUTION: + public Optional findByEmail(String email){ + return findFirstBy( p -> p.getEmail().equals(email)); + } + + public Optional findById(String id){ + return findFirstBy( p -> p.getId().equals(id)); + } + + public Person save(Person person){ + Optional result = this.findById(person.getId()); + + if(result.isPresent()){ + + if( person.getEmail() != null) result.get().setEmail(person.getEmail()); + if( person.getName() != null) result.get().setName(person.getName()); + if( person.getPassword() != null) result.get().setPassword(person.getPassword()); + if( person.getBirthday() != null) result.get().setBirthday(person.getBirthday()); + result.get().setModified(new Date()); + + return result.get(); + + }else{ + + if (person.getId() != null){ + person.setId(UUID.randomUUID().toString().replaceAll("-","")); + } + + person.setCreated(new Date()); + person.setModified(new Date()); + this.directory.add(person); + + return person; + } + } + + public void delete(String id){ + this.findById(id).ifPresent(p -> { this.directory.remove(p); }); + } + + private Optional findFirstBy(Predicate findBy){ + return directory.stream() + .filter(findBy) + .findFirst(); + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/resources/application.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/resources/application.properties new file mode 100644 index 0000000..0226d86 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/main/resources/application.properties @@ -0,0 +1,2 @@ +# SOLUTION: Date format +#spring.jackson.date-format=yyyy-MM-dd \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/test/java/io/pivotal/workshop/directory/DirectoryWebApplicationTests.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/test/java/io/pivotal/workshop/directory/DirectoryWebApplicationTests.java new file mode 100644 index 0000000..b2bf59e --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/02-spring-boot-overview/directory-web/src/test/java/io/pivotal/workshop/directory/DirectoryWebApplicationTests.java @@ -0,0 +1,16 @@ +package io.pivotal.workshop.directory; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class DirectoryWebApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/.mvn/wrapper/maven-wrapper.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c315043 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/build.gradle b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/build.gradle new file mode 100644 index 0000000..1df9956 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/build.gradle @@ -0,0 +1,42 @@ +buildscript { + ext { + springBootVersion = '2.0.0.M7' + } + repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + + +dependencies { + compile('org.springframework.boot:spring-boot-starter-web') + compile('org.springframework.boot:spring-boot-starter-aop') + + + compile('org.springframework.boot:spring-boot-devtools') + optional("org.springframework.boot:spring-boot-configuration-processor") + + // SOLUTION: XML response + // compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml') + + testCompile('org.springframework.boot:spring-boot-starter-test') +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradle/wrapper/gradle-wrapper.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..90a06ce --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradlew b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradlew.bat b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/pom.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/pom.xml new file mode 100644 index 0000000..ca78af2 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + io.pivotal.workshop + directory-web-internals + 0.0.1-SNAPSHOT + jar + + directory-web-internals + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.M7 + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/DirectoryWebInternalsApplication.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/DirectoryWebInternalsApplication.java new file mode 100644 index 0000000..da31c92 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/DirectoryWebInternalsApplication.java @@ -0,0 +1,12 @@ +package io.pivotal.workshop.directory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DirectoryWebInternalsApplication { + + public static void main(String[] args) { + SpringApplication.run(DirectoryWebInternalsApplication.class, args); + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/annotation/Audit.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/annotation/Audit.java new file mode 100644 index 0000000..ddb2121 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/annotation/Audit.java @@ -0,0 +1,12 @@ +package io.pivotal.workshop.directory.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Audit { + Auditor value() default Auditor.NOTHING; +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/annotation/Auditor.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/annotation/Auditor.java new file mode 100644 index 0000000..44fcec3 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/annotation/Auditor.java @@ -0,0 +1,5 @@ +package io.pivotal.workshop.directory.annotation; + +public enum Auditor { + BEFORE, AFTER, BEFORE_AND_AFTER, NOTHING +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/aop/DirectoryAudit.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/aop/DirectoryAudit.java new file mode 100644 index 0000000..9dae222 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/aop/DirectoryAudit.java @@ -0,0 +1,74 @@ +package io.pivotal.workshop.directory.aop; + +import java.util.stream.IntStream; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.pivotal.workshop.directory.annotation.Audit; +import io.pivotal.workshop.directory.config.DirectoryProperties; + +@Aspect +public class DirectoryAudit { + + private DirectoryProperties props; + + public DirectoryAudit(DirectoryProperties props){ + this.props = props; + } + + private static Logger log = LoggerFactory.getLogger("[AUDIT]"); + + @Around("execution(* *(..)) && @annotation(audit)") + public Object audit(ProceedingJoinPoint jp, Audit audit) throws Throwable { + // Step. Get the Arguments + Object[] args = jp.getArgs(); + + // Step. Print execution information + this.printBar(); + this.print("[executing] " + (props.getInfo().toLowerCase().equals("short") ? jp.getSignature().getName() : jp.getSignature() )); + + // Step. Print arguments if any + switch (audit.value()) { + case BEFORE: + case BEFORE_AND_AFTER: + this.printArgs(args); + default: + break; + } + + // Step. Proceed + Object obj = jp.proceed(args); + + // Step. Print result if needed + switch (audit.value()) { + case AFTER: + case BEFORE_AND_AFTER: + this.print("[result] " + obj); + default: + this.printBar(); + break; + } + + // Step. Return + return obj; + } + + private void print(Object obj) { + log.info(obj.toString()); + } + + private void printArgs(Object[] args) { + IntStream.range(0, args.length).forEach(idx -> { + log.info(String.format("[params] arg%d = %s", idx, args[idx])); + }); + + } + + private void printBar(){ + log.info("==========="); + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/aop/SimpleAudit.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/aop/SimpleAudit.java new file mode 100644 index 0000000..a31cd5f --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/aop/SimpleAudit.java @@ -0,0 +1,20 @@ +package io.pivotal.workshop.directory.aop; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.pivotal.workshop.directory.annotation.Audit; + +@Aspect +public class SimpleAudit { + + private static Logger log = LoggerFactory.getLogger("[AUDIT]"); + + @Before("@annotation(audit)") + public void audit(JoinPoint jp, Audit audit){ + log.info("[EXECUTING] " + jp.getSignature()); + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/config/DirectoryConfig.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/config/DirectoryConfig.java new file mode 100644 index 0000000..8e4f482 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/config/DirectoryConfig.java @@ -0,0 +1,33 @@ +package io.pivotal.workshop.directory.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.pivotal.workshop.directory.aop.SimpleAudit; + +@Configuration +public class DirectoryConfig { + + @ConditionalOnClass(name={"io.pivotal.workshop.directory.repository.DirectoryRepository"}) + @Bean + public SimpleAudit simpleAudit(){ + return new SimpleAudit(); + } + +} + +// SOLUTION: +/* +@Configuration +@EnableConfigurationProperties(DirectoryProperties.class) +public class DirectoryConfig { + + @ConditionalOnProperty(prefix="directory",name="audit",havingValue="on") + @Bean + public DirectoryAudit directoryAudit(DirectoryProperties props){ + return new DirectoryAudit(props); + } + +} +*/ \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/config/DirectoryProperties.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/config/DirectoryProperties.java new file mode 100644 index 0000000..7f80ccb --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/config/DirectoryProperties.java @@ -0,0 +1,29 @@ +package io.pivotal.workshop.directory.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + + +//SOLUTION: +@ConfigurationProperties(prefix = "directory") +public class DirectoryProperties { + + private String audit = "off"; + private String info = "long"; + + public String getAudit() { + return audit; + } + + public void setAudit(String audit) { + this.audit = audit; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java new file mode 100644 index 0000000..a73fffb --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java @@ -0,0 +1,116 @@ +package io.pivotal.workshop.directory.controller; + +import java.net.URI; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import io.pivotal.workshop.directory.domain.Person; +import io.pivotal.workshop.directory.repository.DirectoryRepository; + +@RestController +public class DirectoryController { + + private DirectoryRepository repo; + + @Autowired + public DirectoryController(DirectoryRepository repo) { + this.repo = repo; + } + + @RequestMapping("/directory") + public ResponseEntity> findAll() { + return ResponseEntity.ok(this.repo.findAll()); + } + + + //SOLUTION: Challenges + @RequestMapping("/directory/{id}") + public ResponseEntity findById(@PathVariable String id) { + Optional result = this.repo.findById(id); + + if (result.isPresent()) { + return ResponseEntity.ok(result.get()); + } + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("{}"); + } + + @RequestMapping("/directory/search") + public ResponseEntity searchByEmail(@RequestParam String email) { + Optional result = this.repo.findByEmail(email); + + if (result.isPresent()) { + return ResponseEntity.ok(result.get()); + } + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("{}"); + } + + @RequestMapping(value = "/directory/{id}", method = { RequestMethod.DELETE }) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deletePerson(@PathVariable String id) { + this.repo.delete(id); + } + + + @RequestMapping(value = "/directory", method = { RequestMethod.POST }) + public ResponseEntity addPerson(@RequestBody Person person) { + + return this.save(person); + } + + @RequestMapping(value = "/directory", method = { RequestMethod.PUT }) + public ResponseEntity updatePerson(@RequestBody Person person) { + return this.save(person); + } + + private ResponseEntity save(Person person) { + + Person result = this.repo.save(person); + + final URI location = ServletUriComponentsBuilder.fromCurrentServletMapping().path("/directory/{id}").build() + .expand(person.getId()).toUri(); + + return ResponseEntity.created(location).body(result); + } + + + //SOLUTION: Homework + /* + @RequestMapping(value = "/directory", method = { RequestMethod.POST }) + public ResponseEntity addPerson(@Valid @RequestBody Person person, BindingResult bindingResult) { + + return this.save(person, bindingResult); + } + + @RequestMapping(value = "/directory", method = { RequestMethod.PUT }) + public ResponseEntity updatePerson(@Valid @RequestBody Person person, BindingResult bindingResult) { + return this.save(person, bindingResult); + } + + private ResponseEntity save(Person person, BindingResult bindingResult) { + + if (bindingResult.hasErrors()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new PersonError(bindingResult.getAllErrors())); + } + + Person result = this.repo.save(person); + + final URI location = ServletUriComponentsBuilder.fromCurrentServletMapping().path("/directory/{id}").build() + .expand(person.getId()).toUri(); + + return ResponseEntity.created(location).body(result); + } + */ +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/domain/Person.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/domain/Person.java new file mode 100644 index 0000000..5b4ba27 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/domain/Person.java @@ -0,0 +1,115 @@ +package io.pivotal.workshop.directory.domain; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +import javax.validation.constraints.NotNull; + +public class Person { + + private SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd"); + + private String id; + + @NotNull + private String email; + @NotNull + private String name; + @NotNull + private String password; + private Date birthday; + private Date created; + private Date modified; + + public Person() { + this.id = UUID.randomUUID().toString().replaceAll("-", ""); + this.created = new Date(); + this.modified = new Date(); + } + + public Person(String email, String name, String password, String birthday) { + this(); + this.email = email; + this.name = name; + this.password = password; + + try { + this.birthday = date.parse(birthday); + } catch (ParseException e) { + this.birthday = null; + } + } + + public Person(String email, String name, String password, Date birthday) { + this(); + this.email = email; + this.name = name; + this.password = password; + this.birthday = birthday; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getBirthday() { + return birthday; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + @Override + public String toString() { + return "Person [date=" + date + ", id=" + id + ", email=" + email + ", name=" + name + ", password=" + password + + ", birthday=" + birthday + ", created=" + created + ", modified=" + modified + "]"; + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/error/PersonError.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/error/PersonError.java new file mode 100644 index 0000000..5b0611e --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/error/PersonError.java @@ -0,0 +1,40 @@ +package io.pivotal.workshop.directory.error; + +import java.util.Date; +import java.util.List; + +import org.springframework.validation.ObjectError; + +public class PersonError { + + private List errors; + private Date date; + + public PersonError(){ + this.date = new Date(); + } + + public PersonError(List errors){ + this(); + this.errors = errors; + } + + public List getErrors() { + return errors; + } + + public void setErrors(List errors) { + this.errors = errors; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java new file mode 100644 index 0000000..d2c0907 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java @@ -0,0 +1,79 @@ +package io.pivotal.workshop.directory.repository; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; + +import org.springframework.stereotype.Repository; + +import io.pivotal.workshop.directory.annotation.Audit; +import io.pivotal.workshop.directory.domain.Person; + +@Repository +public class DirectoryRepository { + + + @SuppressWarnings("serial") + private List directory = new ArrayList(){{ + add(new Person("john@email.com","John S","password","1985-11-10")); + add(new Person("mike@email.com","Mike H","password","1984-12-02")); + add(new Person("dan@email.com","Dan B","password","1983-03-07")); + add(new Person("bill@email.com","Bill G","password","1983-06-12")); + add(new Person("mark@email.com","Mark S","password","1986-02-22")); + }}; + + public Iterable findAll(){ + return this.directory; + } + + // SOLUTION: + @Audit + public Optional findByEmail(String email){ + return findFirstBy( p -> p.getEmail().equals(email)); + } + + public Optional findById(String id){ + return findFirstBy( p -> p.getId().equals(id)); + } + + public Person save(Person person){ + Optional result = this.findById(person.getId()); + + if(result.isPresent()){ + + if( person.getEmail() != null) result.get().setEmail(person.getEmail()); + if( person.getName() != null) result.get().setName(person.getName()); + if( person.getPassword() != null) result.get().setPassword(person.getPassword()); + if( person.getBirthday() != null) result.get().setBirthday(person.getBirthday()); + result.get().setModified(new Date()); + + return result.get(); + + }else{ + + if (person.getId() != null){ + person.setId(UUID.randomUUID().toString().replaceAll("-","")); + } + + person.setCreated(new Date()); + person.setModified(new Date()); + this.directory.add(person); + + return person; + } + } + + public void delete(String id){ + this.findById(id).ifPresent(p -> { this.directory.remove(p); }); + } + + private Optional findFirstBy(Predicate findBy){ + return directory.stream() + .filter(findBy) + .findFirst(); + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..f61eb2c --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{"properties": [{ + "name": "directory.audit", + "type": "java.lang.String", + "description": "Directory Audit, use the @Audit annotation over methods, possible values: on, off" +},{ + "name": "directory.info", + "type": "java.lang.String", + "description": "Directory Audit, print out information about paramters and result. Possible values: short, long (default)" +}]} \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/resources/application.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/resources/application.properties new file mode 100644 index 0000000..00673cd --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/main/resources/application.properties @@ -0,0 +1,6 @@ +# SOLUTION: Date format +#spring.jackson.date-format=yyyy-MM-dd + + +#directory.audit=off +#directory.info=short \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/test/java/io/pivotal/workshop/directory/DirectoryWebInternalsApplicationTests.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/test/java/io/pivotal/workshop/directory/DirectoryWebInternalsApplicationTests.java new file mode 100644 index 0000000..12c24c0 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/03-spring-boot-internals/directory-web-internals/src/test/java/io/pivotal/workshop/directory/DirectoryWebInternalsApplicationTests.java @@ -0,0 +1,16 @@ +package io.pivotal.workshop.directory; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class DirectoryWebInternalsApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/.mvn/wrapper/maven-wrapper.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c315043 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/build.gradle b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/build.gradle new file mode 100644 index 0000000..1df9956 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/build.gradle @@ -0,0 +1,42 @@ +buildscript { + ext { + springBootVersion = '2.0.0.M7' + } + repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + + +dependencies { + compile('org.springframework.boot:spring-boot-starter-web') + compile('org.springframework.boot:spring-boot-starter-aop') + + + compile('org.springframework.boot:spring-boot-devtools') + optional("org.springframework.boot:spring-boot-configuration-processor") + + // SOLUTION: XML response + // compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml') + + testCompile('org.springframework.boot:spring-boot-starter-test') +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradle/wrapper/gradle-wrapper.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..90a06ce --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradlew b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradlew.bat b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/pom.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/pom.xml new file mode 100644 index 0000000..ff2307b --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + io.pivotal.workshop + directory-web-features + 0.0.1-SNAPSHOT + jar + + directory-web-features + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.M7 + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/DirectoryWebFeaturesApplication.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/DirectoryWebFeaturesApplication.java new file mode 100644 index 0000000..3d38a3e --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/DirectoryWebFeaturesApplication.java @@ -0,0 +1,78 @@ +package io.pivotal.workshop.directory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.Banner; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import java.util.stream.Stream; + +@SpringBootApplication +public class DirectoryWebFeaturesApplication { + + // SOLUTION: Using Application Listeners + //private static final Logger log = LoggerFactory.getLogger("[EVENTS]"); + + private static final Logger log = LoggerFactory.getLogger("[ARGS]"); + + public static void main(String[] args) { + //SpringApplication.run(DirectoryWebFeaturesApplication.class, args); + + //SOLUTION: + /* Turn off Banner + SpringApplication app = new SpringApplication(DirectoryWebFeaturesApplication.class); + app.setBannerMode(Banner.Mode.OFF); + app.run(args); + */ + + /* Using Application Listeners + ApplicationListener event = e -> { + log.info(e.toString()); + }; + + SpringApplication app = new SpringApplication(DirectoryWebFeaturesApplication.class); + app.setBannerMode(Banner.Mode.OFF); + app.setListeners(Arrays.asList(event)); + app.run(args); + */ + + /* Turn off Web Context + SpringApplication app = new SpringApplication(DirectoryWebFeaturesApplication.class); + app.setWebApplicationType(WebApplicationType.NONE); + app.run(args); + */ + + SpringApplication app = new SpringApplication(DirectoryWebFeaturesApplication.class); + app.run(args); + + } + + @Bean + public ApplicationRunner appRunner() { + return args -> { + args.getOptionNames().forEach( s -> { + log.info("Option Name: " + s); + }); + + if (args.containsOption("option")) log.info("Found Value:" + args.getOptionValues("option").toString()); + if (args.containsOption("enable-audit")) log.info("Found Value:" + args.getOptionValues("enable-audit").toString()); + + Stream.of(args.getSourceArgs()).forEach( s -> { + log.info("Argument: " + s); + }); + }; + } + + @Bean + public CommandLineRunner commandRunner() { + return args -> { + Stream.of(args).forEach(s -> { + log.info("CommandLine: " + s); + }); + }; + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/annotation/Audit.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/annotation/Audit.java new file mode 100644 index 0000000..ddb2121 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/annotation/Audit.java @@ -0,0 +1,12 @@ +package io.pivotal.workshop.directory.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Audit { + Auditor value() default Auditor.NOTHING; +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/annotation/Auditor.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/annotation/Auditor.java new file mode 100644 index 0000000..44fcec3 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/annotation/Auditor.java @@ -0,0 +1,5 @@ +package io.pivotal.workshop.directory.annotation; + +public enum Auditor { + BEFORE, AFTER, BEFORE_AND_AFTER, NOTHING +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/aop/DirectoryAudit.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/aop/DirectoryAudit.java new file mode 100644 index 0000000..9dae222 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/aop/DirectoryAudit.java @@ -0,0 +1,74 @@ +package io.pivotal.workshop.directory.aop; + +import java.util.stream.IntStream; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.pivotal.workshop.directory.annotation.Audit; +import io.pivotal.workshop.directory.config.DirectoryProperties; + +@Aspect +public class DirectoryAudit { + + private DirectoryProperties props; + + public DirectoryAudit(DirectoryProperties props){ + this.props = props; + } + + private static Logger log = LoggerFactory.getLogger("[AUDIT]"); + + @Around("execution(* *(..)) && @annotation(audit)") + public Object audit(ProceedingJoinPoint jp, Audit audit) throws Throwable { + // Step. Get the Arguments + Object[] args = jp.getArgs(); + + // Step. Print execution information + this.printBar(); + this.print("[executing] " + (props.getInfo().toLowerCase().equals("short") ? jp.getSignature().getName() : jp.getSignature() )); + + // Step. Print arguments if any + switch (audit.value()) { + case BEFORE: + case BEFORE_AND_AFTER: + this.printArgs(args); + default: + break; + } + + // Step. Proceed + Object obj = jp.proceed(args); + + // Step. Print result if needed + switch (audit.value()) { + case AFTER: + case BEFORE_AND_AFTER: + this.print("[result] " + obj); + default: + this.printBar(); + break; + } + + // Step. Return + return obj; + } + + private void print(Object obj) { + log.info(obj.toString()); + } + + private void printArgs(Object[] args) { + IntStream.range(0, args.length).forEach(idx -> { + log.info(String.format("[params] arg%d = %s", idx, args[idx])); + }); + + } + + private void printBar(){ + log.info("==========="); + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/aop/SimpleAudit.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/aop/SimpleAudit.java new file mode 100644 index 0000000..a31cd5f --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/aop/SimpleAudit.java @@ -0,0 +1,20 @@ +package io.pivotal.workshop.directory.aop; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.pivotal.workshop.directory.annotation.Audit; + +@Aspect +public class SimpleAudit { + + private static Logger log = LoggerFactory.getLogger("[AUDIT]"); + + @Before("@annotation(audit)") + public void audit(JoinPoint jp, Audit audit){ + log.info("[EXECUTING] " + jp.getSignature()); + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/config/DirectoryConfig.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/config/DirectoryConfig.java new file mode 100644 index 0000000..8e4f482 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/config/DirectoryConfig.java @@ -0,0 +1,33 @@ +package io.pivotal.workshop.directory.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.pivotal.workshop.directory.aop.SimpleAudit; + +@Configuration +public class DirectoryConfig { + + @ConditionalOnClass(name={"io.pivotal.workshop.directory.repository.DirectoryRepository"}) + @Bean + public SimpleAudit simpleAudit(){ + return new SimpleAudit(); + } + +} + +// SOLUTION: +/* +@Configuration +@EnableConfigurationProperties(DirectoryProperties.class) +public class DirectoryConfig { + + @ConditionalOnProperty(prefix="directory",name="audit",havingValue="on") + @Bean + public DirectoryAudit directoryAudit(DirectoryProperties props){ + return new DirectoryAudit(props); + } + +} +*/ \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/config/DirectoryProperties.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/config/DirectoryProperties.java new file mode 100644 index 0000000..7f80ccb --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/config/DirectoryProperties.java @@ -0,0 +1,29 @@ +package io.pivotal.workshop.directory.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + + +//SOLUTION: +@ConfigurationProperties(prefix = "directory") +public class DirectoryProperties { + + private String audit = "off"; + private String info = "long"; + + public String getAudit() { + return audit; + } + + public void setAudit(String audit) { + this.audit = audit; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java new file mode 100644 index 0000000..a73fffb --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/controller/DirectoryController.java @@ -0,0 +1,116 @@ +package io.pivotal.workshop.directory.controller; + +import java.net.URI; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import io.pivotal.workshop.directory.domain.Person; +import io.pivotal.workshop.directory.repository.DirectoryRepository; + +@RestController +public class DirectoryController { + + private DirectoryRepository repo; + + @Autowired + public DirectoryController(DirectoryRepository repo) { + this.repo = repo; + } + + @RequestMapping("/directory") + public ResponseEntity> findAll() { + return ResponseEntity.ok(this.repo.findAll()); + } + + + //SOLUTION: Challenges + @RequestMapping("/directory/{id}") + public ResponseEntity findById(@PathVariable String id) { + Optional result = this.repo.findById(id); + + if (result.isPresent()) { + return ResponseEntity.ok(result.get()); + } + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("{}"); + } + + @RequestMapping("/directory/search") + public ResponseEntity searchByEmail(@RequestParam String email) { + Optional result = this.repo.findByEmail(email); + + if (result.isPresent()) { + return ResponseEntity.ok(result.get()); + } + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("{}"); + } + + @RequestMapping(value = "/directory/{id}", method = { RequestMethod.DELETE }) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deletePerson(@PathVariable String id) { + this.repo.delete(id); + } + + + @RequestMapping(value = "/directory", method = { RequestMethod.POST }) + public ResponseEntity addPerson(@RequestBody Person person) { + + return this.save(person); + } + + @RequestMapping(value = "/directory", method = { RequestMethod.PUT }) + public ResponseEntity updatePerson(@RequestBody Person person) { + return this.save(person); + } + + private ResponseEntity save(Person person) { + + Person result = this.repo.save(person); + + final URI location = ServletUriComponentsBuilder.fromCurrentServletMapping().path("/directory/{id}").build() + .expand(person.getId()).toUri(); + + return ResponseEntity.created(location).body(result); + } + + + //SOLUTION: Homework + /* + @RequestMapping(value = "/directory", method = { RequestMethod.POST }) + public ResponseEntity addPerson(@Valid @RequestBody Person person, BindingResult bindingResult) { + + return this.save(person, bindingResult); + } + + @RequestMapping(value = "/directory", method = { RequestMethod.PUT }) + public ResponseEntity updatePerson(@Valid @RequestBody Person person, BindingResult bindingResult) { + return this.save(person, bindingResult); + } + + private ResponseEntity save(Person person, BindingResult bindingResult) { + + if (bindingResult.hasErrors()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new PersonError(bindingResult.getAllErrors())); + } + + Person result = this.repo.save(person); + + final URI location = ServletUriComponentsBuilder.fromCurrentServletMapping().path("/directory/{id}").build() + .expand(person.getId()).toUri(); + + return ResponseEntity.created(location).body(result); + } + */ +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/domain/Person.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/domain/Person.java new file mode 100644 index 0000000..5b4ba27 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/domain/Person.java @@ -0,0 +1,115 @@ +package io.pivotal.workshop.directory.domain; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +import javax.validation.constraints.NotNull; + +public class Person { + + private SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd"); + + private String id; + + @NotNull + private String email; + @NotNull + private String name; + @NotNull + private String password; + private Date birthday; + private Date created; + private Date modified; + + public Person() { + this.id = UUID.randomUUID().toString().replaceAll("-", ""); + this.created = new Date(); + this.modified = new Date(); + } + + public Person(String email, String name, String password, String birthday) { + this(); + this.email = email; + this.name = name; + this.password = password; + + try { + this.birthday = date.parse(birthday); + } catch (ParseException e) { + this.birthday = null; + } + } + + public Person(String email, String name, String password, Date birthday) { + this(); + this.email = email; + this.name = name; + this.password = password; + this.birthday = birthday; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getBirthday() { + return birthday; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + @Override + public String toString() { + return "Person [date=" + date + ", id=" + id + ", email=" + email + ", name=" + name + ", password=" + password + + ", birthday=" + birthday + ", created=" + created + ", modified=" + modified + "]"; + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/error/PersonError.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/error/PersonError.java new file mode 100644 index 0000000..5b0611e --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/error/PersonError.java @@ -0,0 +1,40 @@ +package io.pivotal.workshop.directory.error; + +import java.util.Date; +import java.util.List; + +import org.springframework.validation.ObjectError; + +public class PersonError { + + private List errors; + private Date date; + + public PersonError(){ + this.date = new Date(); + } + + public PersonError(List errors){ + this(); + this.errors = errors; + } + + public List getErrors() { + return errors; + } + + public void setErrors(List errors) { + this.errors = errors; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java new file mode 100644 index 0000000..d2c0907 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/java/io/pivotal/workshop/directory/repository/DirectoryRepository.java @@ -0,0 +1,79 @@ +package io.pivotal.workshop.directory.repository; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; + +import org.springframework.stereotype.Repository; + +import io.pivotal.workshop.directory.annotation.Audit; +import io.pivotal.workshop.directory.domain.Person; + +@Repository +public class DirectoryRepository { + + + @SuppressWarnings("serial") + private List directory = new ArrayList(){{ + add(new Person("john@email.com","John S","password","1985-11-10")); + add(new Person("mike@email.com","Mike H","password","1984-12-02")); + add(new Person("dan@email.com","Dan B","password","1983-03-07")); + add(new Person("bill@email.com","Bill G","password","1983-06-12")); + add(new Person("mark@email.com","Mark S","password","1986-02-22")); + }}; + + public Iterable findAll(){ + return this.directory; + } + + // SOLUTION: + @Audit + public Optional findByEmail(String email){ + return findFirstBy( p -> p.getEmail().equals(email)); + } + + public Optional findById(String id){ + return findFirstBy( p -> p.getId().equals(id)); + } + + public Person save(Person person){ + Optional result = this.findById(person.getId()); + + if(result.isPresent()){ + + if( person.getEmail() != null) result.get().setEmail(person.getEmail()); + if( person.getName() != null) result.get().setName(person.getName()); + if( person.getPassword() != null) result.get().setPassword(person.getPassword()); + if( person.getBirthday() != null) result.get().setBirthday(person.getBirthday()); + result.get().setModified(new Date()); + + return result.get(); + + }else{ + + if (person.getId() != null){ + person.setId(UUID.randomUUID().toString().replaceAll("-","")); + } + + person.setCreated(new Date()); + person.setModified(new Date()); + this.directory.add(person); + + return person; + } + } + + public void delete(String id){ + this.findById(id).ifPresent(p -> { this.directory.remove(p); }); + } + + private Optional findFirstBy(Predicate findBy){ + return directory.stream() + .filter(findBy) + .findFirst(); + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/META-INF/MANIFEST.MF b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..b3363c0 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Implementation-Title: Directory Service +Implementation-Version: 0.0.1-SNAPSHOT \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..f61eb2c --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{"properties": [{ + "name": "directory.audit", + "type": "java.lang.String", + "description": "Directory Audit, use the @Audit annotation over methods, possible values: on, off" +},{ + "name": "directory.info", + "type": "java.lang.String", + "description": "Directory Audit, print out information about paramters and result. Possible values: short, long (default)" +}]} \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/application.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/application.properties new file mode 100644 index 0000000..267081d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/application.properties @@ -0,0 +1,14 @@ +# SOLUTION: Date format +#spring.jackson.date-format=yyyy-MM-dd + +#directory.audit=off +#directory.info=short + + +#debug=false + +logging.level.org.springframework=INFO + +#spring.main.banner-mode= +#spring.main.web-environment= +#spring.main.sources= \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/banner.txt b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/banner.txt new file mode 100644 index 0000000..d4c2c1a --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/banner.txt @@ -0,0 +1,10 @@ + ____ _ _ ____ _ +| _ \(_)_ __ ___ ___| |_ ___ _ __ _ _ / ___| ___ _ ____ _(_) ___ ___ +| | | | | '__/ _ \/ __| __/ _ \| '__| | | | \___ \ / _ \ '__\ \ / / |/ __/ _ \ +| |_| | | | | __/ (__| || (_) | | | |_| | ___) | __/ | \ V /| | (_| __/ +|____/|_|_| \___|\___|\__\___/|_| \__, | |____/ \___|_| \_/ |_|\___\___| + |___/ +Using Spring Boot: ${spring-boot.version} + +${application.version} +${application.title} \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/static/index.html b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/static/index.html new file mode 100644 index 0000000..af4d664 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/main/resources/static/index.html @@ -0,0 +1 @@ +

Directory Service API

\ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/test/java/io/pivotal/workshop/directory/DirectoryWebFeaturesApplicationTests.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/test/java/io/pivotal/workshop/directory/DirectoryWebFeaturesApplicationTests.java new file mode 100644 index 0000000..aaa9ee6 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/04-spring-boot-features/directory-web-features/src/test/java/io/pivotal/workshop/directory/DirectoryWebFeaturesApplicationTests.java @@ -0,0 +1,16 @@ +package io.pivotal.workshop.directory; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class DirectoryWebFeaturesApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/.mvn/wrapper/maven-wrapper.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c315043 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/build.gradle b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/build.gradle new file mode 100644 index 0000000..cc030d3 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/build.gradle @@ -0,0 +1,37 @@ +buildscript { + ext { + springBootVersion = '2.0.0.M7' + } + repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +dependencies { + compile('org.springframework.boot:spring-boot-starter-groovy-templates') + compile('org.springframework.boot:spring-boot-starter-web') + compile('org.webjars:jquery:2.2.4') + compile('org.webjars:bootstrap:3.3.6') + compile('org.webjars:angularjs:1.5.7') + runtime('org.springframework.boot:spring-boot-devtools') + testCompile('org.springframework.boot:spring-boot-starter-test') +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/cs-code.txt b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/cs-code.txt new file mode 100644 index 0000000..077a1e4 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/cs-code.txt @@ -0,0 +1,5 @@ +class Hello { + static void Main() { + System.Console.Write("Hello World"); + } +} \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/erl-code.txt b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/erl-code.txt new file mode 100644 index 0000000..c45b6f0 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/erl-code.txt @@ -0,0 +1,4 @@ + -module(hello). + -export([hello_world/0]). + + hello_world() -> io:fwrite("hello, world\n"). \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/html-code.txt b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/html-code.txt new file mode 100644 index 0000000..c22655d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/html-code.txt @@ -0,0 +1,5 @@ + + +

Hello World

+ + \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/js-code.txt b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/js-code.txt new file mode 100644 index 0000000..7df7d57 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/js-code.txt @@ -0,0 +1 @@ +console.log("Hello World"); \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/pas-code.txt b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/pas-code.txt new file mode 100644 index 0000000..0564b41 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/code/pas-code.txt @@ -0,0 +1,5 @@ +program HelloWorld; + +begin + writeln('Hello World'); +end. \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradle/wrapper/gradle-wrapper.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..90a06ce --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradlew b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradlew.bat b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/pom.xml b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/pom.xml new file mode 100644 index 0000000..8e40d2d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/pom.xml @@ -0,0 +1,119 @@ + + + 4.0.0 + + io.pivotal.workshop + code-snippet-manager + 0.0.1-SNAPSHOT + jar + + code-snippet-manager + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.M7 + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-groovy-templates + + + org.springframework.boot + spring-boot-starter-web + + + + org.webjars + jquery + 2.2.4 + + + org.webjars + bootstrap + 3.3.6 + + + org.webjars + angularjs + 1.5.7 + + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/CodeSnippetManagerApplication.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/CodeSnippetManagerApplication.java new file mode 100644 index 0000000..f98033d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/CodeSnippetManagerApplication.java @@ -0,0 +1,13 @@ +package io.pivotal.workshop.snippet; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CodeSnippetManagerApplication { + + public static void main(String[] args) { + SpringApplication.run(CodeSnippetManagerApplication.class, args); + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/config/SnippetConfiguration.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/config/SnippetConfiguration.java new file mode 100644 index 0000000..335d056 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/config/SnippetConfiguration.java @@ -0,0 +1,39 @@ +package io.pivotal.workshop.snippet.config; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.pivotal.workshop.snippet.domain.Code; +import io.pivotal.workshop.snippet.domain.Language; +import io.pivotal.workshop.snippet.domain.Snippet; +import io.pivotal.workshop.snippet.repository.SnippetRepository; + +@Configuration +public class SnippetConfiguration { + + @Bean + public CommandLineRunner runner(SnippetRepository snippetRepo) { + return args -> { + @SuppressWarnings("serial") + List snippets = new ArrayList() { + { + add(new Snippet("Hello World", new Language("HTML", "xml"),new Code(new String(Files.readAllBytes(Paths.get("code/html-code.txt")))))); + add(new Snippet("Hello World", new Language("C#", "c#"),new Code(new String(Files.readAllBytes(Paths.get("code/cs-code.txt")))))); + add(new Snippet("Hello World", new Language("Pascal", "py"),new Code(new String(Files.readAllBytes(Paths.get("code/pas-code.txt")))))); + add(new Snippet("Hello World", new Language("Erlang", "erl"),new Code(new String(Files.readAllBytes(Paths.get("code/erl-code.txt")))))); + add(new Snippet("Hello World", new Language("JavaScript", "js"),new Code(new String(Files.readAllBytes(Paths.get("code/js-code.txt")))))); + add(new Snippet("Hello World", new Language("Groovy", "groovy"),new Code("println 'Hello World'"))); + } + }; + + snippetRepo.saveAll(snippets); + + }; + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/controller/SnippetController.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/controller/SnippetController.java new file mode 100644 index 0000000..b8d4acb --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/controller/SnippetController.java @@ -0,0 +1,71 @@ +package io.pivotal.workshop.snippet.controller; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import io.pivotal.workshop.snippet.domain.Snippet; +import io.pivotal.workshop.snippet.repository.LanguageRepository; +import io.pivotal.workshop.snippet.repository.SnippetRepository; + +@RestController +public class SnippetController { + + + private SnippetRepository snippetRepository; + private LanguageRepository languageRepository; + + public SnippetController(SnippetRepository snippetRepository,LanguageRepository languageRepository){ + this.snippetRepository = snippetRepository; + this.languageRepository = languageRepository; + } + + @RequestMapping("/") + public ModelAndView home(){ + assert snippetRepository != null; + + Map model = new HashMap(); + model.put("langs", languageRepository.findAll()); + model.put("snippets", snippetRepository.findAll()); + + return new ModelAndView("views/home",model); + } + + @RequestMapping("/snippets") + public ResponseEntity snippets(){ + assert snippetRepository != null; + return ResponseEntity.ok(snippetRepository.findAll()); + } + + @RequestMapping("/snippets/{id}") + public ResponseEntity snippet(@PathVariable("id") String id){ + assert snippetRepository != null; + return ResponseEntity.ok(snippetRepository.findById(id)); + } + + @RequestMapping(value="/snippets",method = { RequestMethod.POST, RequestMethod.PUT}) + public ResponseEntity upsert(@RequestBody Snippet snippet){ + assert snippetRepository != null; + + Snippet _snippet = snippetRepository.saveAll(snippet); + assert _snippet != null; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setLocation(ServletUriComponentsBuilder + .fromCurrentRequest().path("/" + _snippet.getId()) + .buildAndExpand().toUri()); + + return new ResponseEntity<>(_snippet,httpHeaders,HttpStatus.CREATED); + } + +} \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Code.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Code.java new file mode 100644 index 0000000..fa02b24 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Code.java @@ -0,0 +1,32 @@ +package io.pivotal.workshop.snippet.domain; + +public class Code { + private String id; + private String source; + + public Code() { + this.id = java.util.UUID.randomUUID().toString().replaceAll("-", "");; + } + + public Code(String source) { + this(); + this.source = source; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Language.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Language.java new file mode 100644 index 0000000..28beb5d --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Language.java @@ -0,0 +1,46 @@ +package io.pivotal.workshop.snippet.domain; + +public class Language { + + private String id; + private String name; + private String syntax = "text"; + + public Language() { + this.id = java.util.UUID.randomUUID().toString().replaceAll("-", "");; + } + + public Language(String name) { + this(); + this.name = name; + } + + public Language(String name, String syntax) { + this(name); + this.syntax = syntax; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSyntax() { + return syntax; + } + + public void setSyntax(String syntax) { + this.syntax = syntax; + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Snippet.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Snippet.java new file mode 100644 index 0000000..4cfa0cf --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/domain/Snippet.java @@ -0,0 +1,98 @@ +package io.pivotal.workshop.snippet.domain; + +import java.util.Date; + +public class Snippet { + + private String id; + private String title; + private String keywords; + private String description; + private Language lang; + private Code code; + private Date created; + private Date modified; + + public Snippet() { + this.id = java.util.UUID.randomUUID().toString().replaceAll("-", ""); + this.created = new Date(); + this.modified = new Date(); + } + + public Snippet(String title, String keywords, String description, Language lang, Code code) { + this(); + this.title = title; + this.keywords = keywords; + this.description = description; + this.lang = lang; + this.code = code; + } + + public Snippet(String title, Language lang, Code code) { + this(title, "", "", lang, code); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Language getLang() { + return lang; + } + + public void setLang(Language lang) { + this.lang = lang; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getKeywords() { + return keywords; + } + + public void setKeywords(String keywords) { + this.keywords = keywords; + } + + public Code getCode() { + return code; + } + + public void setCode(Code code) { + this.code = code; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/CodeRepository.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/CodeRepository.java new file mode 100644 index 0000000..e9a5de3 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/CodeRepository.java @@ -0,0 +1,54 @@ +package io.pivotal.workshop.snippet.repository; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Repository; + +import io.pivotal.workshop.snippet.domain.Code; + +@Repository +public class CodeRepository implements SimpleRepository{ + + private List codes = new ArrayList<>(); + + public List findAll() { + return codes; + } + + @Override + public void saveAll(Collection items) { + this.codes.addAll(items); + } + + @Override + public Code saveAll(Code item) { + assert item.getSource() != null; + + Code code = findById(item.getId()); + + if(code == null){ + this.codes.add(item); + return item; + } + else { + code.setSource(item.getSource()); + return code; + } + } + + @Override + public Code findById(String id) { + Optional code = codes + .stream() + .filter(c -> c.getId().equals(id)) + .findFirst(); + if (code.isPresent()) return code.get(); + + return null; + } + + +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/LanguageRepository.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/LanguageRepository.java new file mode 100644 index 0000000..6a33b4a --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/LanguageRepository.java @@ -0,0 +1,51 @@ +package io.pivotal.workshop.snippet.repository; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.StreamSupport; + +import org.springframework.stereotype.Repository; + +import io.pivotal.workshop.snippet.domain.Language; + +@Repository +public class LanguageRepository implements SimpleRepository{ + + private List languages = new ArrayList<>(); + + public Iterable findAll(){ + return languages; + } + + public void saveAll(Collection languages){ + this.languages.addAll(languages); + } + + public Language findById(String name) { + Optional language = StreamSupport + .stream(this.languages.spliterator(), false) + .filter(lang -> lang.getName().equals(name)) + .findFirst(); + + if (language.isPresent()) return language.get(); + + return null; + } + + public Language saveAll(Language item) { + assert item.getName() != null; + + Language language = this.findById(item.getName()); + + if(language == null) { + this.languages.add(item); + return item; + }else { + language.setName(item.getName()); + language.setSyntax(item.getSyntax()); + return language; + } + } +} \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/SimpleRepository.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/SimpleRepository.java new file mode 100644 index 0000000..2be274e --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/SimpleRepository.java @@ -0,0 +1,11 @@ +package io.pivotal.workshop.snippet.repository; + +import java.util.Collection; + +public interface SimpleRepository { + + Iterable findAll(); + void saveAll(Collection items); + T saveAll(T item); + T findById(String id); +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/SnippetRepository.java b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/SnippetRepository.java new file mode 100644 index 0000000..d305802 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/java/io/pivotal/workshop/snippet/repository/SnippetRepository.java @@ -0,0 +1,62 @@ +package io.pivotal.workshop.snippet.repository; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Repository; + +import io.pivotal.workshop.snippet.domain.Snippet; + +@Repository +public class SnippetRepository implements SimpleRepository{ + + private List snippets = new ArrayList<>(); + + @Override + public Snippet saveAll(Snippet snippet){ + assert snippet.getTitle() != null; + assert snippet.getCode() != null; + assert snippet.getLang() != null; + + Snippet _snippet = null; + + if (snippet.getId() == null) { + _snippet = new Snippet(snippet.getTitle(), snippet.getLang(),snippet.getCode()); + + } else { + _snippet = this.findById(snippet.getId()); + if(_snippet != null){ + _snippet.setTitle(snippet.getTitle()); + _snippet.setCode(snippet.getCode()); + _snippet.setLang(snippet.getLang()); + _snippet.setModified(new Date()); + } + } + + return _snippet; + } + + @Override + public Iterable findAll(){ + return this.snippets; + } + + @Override + public Snippet findById(String id){ + Optional result = snippets.stream() + .filter(snippet -> snippet.getId().equals(id)) + .findFirst(); + + if (result.isPresent()) return result.get(); + + return null; + } + + @Override + public void saveAll(Collection items) { + this.snippets.addAll(items); + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/application.properties b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/application.properties new file mode 100644 index 0000000..a4c32e9 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.jackson.date-format=yyyy-mm-dd \ No newline at end of file diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/css/offcanvas.css b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/css/offcanvas.css new file mode 100644 index 0000000..2a72199 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/css/offcanvas.css @@ -0,0 +1,59 @@ +/* + * Style tweaks + * -------------------------------------------------- + */ +html, +body { + overflow-x: hidden; /* Prevent scroll on narrow devices */ +} +body { + padding-top: 70px; +} +footer { + padding: 30px 0; +} + +/* + * Off Canvas + * -------------------------------------------------- + */ +@media screen and (max-width: 767px) { + .row-offcanvas { + position: relative; + -webkit-transition: all .25s ease-out; + -o-transition: all .25s ease-out; + transition: all .25s ease-out; + } + + .row-offcanvas-right { + right: 0; + } + + .row-offcanvas-left { + left: 0; + } + + .row-offcanvas-right + .sidebar-offcanvas { + right: -50%; /* 6 columns */ + } + + .row-offcanvas-left + .sidebar-offcanvas { + left: -50%; /* 6 columns */ + } + + .row-offcanvas-right.active { + right: 50%; /* 6 columns */ + } + + .row-offcanvas-left.active { + left: 50%; /* 6 columns */ + } + + .sidebar-offcanvas { + position: absolute; + top: 0; + width: 50%; /* 6 columns */ + } +} diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/css/theme.css b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/css/theme.css new file mode 100644 index 0000000..bbd2546 --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/css/theme.css @@ -0,0 +1,238 @@ +.syntaxhighlighter a, +.syntaxhighlighter div, +.syntaxhighlighter code, +.syntaxhighlighter table, +.syntaxhighlighter table td, +.syntaxhighlighter table tr, +.syntaxhighlighter table tbody, +.syntaxhighlighter table thead, +.syntaxhighlighter table caption, +.syntaxhighlighter textarea { + -moz-border-radius: 0 0 0 0 !important; + -webkit-border-radius: 0 0 0 0 !important; + background: none !important; + border: 0 !important; + bottom: auto !important; + float: none !important; + height: auto !important; + left: auto !important; + line-height: 1.1em !important; + margin: 0 !important; + outline: 0 !important; + overflow: visible !important; + padding: 0 !important; + position: static !important; + right: auto !important; + text-align: left !important; + top: auto !important; + vertical-align: baseline !important; + width: auto !important; + box-sizing: content-box !important; + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; + font-weight: normal !important; + font-style: normal !important; + font-size: 1em !important; + min-height: inherit !important; + min-height: auto !important; } + +.syntaxhighlighter { + width: 100% !important; + margin: 1em 0 1em 0 !important; + position: relative !important; + overflow: auto !important; + font-size: 1em !important; } + .syntaxhighlighter .container:before, .syntaxhighlighter .container:after { + content: none !important; } + .syntaxhighlighter.source { + overflow: hidden !important; } + .syntaxhighlighter .bold { + font-weight: bold !important; } + .syntaxhighlighter .italic { + font-style: italic !important; } + .syntaxhighlighter .line { + white-space: pre !important; } + .syntaxhighlighter table { + width: 100% !important; } + .syntaxhighlighter table caption { + text-align: left !important; + padding: .5em 0 0.5em 1em !important; } + .syntaxhighlighter table td.code { + width: 100% !important; } + .syntaxhighlighter table td.code .container { + position: relative !important; } + .syntaxhighlighter table td.code .container textarea { + box-sizing: border-box !important; + position: absolute !important; + left: 0 !important; + top: 0 !important; + width: 100% !important; + height: 100% !important; + border: none !important; + background: white !important; + padding-left: 1em !important; + overflow: hidden !important; + white-space: pre !important; } + .syntaxhighlighter table td.gutter .line { + text-align: right !important; + padding: 0 0.5em 0 1em !important; } + .syntaxhighlighter table td.code .line { + padding: 0 1em !important; } + .syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { + padding-left: 0em !important; } + .syntaxhighlighter.show { + display: block !important; } + .syntaxhighlighter.collapsed table { + display: none !important; } + .syntaxhighlighter.collapsed .toolbar { + padding: 0.1em 0.8em 0em 0.8em !important; + font-size: 1em !important; + position: static !important; + width: auto !important; + height: auto !important; } + .syntaxhighlighter.collapsed .toolbar span { + display: inline !important; + margin-right: 1em !important; } + .syntaxhighlighter.collapsed .toolbar span a { + padding: 0 !important; + display: none !important; } + .syntaxhighlighter.collapsed .toolbar span a.expandSource { + display: inline !important; } + .syntaxhighlighter .toolbar { + position: absolute !important; + right: 1px !important; + top: 1px !important; + width: 11px !important; + height: 11px !important; + font-size: 10px !important; + z-index: 10 !important; } + .syntaxhighlighter .toolbar span.title { + display: inline !important; } + .syntaxhighlighter .toolbar a { + display: block !important; + text-align: center !important; + text-decoration: none !important; + padding-top: 1px !important; } + .syntaxhighlighter .toolbar a.expandSource { + display: none !important; } + .syntaxhighlighter.ie { + font-size: .9em !important; + padding: 1px 0 1px 0 !important; } + .syntaxhighlighter.ie .toolbar { + line-height: 8px !important; } + .syntaxhighlighter.ie .toolbar a { + padding-top: 0px !important; } + .syntaxhighlighter.printing .line.alt1 .content, + .syntaxhighlighter.printing .line.alt2 .content, + .syntaxhighlighter.printing .line.highlighted .number, + .syntaxhighlighter.printing .line.highlighted.alt1 .content, + .syntaxhighlighter.printing .line.highlighted.alt2 .content { + background: none !important; } + .syntaxhighlighter.printing .line .number { + color: #bbbbbb !important; } + .syntaxhighlighter.printing .line .content { + color: black !important; } + .syntaxhighlighter.printing .toolbar { + display: none !important; } + .syntaxhighlighter.printing a { + text-decoration: none !important; } + .syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { + color: black !important; } + .syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { + color: #008200 !important; } + .syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { + color: blue !important; } + .syntaxhighlighter.printing .keyword { + color: #006699 !important; + font-weight: bold !important; } + .syntaxhighlighter.printing .preprocessor { + color: gray !important; } + .syntaxhighlighter.printing .variable { + color: #aa7700 !important; } + .syntaxhighlighter.printing .value { + color: #009900 !important; } + .syntaxhighlighter.printing .functions { + color: #ff1493 !important; } + .syntaxhighlighter.printing .constants { + color: #0066cc !important; } + .syntaxhighlighter.printing .script { + font-weight: bold !important; } + .syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { + color: gray !important; } + .syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { + color: #ff1493 !important; } + .syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { + color: red !important; } + .syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { + color: black !important; } + +.syntaxhighlighter { + background-color: white !important; } + .syntaxhighlighter .line.alt1 { + background-color: white !important; } + .syntaxhighlighter .line.alt2 { + background-color: white !important; } + .syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { + background-color: #e0e0e0 !important; } + .syntaxhighlighter .line.highlighted.number { + color: black !important; } + .syntaxhighlighter table caption { + color: black !important; } + .syntaxhighlighter table td.code .container textarea { + background: white; + color: black; } + .syntaxhighlighter .gutter { + color: #afafaf !important; } + .syntaxhighlighter .gutter .line { + border-right: 3px solid #6ce26c !important; } + .syntaxhighlighter .gutter .line.highlighted { + background-color: #6ce26c !important; + color: white !important; } + .syntaxhighlighter.printing .line .content { + border: none !important; } + .syntaxhighlighter.collapsed { + overflow: visible !important; } + .syntaxhighlighter.collapsed .toolbar { + color: #00f !important; + background: #fff !important; + border: 1px solid #6ce26c !important; } + .syntaxhighlighter.collapsed .toolbar a { + color: #00f !important; } + .syntaxhighlighter.collapsed .toolbar a:hover { + color: #f00 !important; } + .syntaxhighlighter .toolbar { + color: #fff !important; + background: #6ce26c !important; + border: none !important; } + .syntaxhighlighter .toolbar a { + color: #fff !important; } + .syntaxhighlighter .toolbar a:hover { + color: #000 !important; } + .syntaxhighlighter .plain, .syntaxhighlighter .plain a { + color: black !important; } + .syntaxhighlighter .comments, .syntaxhighlighter .comments a { + color: #008200 !important; } + .syntaxhighlighter .string, .syntaxhighlighter .string a { + color: blue !important; } + .syntaxhighlighter .keyword { + font-weight: bold !important; + color: #006699 !important; } + .syntaxhighlighter .preprocessor { + color: gray !important; } + .syntaxhighlighter .variable { + color: #aa7700 !important; } + .syntaxhighlighter .value { + color: #009900 !important; } + .syntaxhighlighter .functions { + color: #ff1493 !important; } + .syntaxhighlighter .constants { + color: #0066cc !important; } + .syntaxhighlighter .script { + font-weight: bold !important; + color: #006699 !important; + background-color: none !important; } + .syntaxhighlighter .color1, .syntaxhighlighter .color1 a { + color: gray !important; } + .syntaxhighlighter .color2, .syntaxhighlighter .color2 a { + color: #ff1493 !important; } + .syntaxhighlighter .color3, .syntaxhighlighter .color3 a { + color: red !important; } diff --git a/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/js/syntaxhighlighter.js b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/js/syntaxhighlighter.js new file mode 100644 index 0000000..006e4ab --- /dev/null +++ b/src/main/UploadedContent/Spring-Boot-2.0.0.M7.a.RELEASE/05-spring-boot-web/code-snippet-manager/src/main/resources/static/js/syntaxhighlighter.js @@ -0,0 +1,5332 @@ +/*! + * SyntaxHighlighter + * https://github.com/syntaxhighlighter/syntaxhighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 4.0.1 (Tue, 05 Jul 2016 16:28:21 GMT) + * + * @copyright + * Copyright (C) 2004-2016 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; +/******/ +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.loaded = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _core = __webpack_require__(1); + + Object.keys(_core).forEach(function (key) { + if (key === "default") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _core[key]; + } + }); + }); + + var _domready = __webpack_require__(52); + + var _domready2 = _interopRequireDefault(_domready); + + var _core2 = _interopRequireDefault(_core); + + var _dasherize = __webpack_require__(53); + + var dasherize = _interopRequireWildcard(_dasherize); + + function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + // configured through the `--compat` parameter. + if (false) { + require('./compatibility_layer_v3'); + } + + (0, _domready2.default)(function () { + return _core2.default.highlight(dasherize.object(window.syntaxhighlighterConfig || {})); + }); + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + var optsParser = __webpack_require__(2), + match = __webpack_require__(5), + Renderer = __webpack_require__(9).default, + utils = __webpack_require__(10), + transformers = __webpack_require__(11), + dom = __webpack_require__(17), + config = __webpack_require__(18), + defaults = __webpack_require__(19), + HtmlScript = __webpack_require__(20); + + var sh = { + Match: match.Match, + Highlighter: __webpack_require__(22), + + config: __webpack_require__(18), + regexLib: __webpack_require__(3).commonRegExp, + + /** Internal 'global' variables. */ + vars: { + discoveredBrushes: null, + highlighters: {} + }, + + /** This object is populated by user included external brush files. */ + brushes: {}, + + /** + * Finds all elements on the page which should be processes by SyntaxHighlighter. + * + * @param {Object} globalParams Optional parameters which override element's + * parameters. Only used if element is specified. + * + * @param {Object} element Optional element to highlight. If none is + * provided, all elements in the current document + * are returned which qualify. + * + * @return {Array} Returns list of { target: DOMElement, params: Object } objects. + */ + findElements: function findElements(globalParams, element) { + var elements = element ? [element] : utils.toArray(document.getElementsByTagName(sh.config.tagName)), + conf = sh.config, + result = []; + + // support for + + + + + + +
+
+
+
+

Review all the necessary steps required to create a simple web application with the Spring Framework 5.

+
+
+

Time: 25 minutes.

+
+ +
+
+
+

Requirements:

+
+
+ +
+
+
+
+

Simple Spring Web Application with Thymeleaf

+
+
+
    +
  1. +

    To create a simple Spring Web application, open a terminal window and execute the following statement:

    +
    +
    +
    mvn archetype:generate -DgroupId=io.pivotal.workshop -DartifactId=simple-spring-webapp -Dversion=1.0-SNAPSHOT -DinteractiveMode=false -DarchetypeArtifactId=maven-archetype-webapp
    +
    +
    +
    + + + + + +
    + + +If you have a Maven version below 3.x, you need to use the command mvn archetype:create instead. +
    +
    +
  2. +
  3. +

    Create the missing Java EE structure (ex: src/main/java, src/main/webapp/WEB-INF, etc.)

    +
  4. +
  5. +

    Create the web controller SimpleController class in src/main/java/io/pivotal/workshop/web folder.

    +
    +
    io.pivotal.workshop.web.SimpleController.java
    +
    +
    package io.pivotal.workshop.web;
    +
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +import org.springframework.stereotype.Controller;
    +import org.springframework.web.bind.annotation.RequestMapping;
    +import org.springframework.web.bind.annotation.RequestMethod;
    +import org.springframework.web.servlet.ModelAndView;
    +
    +@Controller
    +public class SimpleController{
    +
    +        Logger log = LoggerFactory.getLogger(SimpleController.class);
    +
    +        @RequestMapping("/")
    +        public ModelAndView index(){
    +                log.debug("About to redirect...");
    +                return new ModelAndView("redirect:/showMessage");
    +        }
    +
    +        @RequestMapping(value="/showMessage",method = RequestMethod.GET)
    +        public ModelAndView helloWorld(){
    +                final String message = "Simple Spring MVC Web App with Thymeleaf and Spring 5";
    +                 log.debug("Showing the Message: " + message);
    +                ModelAndView model = new ModelAndView("showMessage");
    +                model.addObject("message", message);
    +                return model;
    +        }
    +}
    +
    +
    +
  6. +
  7. +

    Modify the src/main/webapp/WEB-INF/web.xml file.

    +
    +
    web.xml
    +
    +
    <!DOCTYPE web-app PUBLIC
    + "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    + "http://java.sun.com/dtd/web-app_2_3.dtd" >
    +
    +<web-app>
    +  <display-name>Simple Spring Web Application</display-name>
    +
    +  <servlet>
    +      <servlet-name>dispatcherServlet</servlet-name>
    +      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    +      <load-on-startup>1</load-on-startup>
    +  </servlet>
    +
    +  <servlet-mapping>
    +      <servlet-name>dispatcherServlet</servlet-name>
    +      <url-pattern>/</url-pattern>
    +  </servlet-mapping>
    +
    +</web-app>
    +
    +
    +
  8. +
  9. +

    Create the src/main/webapp/WEB-INF/dispatcherServlet-servlet.xml file.

    +
    +
    dispatcherServlet-servlet.xml
    +
    +
    <?xml version="1.0" encoding="UTF-8"?>
    +
    +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +        xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    +        xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
    +                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    +                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    +
    +        <!--
    +        DispatcherServlet creates its own WebApplicationContext and the handlers/controllers/view-resolvers are managed
    +        by this context.
    +        -->
    +
    +         <context:component-scan base-package="io.pivotal.workshop.web" />
    +
    +        <!-- Useful when only JSP is required. The javax.servlet.jsp:avax.servlet.jsp-api:2.3.1 dendency is necessary-->
    +        <!--
    +        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    +                <property name="prefix" value="/WEB-INF/view/"/>
    +                <property name="suffix" value=".jsp"/>
    +        </bean>
    +        -->
    +
    +        <!-- Thymeleaf Configuration -->
    +        <bean id="templateResolver" class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
    +                <property name="prefix" value="/WEB-INF/view/" />
    +                <property name="suffix" value=".html" />
    +                <property name="templateMode" value="HTML5" />
    +        </bean>
    +
    +        <bean id="templateEngine" class="org.thymeleaf.spring5.SpringTemplateEngine">
    +                <property name="templateResolver" ref="templateResolver" />
    +                <property name="enableSpringELCompiler" value="true" />
    +        </bean>
    +
    +        <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
    +                <property name="templateEngine" ref="templateEngine" />
    +</bean>
    +
    +</beans>
    +
    +
    +
  10. +
  11. +

    Modify the src/main/webapp/index.html file and move it to the src/main/webapp/WEB-INF/view directory.

    +
    +
    index.html
    +
    +
    <!DOCTYPE html>
    +
    +<html lang="en" xmlns:th="http://www.thymeleaf.org">
    +        <head>
    +                <meta charset="utf-8">
    +                <title>Welcome</title>
    +        </head>
    +        <body>
    +                <p>This page never will show up</p>
    +        </body>
    +</html>
    +
    +
    +
    +

    This page will never show up, because the handler will do a redirect to the showMessage.html.

    +
    +
  12. +
  13. +

    Create the src/main/webapp/WEB-INF/view/showMessage.html file.

    +
    +
    showMessage.jsp
    +
    +
    <!DOCTYPE html>
    +
    +<html lang="en" xmlns:th="http://www.thymeleaf.org">
    +        <head>
    +                <meta charset="utf-8">
    +                <title>Welcome</title>
    +        </head>
    +        <body style="font-family: Verdana, sans-serif;">
    +        <h2><p th:text="${message}"/></h2>
    +        </body>
    +</html>
    +
    +
    +
  14. +
  15. +

    Create the logging configuration. Create a logback.xml file in the src/main/resources directory.

    +
    +
    logback.xml
    +
    +
    <configuration>
    +    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    +        <encoder>
    +            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    +        </encoder>
    +    </appender>
    +
    +    <logger name="io.pivotal.workshop" level="DEBUG"/>
    +        <logger name="org.thymeleaf" level="OFF"/>
    +
    +    <root level="INFO">
    +        <appender-ref ref="STDOUT" />
    +    </root>
    +</configuration>
    +
    +
    +
  16. +
  17. +

    Modify the pom.xml file. Add the necessary dependencies. See all the Spring 5 dependencies.

    +
    +
    pom.xml
    +
    +
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    +        <modelVersion>4.0.0</modelVersion>
    +        <groupId>io.pivotal.workshop</groupId>
    +        <artifactId>simple-spring-webapp</artifactId>
    +        <packaging>war</packaging>
    +        <version>1.0-SNAPSHOT</version>
    +        <name>simple-spring-webapp Maven Webapp</name>
    +        <url>http://maven.apache.org</url>
    +        <parent>
    +                <groupId>io.pivotal.education.boot</groupId>
    +                <artifactId>parentProject</artifactId>
    +                <version>1.0.a.RELEASE</version>
    +                <relativePath>../../</relativePath>
    +        </parent>
    +        <properties>
    +
    +                <!-- Generic properties -->
    +                <java.version>1.8</java.version>
    +
    +                <!-- Web -->
    +                <servlet.version>3.1.0</servlet.version>
    +
    +                <!-- Spring -->
    +                <spring-framework.version>5.0.1.RELEASE</spring-framework.version>
    +
    +                <!-- Thymeleaf -->
    +                <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
    +
    +                <!--  Logging -->
    +                <slf4j.version>1.7.25</slf4j.version>
    +                <logback.version>1.2.3</logback.version>
    +        </properties>
    +
    +        <dependencies>
    +
    +                <!-- Spring MVC -->
    +                <dependency>
    +                        <groupId>org.springframework</groupId>
    +                        <artifactId>spring-context</artifactId>
    +                        <version>${spring-framework.version}</version>
    +                </dependency>
    +                <dependency>
    +                        <groupId>org.springframework</groupId>
    +                        <artifactId>spring-aop</artifactId>
    +                        <version>${spring-framework.version}</version>
    +                </dependency>
    +                <dependency>
    +                        <groupId>org.springframework</groupId>
    +                        <artifactId>spring-webmvc</artifactId>
    +                        <version>${spring-framework.version}</version>
    +                </dependency>
    +                <dependency>
    +                        <groupId>org.springframework</groupId>
    +                        <artifactId>spring-web</artifactId>
    +                        <version>${spring-framework.version}</version>
    +                </dependency>
    +
    +                <dependency>
    +                        <groupId>org.thymeleaf</groupId>
    +                        <artifactId>thymeleaf</artifactId>
    +                        <version>${thymeleaf.version}</version>
    +                </dependency>
    +                <dependency>
    +                        <groupId>org.thymeleaf</groupId>
    +                        <artifactId>thymeleaf-spring5</artifactId>
    +                        <version>${thymeleaf.version}</version>
    +                </dependency>
    +
    +
    +                <dependency>
    +                        <groupId>org.slf4j</groupId>
    +                        <artifactId>slf4j-api</artifactId>
    +                        <version>${slf4j.version}</version>
    +                </dependency>
    +                <dependency>
    +                        <groupId>ch.qos.logback</groupId>
    +                        <artifactId>logback-classic</artifactId>
    +                        <version>${logback.version}</version>
    +                </dependency>
    +
    +                <!-- Other Web dependencies -->
    +                <dependency>
    +                        <groupId>javax.servlet</groupId>
    +                        <artifactId>javax.servlet-api</artifactId>
    +                        <version>${servlet.version}</version>
    +                        <scope>provided</scope>
    +                </dependency>
    +        </dependencies>
    +
    +        <build>
    +                <finalName>simple-spring-webapp</finalName>
    +        </build>
    +</project>
    +
    +
    +
  18. +
+
+
+

Building the Spring Web Application

+
+

To build the Spring Web application, execute the following command:

+
+
+
+
mvn clean compile package
+
+
+
+

the above command will generate the target/simple-spring-webapp.war.

+
+
+
+

Deploying the Spring Web Application

+
+

To deploy the application, install any application container that supports the Java Servlets specification.

+
+
+

For example, to deploy to Apache Tomcat, copy the target/simple-spring-webapp.war file into the $TOMCAT-INSTALLATION/webapps/ directory and start the container. Then, go to your browser and hit the http://localhost:8080/simple-spring-webapp/ URL.

+
+
+

You should see the text: Simple Spring MVC Web App with Thymeleaf and Spring 5.

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/02-spring-boot-overview.html b/src/main/UploadedContent/student-lab-instructions/02-spring-boot-overview.html new file mode 100644 index 0000000..ef84dc4 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/02-spring-boot-overview.html @@ -0,0 +1,1078 @@ + + + + + + + +Spring Boot - Create a Simple Rest API + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with the Spring Initializr interface by creating a simple Spring Boot Web application.

+
+
+

Time: 25 minutes.

+
+ +
+
+
+

Directory Web App

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Directory Web App Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Directory Web App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    directory-web

    Name:

    directory-web

    Package Name:

    io.pivotal.workshop.directory

    Dependencies:

    Web

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Once imported, review the pom.xml file (or build.gradle).

    +
    +
    maven - pom.xml
    +
    +
    <?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns="http://maven.apache.org/POM/4.0.0"
    +    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +        xsi:schemaLocation="
    +    http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +        <modelVersion>4.0.0</modelVersion>
    +
    +        <groupId>io.pivotal.workshop</groupId>
    +        <artifactId>directory-web-app</artifactId>
    +        <version>0.0.1-SNAPSHOT</version>
    +        <packaging>jar</packaging>
    +
    +        <name>directory-web-app</name>
    +        <description>Demo project for Spring Boot</description>
    +
    +        <parent>
    +                <groupId>org.springframework.boot</groupId>
    +                <artifactId>spring-boot-starter-parent</artifactId>
    +                <version>2.0.0.RC1</version>
    +                <relativePath/> <!-- lookup parent from repository -->
    +        </parent>
    +
    +        <properties>
    +                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +                <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    +                <java.version>1.8</java.version>
    +        </properties>
    +
    +        <dependencies>
    +                <dependency>
    +                        <groupId>org.springframework.boot</groupId>
    +                        <artifactId>spring-boot-starter-web</artifactId>
    +                </dependency>
    +
    +                <dependency>
    +                        <groupId>org.springframework.boot</groupId>
    +                        <artifactId>spring-boot-starter-test</artifactId>
    +                        <scope>test</scope>
    +                </dependency>
    +        </dependencies>
    +
    +        <build>
    +                <plugins>
    +                        <plugin>
    +                                <groupId>org.springframework.boot</groupId>
    +                                <artifactId>spring-boot-maven-plugin</artifactId>
    +                        </plugin>
    +                </plugins>
    +        </build>
    +
    +    <repositories>
    +                <repository>
    +                        <id>spring-milestones</id>
    +                        <name>Spring Milestones</name>
    +                        <url>https://repo.spring.io/libs-milestone</url>
    +                        <snapshots>
    +                                <enabled>false</enabled>
    +                        </snapshots>
    +                </repository>
    +        </repositories>
    +
    +</project>
    +
    +
    +
    +

    Remember the Spring Boot components:

    +
    +
    +
      +
    • +

      spring-boot-starter-parent

      +
    • +
    • +

      dependencies: spring-boot-starter-web

      +
    • +
    • +

      plugin: spring-boot-maven-plugin

      +
    • +
    +
    +
    +
    gradle - build.gradle
    +
    +
    buildscript {
    +        ext {
    +                springBootVersion = '2.0.0.RC1'
    +        }
    +        repositories {
    +                mavenCentral()
    +        maven { url "https://repo.spring.io/snapshot" }
    +                maven { url "https://repo.spring.io/milestone" }
    +        }
    +        dependencies {
    +                classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    +        }
    +}
    +
    +apply plugin: 'java'
    +apply plugin: 'eclipse'
    +apply plugin: 'org.springframework.boot'
    +apply plugin: 'io.spring.dependency-management'
    +
    +version = '0.0.1-SNAPSHOT'
    +sourceCompatibility = 1.8
    +
    +repositories {
    +        mavenCentral()
    +    maven { url "https://repo.spring.io/snapshot" }
    +        maven { url "https://repo.spring.io/milestone" }
    +}
    +
    +
    +dependencies {
    +        compile('org.springframework.boot:spring-boot-starter-web')
    +        testCompile('org.springframework.boot:spring-boot-starter-test')
    +}
    +
    +
    +
  16. +
  17. +

    Create the domain io.pivotal.workshop.directory.domain.Person class.

    +
    +
    io.pivotal.workshop.directory.domain.Person.java
    +
    +
    package io.pivotal.workshop.directory.domain;
    +
    +import java.text.ParseException;
    +import java.text.SimpleDateFormat;
    +import java.util.Date;
    +import java.util.UUID;
    +
    +import javax.validation.constraints.NotNull;
    +
    +public class Person {
    +
    +        private SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd");
    +
    +        private String id;
    +
    +        @NotNull
    +        private String email;
    +        @NotNull
    +        private String name;
    +        @NotNull
    +        private String password;
    +        private Date birthday;
    +        private Date created;
    +        private Date modified;
    +
    +        public Person(){
    +                this.id = UUID.randomUUID().toString().replaceAll("-", "");
    +                this.created = new Date();
    +                this.modified = new Date();
    +        }
    +
    +        public Person(String email, String name, String password, String birthday) {
    +                this();
    +                this.email = email;
    +                this.name = name;
    +                this.password = password;
    +
    +                try {
    +                        this.birthday = date.parse(birthday);
    +                } catch (ParseException e) {
    +                        this.birthday = null;
    +                }
    +        }
    +
    +        public Person(String email, String name, String password, Date birthday) {
    +                this();
    +                this.email = email;
    +                this.name = name;
    +                this.password = password;
    +                this.birthday = birthday;
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getEmail() {
    +                return email;
    +        }
    +
    +        public void setEmail(String email) {
    +                this.email = email;
    +        }
    +
    +        public String getName() {
    +                return name;
    +        }
    +
    +        public void setName(String name) {
    +                this.name = name;
    +        }
    +
    +        public String getPassword() {
    +                return password;
    +        }
    +
    +        public void setPassword(String password) {
    +                this.password = password;
    +        }
    +
    +        public Date getBirthday() {
    +                return birthday;
    +        }
    +
    +        public void setBirthday(Date birthday) {
    +                this.birthday = birthday;
    +        }
    +
    +        public Date getCreated() {
    +                return created;
    +        }
    +
    +        public void setCreated(Date created) {
    +                this.created = created;
    +        }
    +
    +        public Date getModified() {
    +                return modified;
    +        }
    +
    +        public void setModified(Date modified) {
    +                this.modified = modified;
    +        }
    +
    +}
    +
    +
    +
  18. +
  19. +

    Create the io.pivotal.workshop.directory.repository.DirectoryRepository class. This is you in-memory persistence.

    +
    +
    io.pivotal.workshop.directory.repository.DirectoryRepository.java
    +
    +
    package io.pivotal.workshop.directory.repository;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +import org.springframework.stereotype.Repository;
    +
    +import io.pivotal.workshop.directory.domain.Person;
    +
    +@Repository
    +public class DirectoryRepository {
    +
    +
    +        @SuppressWarnings("serial")
    +        private List<Person> directory = new ArrayList<Person>(){{
    +        add(new Person("john@email.com","John S","password","1985-11-10"));
    +                add(new Person("mike@email.com","Mike H","password","1984-12-02"));
    +                add(new Person("dan@email.com","Dan B","password","1983-03-07"));
    +                add(new Person("bill@email.com","Bill G","password","1983-06-12"));
    +                add(new Person("mark@email.com","Mark S","password","1986-02-22"));
    +        }};
    +
    +        public Iterable<Person> findAll(){
    +                return this.directory;
    +        }
    +
    +}
    +
    +
    +
  20. +
  21. +

    Create the io.pivotal.workshop.directory.controller.DirectoryController class. This will handle your REST API.

    +
    +
    io.pivotal.workshop.directory.controller.DirectoryController.java
    +
    +
    package io.pivotal.workshop.directory.controller;
    +
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.http.ResponseEntity;
    +import org.springframework.web.bind.annotation.RequestMapping;
    +import org.springframework.web.bind.annotation.RestController;
    +
    +import io.pivotal.workshop.directory.domain.Person;
    +import io.pivotal.workshop.directory.repository.DirectoryRepository;
    +
    +@RestController
    +public class DirectoryController {
    +
    +        private DirectoryRepository repo;
    +
    +        @Autowired
    +        public DirectoryController(DirectoryRepository repo){
    +                this.repo = repo;
    +        }
    +
    +        @RequestMapping("/directory")
    +        public ResponseEntity<Iterable<Person>> findAll(){
    +                return ResponseEntity.ok(this.repo.findAll());
    +        }
    +
    +
    +}
    +
    +
    +
  22. +
  23. +

    Run your application and test the /directory endpoint by open a browser and hit: http://localhost:8080/directory

    +
  24. +
+
+
+ + + + + +
+
Tip
+
+If you are not using any IDE, then you can run your application with maven: ./mvnw spring-boot:run or if you are using gradle: ./gradlew bootRun +
+
+
+
+
+

Challenges

+
+
+

So far this is a very simple application, but still missing some of the HTTP request methods:

+
+
+
    +
  • +

    Add a index.html page to be render as Home page (tip: src/main/resources/static).

    +
  • +
  • +

    Add the POST method for adding a new person (tip: Use the @RequestBody annotation).

    +
  • +
  • +

    Add the PUT method for updating a new person (tip: Use the @RequestBody annotation).

    +
  • +
  • +

    Add the DELETE method for removing a person by id (tip: Use the @PathVariable annotation).

    +
  • +
  • +

    Add a GET method for search by email (tip: Use the @RequestParam annotation).

    +
  • +
  • +

    Add a GET method for finding by Id (tip: Use the @PathVariable annotation).

    +
  • +
+
+
+

do any necessary changes to the classes.

+
+
+

[EXTRA - OPTIONAL] Challenges

+
+
    +
  • +

    Mask the password when getting a directory JSON object.

    +
  • +
  • +

    The /directory endpoint response as a JSON object and the dates are show as a long type. Modify it to get a formatted 'yyyy-MM-dd' date.

    +
  • +
  • +

    By default Spring Boot serialize automatically a JSON object, add a XML serialization too.

    +
  • +
+
+
+
+

HOMEWORK

+
+
    +
  • +

    Add validation to the Person object when doing POST or UPDATE.

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/03-spring-boot-internals.html b/src/main/UploadedContent/student-lab-instructions/03-spring-boot-internals.html new file mode 100644 index 0000000..fb7f066 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/03-spring-boot-internals.html @@ -0,0 +1,952 @@ + + + + + + + +Spring Boot Internals + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with the Spring Boot Internals by using the @Conditional annotations.

+
+
+

Time: 35 minutes.

+
+ +
+
+
+

Directory Web Internals App

+
+
+

This application will use a custom @Audit annotation over any method and log the execution.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Directory Web App Project metadata with (See Figure 1):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Directory Web App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    directory-web-internals

    Name:

    directory-web-internals

    Package Name:

    io.pivotal.workshop.directory

    Dependencies:

    Web

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    You can copy all the code from the previous lab (we are going to use it).

    +
  16. +
  17. +

    Add the following dependency to your pom.xml or build.gradle:

    +
    +
    maven - pom.xml
    +
    +
    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-aop</artifactId>
    +</dependency>
    +
    +
    +
    +
    gradle - build.gradle
    +
    +
    compile('org.springframework.boot:spring-boot-starter-aop')
    +
    +
    +
    +

    we are going to use AOP, Aspect Oriented Programming.

    +
    +
    + + + + + +
    +
    Tip
    +
    +Adding dependencies like this is commonly used. You don’t need to re-gerate the project if you forgot to add a dependency. There is a naming convention for Spring Boot: spring-boot-starter-[TECHNOLOGY]. +
    +
    +
  18. +
  19. +

    Create the custom @Audit annotation by adding the io.pivotal.workshop.directory.annotation.Audit interface to the project.

    +
    +
    +
    package io.pivotal.workshop.directory.annotation;
    +
    +import java.lang.annotation.ElementType;
    +import java.lang.annotation.Retention;
    +import java.lang.annotation.RetentionPolicy;
    +import java.lang.annotation.Target;
    +
    +@Target(ElementType.METHOD)
    +@Retention(RetentionPolicy.RUNTIME)
    +public @interface Audit {
    +        Auditor value() default Auditor.NOTHING;
    +}
    +
    +
    +
  20. +
  21. +

    Create the io.pivotal.workshop.directory.annotation.Auditor enum, that is part of the @Audit annotation possible values.

    +
    +
    +
    package io.pivotal.workshop.directory.annotation;
    +
    +public enum Auditor {
    +        BEFORE, AFTER, BEFORE_AND_AFTER, NOTHING
    +}
    +
    +
    +
    +

    the values in the enum above will be used later in the challenges section.

    +
    +
  22. +
  23. +

    Create the io.pivotal.workshop.directory.aop.SimpleAudit class that will be used as cross-cutting concern for logging the execution of any method that has the @Audit annotation.

    +
    +
    +
    package io.pivotal.workshop.directory.aop;
    +
    +import org.aspectj.lang.JoinPoint;
    +import org.aspectj.lang.annotation.Aspect;
    +import org.aspectj.lang.annotation.Before;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import io.pivotal.workshop.directory.annotation.Audit;
    +
    +@Aspect
    +public class SimpleAudit {
    +
    +        private static Logger log = LoggerFactory.getLogger("[AUDIT]");
    +
    +        @Before("@annotation(audit)")
    +        public void audit(JoinPoint jp, Audit audit){
    +                log.info("[EXECUTING] " + jp.getSignature());
    +        }
    +}
    +
    +
    +
  24. +
  25. +

    In the io.pivotal.workshop.directory.repository.DirectoryRepository class add the findByEmail method (a challenge from the previous lab).

    +
    +
    +
        @Audit
    +        public Optional<Person> findByEmail(String email){
    +                return findFirstBy( p -> p.getEmail().equals(email));
    +        }
    +
    +
    +
    +

    as you can see, this method will have the @Audit annotation that will be log the execution of this method. If you missed the challenge from the previous lab, here it is, the +findFirstBy code:

    +
    +
    +
    +
    private Optional<Person> findFirstBy(Predicate<Person> findBy){
    +        return directory.stream()
    +                .filter(findBy)
    +                .findFirst();
    +}
    +
    +
    +
  26. +
  27. +

    Create a io.pivotal.workshop.directory.config.DirectoryConfig class that will create the aspect as Spring bean.

    +
    +
    +
    package io.pivotal.workshop.directory.config;
    +
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +
    +import io.pivotal.workshop.directory.aop.SimpleAudit;
    +
    +@Configuration
    +public class DirectoryConfig {
    +
    +        @ConditionalOnClass(name={"io.pivotal.workshop.directory.repository.PersonRepository"})
    +        @Bean
    +        public SimpleAudit simpleAudit(){
    +                return new SimpleAudit();
    +        }
    +
    +}
    +
    +
    +
    +

    is important to notice the usage of the @ConditionalOnClass annotation. Here the simpleAudit bean will be created only if the PersonRepository is on your classpath.

    +
    +
  28. +
  29. +

    In the io.pivotal.workshop.directory.controller.DirectoryController class add the searchByEmail method ( achallenge from the previous lab).

    +
    +
    +
    @RequestMapping("/directory/search")
    +public ResponseEntity<?> searchByEmail(@RequestParam String email) {
    +        Optional<Person> result = this.repo.findByEmail(email);
    +
    +        if (result.isPresent()) {
    +                return ResponseEntity.ok(result.get());
    +        }
    +
    +        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("{}");
    +}
    +
    +
    +
  30. +
  31. +

    Run your application and test the /directory/search endpoint by open a browser and hit: http://localhost:8080/directory/search?email=john@email.com.

    +
    + + + + + +
    +
    Tip
    +
    +If you are not using any IDE, then you can run your application with maven: ./mvnw spring-boot:run or if you are using gradle: ./gradlew bootRun +
    +
    +
  32. +
  33. +

    You should see the result.

    +
    +
    Figure 2: Directory Web Internals App - http://localhost:8080/directory/search?email=john@email.com
    +

    03 spring boot internals 02

    +
    +
  34. +
+
+
+
+
+

Questions

+
+
+

Q: Did you see the logs in the console window (or terminal)?

+
+
+

A: NO, because the @ConditionalOnClass annotation is looking for a PersonRepository class.

+
+
+

Modify the DirectoryConfig class and change the right class in the @ConditionalOnClass annotation and run the application again:

+
+
+
+
@ConditionalOnClass(name={"io.pivotal.workshop.directory.repository.DirectoryRepository"})
+
+
+
+

now you should see:

+
+
+
+
[AUDIT] : [EXECUTING] Optional io.pivotal.workshop.directory.repository.DirectoryRepository.findByEmail(String)
+
+
+
+
+
+

Challenges

+
+
+

This is a trivial example using the @ConditionalOnClass annotation, so let’s add more features to our project and have a real use of the Auditor enum:

+
+
+
    +
  • +

    Create a new @Around advice for using the Auditor enum, so it can log also:

    +
    +
      +
    • +

      the parameters values if @Audit(Auditor.BEFORE).

      +
    • +
    • +

      the return value if @Audit(Auditor.AFTER).

      +
    • +
    • +

      the parameters and return values if @Audit(Auditor.BEFORE_AND_AFTER).

      +
    • +
    +
    +
  • +
  • +

    Use the @ConditionalOnProperty to evaluate if the directory.audit is on or off. If is on then log, but if is off or not present then don’t do anything.

    +
  • +
  • +

    Add the logic to the Aspect to review the directory.info if is with value short to log just the name of the method being executed, or with value long to show the full name of the method being executed.

    +
  • +
+
+
+

[HOMEWORK]

+
+

This lab is just simple enough to get started, but everything is in the same code. +The challenge is now to create a new audit project and use it in the directory-web-internals by creating @EnableAudit annotation.

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/04-spring-boot-features.html b/src/main/UploadedContent/student-lab-instructions/04-spring-boot-features.html new file mode 100644 index 0000000..c3b96a1 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/04-spring-boot-features.html @@ -0,0 +1,1052 @@ + + + + + + + +Spring Boot Features + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with the main Spring Boot features.

+
+
+

Time: 25 minutes.

+
+ +
+
+
+

Directory Web Features

+
+
+

This lab will show you how to use some of the Spring Boot features. You are going to use the code from previous labs (directory-web-internals project)

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Directory Web App Project metadata with (See Figure 1):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Directory Web App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    directory-web-features

    Name:

    directory-web-features

    Package Name:

    io.pivotal.workshop.directory

    Dependencies:

    Web, AOP

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web and AOP in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    You can copy all the code from the previous lab (we are going to use it).

    +
  16. +
+
+
+

SpringBootApplication: Banner and Web Environment

+
+

Let’s start with the SpringBootApplication class. In the main source code: DirectoryWebFeaturesApplication class, add the following code:

+
+
+
io.pivotal.workshop.directory.DirectoryWebFeaturesApplication.java
+
+
package io.pivotal.workshop.directory;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class DirectoryWebFeaturesApplication {
+
+        public static void main(String[] args) {
+
+                SpringApplication app = new SpringApplication(DirectoryWebFeaturesApplication.class);
+                app.run(args);
+        }
+}
+
+
+
+

the SpringApplication will be use to turn on/off some features.

+
+
+

Challenges

+
+
    +
  • +

    Add a banner.txt with some ASCII ART in the src/main/resources folder and run the application. You can choose some ascii art from: http://patorjk.com/software/taag

    +
  • +
  • +

    Turn off the Banner using the app instance.

    +
  • +
  • +

    Turn off the Web Environment using the app instance.

    +
  • +
  • +

    You can turn on/off the Banner and Web Environment using the application.properties, find out how.

    +
  • +
+
+
+
+
+

SpringBootApplication: CommandLineRunner / ApplicationRunner

+
+

In the main source code: DirectoryWebFeaturesApplication class, replace the class definition with the following code:

+
+
+
io.pivotal.workshop.directory.DirectoryWebFeaturesApplication.java
+
+
package io.pivotal.workshop.directory;
+
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+public class DirectoryWebFeaturesApplication {
+
+        public static void main(String[] args) {
+
+                SpringApplication app = new SpringApplication(DirectoryWebFeaturesApplication.class);
+                app.run(args);
+        }
+
+    private static final Logger log = LoggerFactory.getLogger("[ARGUMENTS]");
+
+    @Bean
+        public CommandLineRunner commandRunner() {
+                return args -> {
+                        Stream.of(args).forEach(s -> {
+                                log.info("CommandLine: " + s);
+                        });
+                };
+        }
+}
+
+
+
+

the CommandLineRunner is use to execute code before your Spring Boot application start. The previous code will log all the arguments passed to the application.

+
+
+

Package and run the application with the following arguments: --option=A,B,C --enable-audit=yes

+
+
+

+ +.create the JAR

+
+
+
+
./mvnw clean package -DskipTests=true
+
+
+
+

+ +.run the application with some arguments

+
+
+
+
java -jar target/directory-web-features-0.0.1-SNAPSHOT.jar --option=A,B,C --enable-audit=yes
+
+
+
+

see the logs.

+
+
+

Challenges

+
+
    +
  • +

    Use now the ApplicationRunner and get the option and enable-audit values, you should log something similar:

    +
    +
    +
    [ARGS]  : Option Name: enable-audit
    +[ARGS]  : Option Name: option
    +[ARGS]  : Found Value:[A,B,C]
    +[ARGS]  : Found Value:[yes]
    +[ARGS]  : Argument: --option=A,B,C
    +[ARGS]  : Argument: --enable-audit=yes
    +
    +
    +
  • +
+
+
+
+
+

SpringBootApplication: External Configuration

+
+

If you didn’t finish the previous lab (directory-web-internals) this is you chance! Create a DirectoryProperties class that will hold the information about audit (on/off) and the info to be display (long/short) for the AOP audit.

+
+
+

Here you will use the @ConfigurationProperties annotation to enable a properties binding, you will use the prefix directory.

+
+
+
io.pivotal.workshop.directory.config.DirectoryProperties.java
+
+
package io.pivotal.workshop.directory.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "directory")
+public class DirectoryProperties {
+
+        private String audit = "off";
+        private String info = "long";
+
+        public String getAudit() {
+                return audit;
+        }
+
+        public void setAudit(String audit) {
+                this.audit = audit;
+        }
+
+        public String getInfo() {
+                return info;
+        }
+
+        public void setInfo(String info) {
+                this.info = info;
+        }
+
+}
+
+
+
+

Modify the application.properties by adding the new properties:

+
+
+
src/main/resource/application.properties
+
+
directory.audit=on
+directory.info=short
+
+
+
+

Create the aspect DirectoryAudit and read the values to audit the method findByEmail from the DirectoryRepository class. (This is the challenge from the directory-web-internals lab).

+
+
+
io.pivotal.workshop.directory.aop.DirectoryAudit.java
+
+
package io.pivotal.workshop.directory.aop;
+
+import java.util.stream.IntStream;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.pivotal.workshop.directory.annotation.Audit;
+import io.pivotal.workshop.directory.config.DirectoryProperties;
+
+@Aspect
+public class DirectoryAudit {
+
+        private DirectoryProperties props;
+
+        public DirectoryAudit(DirectoryProperties props){
+                this.props = props;
+        }
+
+        private static Logger log = LoggerFactory.getLogger("[AUDIT]");
+
+        @Around("execution(* *(..)) && @annotation(audit)")
+        public Object audit(ProceedingJoinPoint jp, Audit audit) throws Throwable {
+                Object[] args = jp.getArgs();
+
+                this.printBar();
+                this.print("[executing] " + (props.getInfo().toLowerCase().equals("short") ? jp.getSignature().getName() : jp.getSignature() ));
+
+                switch (audit.value()) {
+                case BEFORE:
+                case BEFORE_AND_AFTER:
+                        this.printArgs(args);
+                default:
+                        break;
+                }
+
+                Object obj = jp.proceed(args);
+
+                switch (audit.value()) {
+                case AFTER:
+                case BEFORE_AND_AFTER:
+                        this.print("[result] " + obj);
+                default:
+                        this.printBar();
+                        break;
+                }
+
+                return obj;
+        }
+
+        private void print(Object obj) {
+                log.info(obj.toString());
+        }
+
+        private void printArgs(Object[] args) {
+                IntStream.range(0, args.length).forEach(idx -> {
+                        log.info(String.format("[params] arg%d = %s", idx, args[idx]));
+                });
+
+        }
+
+        private void printBar(){
+                log.info("===========");
+        }
+}
+
+
+
+

In the DirectoryRepository add the @Audit annotation in the findByEmail method.

+
+
+
io.pivotal.workshop.directory.repository.DirectoryRepository.java - snippet
+
+
@Audit
+public Optional<Person> findByEmail(String email){
+        return findFirstBy( p -> p.getEmail().equals(email));
+}
+
+
+
+

Run you application and check out the logs.

+
+
+ + + + + +
+
Warning
+
+The on/off feature only works if you did the challenge from the previous lab. You need to add the @ConditionalOnProperty to evaluate if the directory.audit is on or off. +
+
+
+

Challenges

+
+
    +
  • +

    Run you application by testing the @Audit annotation values: Auditor.BEFORE,Auditor.AFTER,Auditor.BEFORE_AND_AFTER

    +
  • +
  • +

    Package your application and:

    +
    +
      +
    • +

      override the audit and info properties in the command line.

      +
    • +
    • +

      use environment variables to override the audit and info properties.

      +
    • +
    • +

      create a application.yml file in the current directory, add the audit and info properties and execute the program, what happen? did it worked?

      +
    • +
    +
    +
  • +
+
+
+ + + + + +
+
Tip
+
+Remember that, by adding an application.properties or application.yml to the current directory will override the values. +
+
+
+
+
+

[HOMEWORK]

+
+
    +
  • +

    Create profiles by:

    +
    +
      +
    • +

      Creating an application-qa.properties and application-prod.properties. Add the properties audit and info and test by enable the two profiles.

      +
    • +
    • +

      Creating an application.yml and add the profile section. Test by enable each profile.

      +
    • +
    +
    +
  • +
  • +

    Instead of using the @Configuration properties, use application arguments to enable the audit and info.

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/05-spring-boot-web.html b/src/main/UploadedContent/student-lab-instructions/05-spring-boot-web.html new file mode 100644 index 0000000..96c51b0 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/05-spring-boot-web.html @@ -0,0 +1,1355 @@ + + + + + + + +Spring Boot Web + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with the Spring MVC and the Spring Boot Web applications.

+
+
+

Time: 30 minutes.

+
+ +
+
+
+

Code Snippet Manager

+
+
+

As developers normally we require to have some code snippets that help us to code fast and safe.

+
+
+

The main idea of this application is to create a site that can manage our Code Snippets. We are going to create a RESTful API for any external client.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Code Snippet Manager App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager

    Name:

    code-snippet-manager

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Web, DevTools, Groovy Templates

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io/
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web, DevTools, Groovy Templates, in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Let’s start by defining the domain models. Create the following classes:

    +
    +
    io.pivotal.workshop.snippet.domain.Language.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +public class Language {
    +
    +        private String id;
    +        private String name;
    +        private String syntax = "text";
    +
    +        public Language() {
    +                this.id = java.util.UUID.randomUUID().toString().replaceAll("-", "");;
    +        }
    +
    +        public Language(String name) {
    +                this();
    +                this.name = name;
    +        }
    +
    +        public Language(String name, String syntax) {
    +                this(name);
    +                this.syntax = syntax;
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getName() {
    +                return name;
    +        }
    +
    +        public void setName(String name) {
    +                this.name = name;
    +        }
    +
    +        public String getSyntax() {
    +                return syntax;
    +        }
    +
    +        public void setSyntax(String syntax) {
    +                this.syntax = syntax;
    +        }
    +}
    +
    +
    +
    +

    We can have multiple snippets in different programming languages, that’s why we have this class. Take a look that also there is a syntax field; this field will be use later on for syntax highlight.

    +
    +
    +
    io.pivotal.workshop.snippet.domain.Code.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +public class Code {
    +        private String id;
    +        private String source;
    +
    +        public Code() {
    +                this.id = java.util.UUID.randomUUID().toString().replaceAll("-", "");;
    +        }
    +
    +        public Code(String source) {
    +                this();
    +                this.source = source;
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getSource() {
    +                return source;
    +        }
    +
    +        public void setSource(String source) {
    +                this.source = source;
    +        }
    +
    +}
    +
    +
    +
    +

    This class will be use to hold the actual snippet code.

    +
    +
    +
    io.pivotal.workshop.snippet.domain.Snippet.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +import java.util.Date;
    +
    +public class Snippet {
    +
    +        private String id;
    +        private String title;
    +        private String keywords;
    +        private String description;
    +        private Language lang;
    +        private Code code;
    +        private Date created;
    +        private Date modified;
    +
    +        public Snippet() {
    +                this.id = java.util.UUID.randomUUID().toString().replaceAll("-", "");
    +                this.created = new Date();
    +                this.modified = new Date();
    +        }
    +
    +        public Snippet(String title, String keywords, String description, Language lang, Code code) {
    +                this();
    +                this.title = title;
    +                this.keywords = keywords;
    +                this.description = description;
    +                this.lang = lang;
    +                this.code = code;
    +        }
    +
    +        public Snippet(String title, Language lang, Code code) {
    +                this(title, "", "", lang, code);
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getTitle() {
    +                return title;
    +        }
    +
    +        public void setTitle(String title) {
    +                this.title = title;
    +        }
    +
    +        public Language getLang() {
    +                return lang;
    +        }
    +
    +        public void setLang(Language lang) {
    +                this.lang = lang;
    +        }
    +
    +        public String getDescription() {
    +                return description;
    +        }
    +
    +        public void setDescription(String description) {
    +                this.description = description;
    +        }
    +
    +        public String getKeywords() {
    +                return keywords;
    +        }
    +
    +        public void setKeywords(String keywords) {
    +                this.keywords = keywords;
    +        }
    +
    +        public Code getCode() {
    +                return code;
    +        }
    +
    +        public void setCode(Code code) {
    +                this.code = code;
    +        }
    +
    +        public Date getCreated() {
    +                return created;
    +        }
    +
    +        public void setCreated(Date created) {
    +                this.created = created;
    +        }
    +
    +        public Date getModified() {
    +                return modified;
    +        }
    +
    +        public void setModified(Date modified) {
    +                this.modified = modified;
    +        }
    +}
    +
    +
    +
    +

    The above class will be the main reponse.

    +
    +
  16. +
  17. +

    Next let’s create a base interface that will be use as main repository. In this case we are going to hold all the data in Memory using collections:

    +
    +
    io.pivotal.workshop.snippet.repository.SimpleRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import java.util.Collection;
    +
    +public interface SimpleRepository<T> {
    +
    +        Iterable<T> findAll();
    +        void saveAll(Collection<T> items);
    +        T saveAll(T item);
    +        T findById(String id);
    +}
    +
    +
    +
    +

    as you can see it has just the most common actions.

    +
    +
  18. +
  19. +

    Let’s create now all the Repositories that will be implementing the SimpleRepository interface.

    +
    +
    io.pivotal.workshop.snippet.repository.LanguageRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import java.util.ArrayList;
    +import java.util.Collection;
    +import java.util.List;
    +import java.util.Optional;
    +import java.util.stream.StreamSupport;
    +
    +import org.springframework.stereotype.Repository;
    +
    +import io.pivotal.workshop.snippet.domain.Language;
    +
    +@Repository
    +public class LanguageRepository implements SimpleRepository<Language>{
    +
    +    private List<Language> languages = new ArrayList<>();
    +
    +        public Iterable<Language> findAll(){
    +                return languages;
    +        }
    +
    +        public void saveAll(Collection<Language> languages){
    +                this.languages.addAll(languages);
    +        }
    +
    +        public Language findById(String name) {
    +                Optional<Language> language = StreamSupport
    +                        .stream(this.languages.spliterator(), false)
    +                        .filter(lang -> lang.getName().equals(name))
    +                        .findFirst();
    +
    +                if (language.isPresent()) return language.get();
    +
    +                return null;
    +        }
    +
    +        public Language saveAll(Language item) {
    +                assert item.getName() != null;
    +
    +                Language language = this.findById(item.getName());
    +
    +                if(language == null) {
    +                        this.languages.add(item);
    +                        return item;
    +                }else {
    +                        language.setName(item.getName());
    +                        language.setSyntax(item.getSyntax());
    +                        return language;
    +                }
    +        }
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.repository.CodeRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import java.util.ArrayList;
    +import java.util.Collection;
    +import java.util.List;
    +import java.util.Optional;
    +
    +import org.springframework.stereotype.Repository;
    +
    +import io.pivotal.workshop.snippet.domain.Code;
    +
    +@Repository
    +public class CodeRepository implements SimpleRepository<Code>{
    +
    +        private List<Code> codes = new ArrayList<>();
    +
    +        public List<Code> findAll() {
    +                return codes;
    +        }
    +
    +        @Override
    +        public void saveAll(Collection<Code> items) {
    +                this.codes.addAll(items);
    +        }
    +
    +        @Override
    +        public Code saveAll(Code item) {
    +                assert item.getSource() != null;
    +
    +                Code code = findById(item.getId());
    +
    +                if(code == null){
    +                        this.codes.add(item);
    +                        return item;
    +                }
    +                else {
    +                        code.setSource(item.getSource());
    +                        return code;
    +                }
    +        }
    +
    +        @Override
    +        public Code findById(String id) {
    +                Optional<Code> code = codes
    +                                .stream()
    +                                .filter(c -> c.getId().equals(id))
    +                                .findFirst();
    +                if (code.isPresent()) return code.get();
    +
    +                return null;
    +        }
    +
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.repository.SnippetRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import java.util.ArrayList;
    +import java.util.Collection;
    +import java.util.Date;
    +import java.util.List;
    +import java.util.Optional;
    +
    +import org.springframework.stereotype.Repository;
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +
    +@Repository
    +public class SnippetRepository implements SimpleRepository<Snippet>{
    +
    +        private List<Snippet> snippets = new ArrayList<>();
    +
    +        @Override
    +        public Snippet saveAll(Snippet snippet){
    +                assert snippet.getTitle() != null;
    +                assert snippet.getCode() != null;
    +                assert snippet.getLang() != null;
    +
    +                Snippet _snippet =  null;
    +
    +                if (snippet.getId() == null) {
    +                        _snippet = new Snippet(snippet.getTitle(), snippet.getLang(),snippet.getCode());
    +
    +                } else {
    +                         _snippet = this.findById(snippet.getId());
    +                         if(_snippet != null){
    +                                 _snippet.setTitle(snippet.getTitle());
    +                                 _snippet.setCode(snippet.getCode());
    +                                 _snippet.setLang(snippet.getLang());
    +                                 _snippet.setModified(new Date());
    +                         }
    +                }
    +
    +                return _snippet;
    +        }
    +
    +        @Override
    +        public Iterable<Snippet> findAll(){
    +                return this.snippets;
    +        }
    +
    +        @Override
    +        public Snippet findById(String id){
    +                Optional<Snippet> result = snippets.stream()
    +                                .filter(snippet -> snippet.getId().equals(id))
    +                                .findFirst();
    +
    +                if (result.isPresent()) return result.get();
    +
    +                return null;
    +        }
    +
    +        @Override
    +        public void saveAll(Collection<Snippet> items) {
    +                this.snippets.addAll(items);
    +        }
    +}
    +
    +
    +
  20. +
  21. +

    Next lets do the main controller that will expose the REST API:

    +
    +
    io.pivotal.workshop.snippet.controller.SnippetController.java
    +
    +
    @RestController
    +public class SnippetController {
    +
    +
    +        private SnippetRepository snippetRepository;
    +        private LanguageRepository languageRepository;
    +
    +        public SnippetController(SnippetRepository snippetRepository,LanguageRepository languageRepository){
    +                this.snippetRepository = snippetRepository;
    +                this.languageRepository = languageRepository;
    +        }
    +
    +        @RequestMapping("/")
    +        public ModelAndView home(){
    +                assert snippetRepository != null;
    +
    +                Map<String,Object> model = new HashMap<String,Object>();
    +                model.put("langs", languageRepository.findAll());
    +                model.put("snippets", snippetRepository.findAll());
    +
    +                return new ModelAndView("views/home",model);
    +        }
    +
    +        @RequestMapping("/snippets")
    +        public ResponseEntity<?> snippets(){
    +                assert snippetRepository != null;
    +                return ResponseEntity.ok(snippetRepository.findAll());
    +        }
    +}
    +
    +
    +
  22. +
  23. +

    Create a configuration class to initialize your repositories:

    +
    +
    io.pivotal.workshop.snippet.config.SnippetConfiguration.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +import java.nio.file.Files;
    +import java.nio.file.Paths;
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +import org.springframework.boot.CommandLineRunner;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +
    +import io.pivotal.workshop.snippet.domain.Code;
    +import io.pivotal.workshop.snippet.domain.Language;
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import io.pivotal.workshop.snippet.repository.SnippetRepository;
    +
    +@Configuration
    +public class SnippetConfiguration {
    +
    +        @Bean
    +        public CommandLineRunner runner(SnippetRepository snippetRepo) {
    +                return args -> {
    +                        @SuppressWarnings("serial")
    +                        List<Snippet> snippets = new ArrayList<Snippet>() {
    +                                {
    +                                        add(new Snippet("Hello World", new Language("HTML", "xml"),new Code(new String(Files.readAllBytes(Paths.get("code/html-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("C#", "c#"),new Code(new String(Files.readAllBytes(Paths.get("code/cs-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("Pascal", "py"),new Code(new String(Files.readAllBytes(Paths.get("code/pas-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("Erlang", "erl"),new Code(new String(Files.readAllBytes(Paths.get("code/erl-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("JavaScript", "js"),new Code(new String(Files.readAllBytes(Paths.get("code/js-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("Groovy", "groovy"),new Code("println 'Hello World'")));
    +                                }
    +                        };
    +
    +                        snippetRepo.saveAll(snippets);
    +
    +                };
    +        }
    +}
    +
    +
    +
    +

    as you can see from the above code, we are building up our data.

    +
    +
    +

    If you take a close look, we are using the Files.readAllBytes helper class to read from a file, and this file is located in the code/ folder.

    +
    +
  24. +
  25. +

    Add some code snippets in the code/ folder.

    +
    +
    code/html-code.txt - HTML
    +
    +
    <html>
    +  <body>
    +    <h1>Hello World</h1>
    +  </body>
    +</html>
    +
    +
    +
    +
    code/cs-code.txt - C#
    +
    +
    class Hello {
    + static void Main() {
    +  System.Console.Write("Hello World");
    + }
    +}
    +
    +
    +
    +
    code/pas-code.txt - Pascal
    +
    +
    program HelloWorld;
    +
    +begin
    +  writeln('Hello World');
    +end.
    +
    +
    +
    +
    code/erl-code.txt - Erlang
    +
    +
     -module(hello).
    + -export([hello_world/0]).
    +
    + hello_world() -> io:fwrite("hello, world\n").
    +
    +
    +
    +
    code/js-code.txt - JavaScript
    +
    +
    console.log("Hello World");
    +
    +
    +
  26. +
  27. +

    A lot of code, right? Well now its time to run the application. You can use your IDE or command line. Once running you application, go to your browser and hit: http://localhost:8080/snippets and you should get the a response like Figure 2.0.

    +
    +
    Figure 2.0: Snippet Code Manager - http://localhost:8080/snippets
    +

    /snippets

    +
    +
  28. +
+
+
+
+
+

Challenges

+
+
+

Adding a UI - Home Page

+
+

You can see that the io.pivotal.workshop.snippet.controller.SnippetController.java has the home() method, and it returns the view: views/home and the model, a map that contains the languages and the snippets: +Add the following missing dependencies to your pom.xml because you will need them:

+
+
+
+
<dependency>
+    <groupId>org.webjars</groupId>
+        <artifactId>jquery</artifactId>
+        <version>2.2.4</version>
+</dependency>
+<dependency>
+        <groupId>org.webjars</groupId>
+        <artifactId>bootstrap</artifactId>
+        <version>3.3.6</version>
+</dependency>
+<dependency>
+        <groupId>org.webjars</groupId>
+        <artifactId>angularjs</artifactId>
+        <version>1.5.7</version>
+</dependency>
+
+
+
+

Add the following libraries in the src/main/resources/static/css: +theme.css and offcanvas.css +and in the src/main/resources/static/js folder add the syntaxhighlighter.js script.

+
+
+
    +
  • +

    Create the necessary layout and home page to have the same as Figure 3.0.

    +
    +
    Figure 3.0: Code Snippet Manager - http://localhost:8080
    +

    /

    +
    +
  • +
+
+
+ + + + + +
+
Tip
+
+You can take a look at the Groovy Template Engine: http://docs.groovy-lang.org/next/html/documentation/template-engines.html#_the_markuptemplateengine +
+
+
+
+

REST API

+
+
    +
  • +

    Complete the RESTful API:

    +
    +
      +
    • +

      provide the /snippets/{id} path endpoint to search by snippet Id.

      +
    • +
    • +

      provide methods to handle the: POST, PUT for adding a new snippet and updating the snippet.

      +
    • +
    +
    +
  • +
  • +

    Make changes to repose either JSON or XML.

    +
  • +
  • +

    When doing a XML response, the snippet code should be in a CDATA tag. Modify the code to show a CDATA for every source code. (tip: @JacksonXmlCData)

    +
  • +
+
+
+
+
+
+

[HOMEWORK]

+
+
+
    +
  • +

    Add validation to the POST and PUT methods (tip: @Valid).

    +
  • +
  • +

    Finish up any missing UI or Controller methods.

    +
  • +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/06-spring-boot-data.html b/src/main/UploadedContent/student-lab-instructions/06-spring-boot-data.html new file mode 100644 index 0000000..87af322 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/06-spring-boot-data.html @@ -0,0 +1,1987 @@ + + + + + + + +Spring Boot Data + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with the Spring Data and the Spring Boot Data features.

+
+
+

Time: 60 minutes (20 minutes per section).

+
+ +
+
+
+

Code Snippet Manager JDBC

+
+
+

You will continue with the Code Snippet Manager code, but this time using a persistence engine. You will reuse the code from previous labs.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager JDBC Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Code Snippet Manager JDBC App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-jdbc

    Name:

    code-snippet-manager-jdbc

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Web, DevTools, Groovy Templates, JDBC, H2, MySQL

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io/
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web, DevTools, Groovy Templates, JDBC, H2, and MySQL in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy all the packages (with code) into the new project.

    +
  16. +
  17. +

    The classes in io.pivotal.workshop.snippet.domain: Code, Language and Snippet should be the same.

    +
  18. +
  19. +

    Create a new class CrossSnippetLanguageCode in the domain package:

    +
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +public class CrossSnippetLanguageCode {
    +
    +        private String snippetId;
    +        private String languageId;
    +        private String codeId;
    +
    +        public CrossSnippetLanguageCode() {
    +        }
    +
    +        public CrossSnippetLanguageCode(String snippetId, String languageId, String codeId) {
    +
    +                this.snippetId = snippetId;
    +                this.languageId = languageId;
    +                this.codeId = codeId;
    +        }
    +
    +        public String getSnippetId() {
    +                return snippetId;
    +        }
    +
    +        public void setSnippetId(String snippetId) {
    +                this.snippetId = snippetId;
    +        }
    +
    +        public String getLanguageId() {
    +                return languageId;
    +        }
    +
    +        public void setLanguageId(String languageId) {
    +                this.languageId = languageId;
    +        }
    +
    +        public String getCodeId() {
    +                return codeId;
    +        }
    +
    +        public void setCodeId(String codeId) {
    +                this.codeId = codeId;
    +        }
    +
    +}
    +
    +
    +
    +

    this class will hold the relationship between the Code and Language within the Snippet class.

    +
    +
  20. +
  21. +

    Because you will reuse the SimpleRepository interface, is necessary modify all the implementations. Modify the code accordingly:

    +
    +
    io.pivotal.workshop.snippet.repository.LanguageRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import java.sql.PreparedStatement;
    +import java.util.Collection;
    +
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.dao.EmptyResultDataAccessException;
    +import org.springframework.jdbc.core.JdbcTemplate;
    +import org.springframework.stereotype.Repository;
    +
    +import io.pivotal.workshop.snippet.domain.Language;
    +import io.pivotal.workshop.snippet.repository.mapper.LanguageRowMapper;
    +
    +@Repository
    +public class LanguageRepository implements SimpleRepository<Language>{
    +
    +    private final String SQL_FIND_ALL = "select * from language";
    +    private final String SQL_FIND_ONE = "select * from language where name = ?";
    +    private final String SQL_INSERT = "insert into language(id,name,syntax) values(?,?,?)";
    +    private final String SQL_UPDATE = "update language set name = ?, syntax = ? where id = ?";
    +
    +    private JdbcTemplate jdbcTemplate;
    +
    +    @Autowired
    +    public LanguageRepository(JdbcTemplate jdbcTemplate){
    +                    this.jdbcTemplate = jdbcTemplate;
    +    }
    +
    +    @Override
    +        public Iterable<Language> findAll(){
    +                return this.jdbcTemplate.query(SQL_FIND_ALL, new LanguageRowMapper());
    +        }
    +
    +    @Override
    +        public void saveAll(Collection<Language> languages){
    +                languages.forEach( lang -> saveAll(lang) );
    +        }
    +
    +    @Override
    +        public Language findById(String name) {
    +                    try {
    +                            return this.jdbcTemplate.queryForObject(SQL_FIND_ONE, new Object[]{name}, new LanguageRowMapper());
    +                } catch (EmptyResultDataAccessException ex){
    +                            return null;
    +                    }
    +        }
    +
    +    @Override
    +        public Language saveAll(Language item) {
    +                assert item.getName() != null;
    +
    +                Language language = this.findById(item.getName());
    +
    +                if(language == null) {
    +                        this.jdbcTemplate.update( psc -> {
    +                                PreparedStatement ps = psc.prepareStatement(SQL_INSERT);
    +                                ps.setString(1, item.getId());
    +                                ps.setString(2, item.getName());
    +                                ps.setString(3, item.getSyntax());
    +                                return ps;
    +                        });
    +                        return item;
    +                }else {
    +                        this.jdbcTemplate.update( psc -> {
    +                                PreparedStatement ps = psc.prepareStatement(SQL_UPDATE);
    +                                ps.setString(1, item.getName());
    +                                ps.setString(2, item.getSyntax());
    +                                ps.setString(3, item.getId());
    +                                return ps;
    +                        });
    +                        return language;
    +                }
    +        }
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.repository.CodeRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import java.sql.PreparedStatement;
    +import java.util.Collection;
    +import java.util.List;
    +
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.dao.EmptyResultDataAccessException;
    +import org.springframework.jdbc.core.JdbcTemplate;
    +import org.springframework.stereotype.Repository;
    +
    +import io.pivotal.workshop.snippet.domain.Code;
    +import io.pivotal.workshop.snippet.repository.mapper.CodeRowMapper;
    +
    +@Repository
    +public class CodeRepository implements SimpleRepository<Code> {
    +
    +        private final String SQL_FIND_ALL = "select * from code";
    +        private final String SQL_FIND_ONE = "select * from code where id = ?";
    +        private final String SQL_INSERT = "insert into code(id,source) values(?,?)";
    +        private final String SQL_UPDATE = "update code set source = ? where id = ?";
    +
    +        private JdbcTemplate jdbcTemplate;
    +
    +        @Autowired
    +        public CodeRepository(JdbcTemplate jdbcTemplate) {
    +                this.jdbcTemplate = jdbcTemplate;
    +        }
    +
    +        @Override
    +        public List<Code> findAll() {
    +                return this.jdbcTemplate.query(SQL_FIND_ALL, new CodeRowMapper());
    +        }
    +
    +        @Override
    +        public void saveAll(Collection<Code> items) {
    +                items.forEach(code -> this.saveAll(code));
    +        }
    +
    +        @Override
    +        public Code findById(String id) {
    +                try {
    +                        return this.jdbcTemplate.queryForObject(SQL_FIND_ONE, new Object[] { id }, new CodeRowMapper());
    +                } catch (EmptyResultDataAccessException ex) {
    +                        return null;
    +                }
    +        }
    +
    +        @Override
    +        public Code saveAll(Code item) {
    +                assert item.getSource() != null;
    +
    +                Code code = this.findById(item.getId());
    +
    +                if (code == null) {
    +                        this.jdbcTemplate.update(psc -> {
    +                                PreparedStatement ps = psc.prepareStatement(SQL_INSERT);
    +                                ps.setString(1, item.getId());
    +                                ps.setString(2, item.getSource());
    +                                return ps;
    +                        });
    +                        return item;
    +                } else {
    +                        this.jdbcTemplate.update(psc -> {
    +                                PreparedStatement ps = psc.prepareStatement(SQL_UPDATE);
    +                                ps.setString(1, item.getSource());
    +                                ps.setString(2, item.getId());
    +                                return ps;
    +                        });
    +                        return code;
    +                }
    +        }
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.repository.SnippetRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import java.sql.PreparedStatement;
    +import java.text.SimpleDateFormat;
    +import java.util.Collection;
    +import java.util.Date;
    +import java.util.List;
    +
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.dao.EmptyResultDataAccessException;
    +import org.springframework.jdbc.core.JdbcTemplate;
    +import org.springframework.stereotype.Repository;
    +
    +import io.pivotal.workshop.snippet.domain.CrossSnippetLanguageCode;
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import io.pivotal.workshop.snippet.repository.mapper.SnippetRowMapper;
    +
    +@Repository
    +public class SnippetRepository implements SimpleRepository<Snippet> {
    +
    +        private final String SQL_FIND_ALL = "select s.*,l.id lang_id, l.name lang_name, l.syntax lang_syntax, c.id code_id, c.source code_source from cross_snippet_language_code cx " +
    +                        "inner join snippet s on cx.snippet_id = s.id " +
    +                        "inner join language l on cx.language_id = l.id " +
    +                        "inner join code c on cx.code_id = c.id ";
    +
    +        private final String SQL_FIND_ONE = SQL_FIND_ALL + " where s.id = ?";
    +        private final String SQL_INSERT = "insert into snippet(id,title, keywords, description, created, modified) values(?,?,?,?,?,?)";
    +        private final String SQL_UPDATE = "update snippet set title = ?, keywords = ?, description = ?, modified = ? where id = ?";
    +
    +        private JdbcTemplate jdbcTemplate;
    +        private LanguageRepository langRepo;
    +        private CodeRepository codeRepo;
    +        private CrossSnippetLanguageCodeRepository crossRepo;
    +
    +        @Autowired
    +        public SnippetRepository(JdbcTemplate jdbcTemplate, LanguageRepository langRepo, CodeRepository codeRepo, CrossSnippetLanguageCodeRepository crossRepo) {
    +                this.jdbcTemplate = jdbcTemplate;
    +                this.langRepo = langRepo;
    +                this.codeRepo = codeRepo;
    +                this.crossRepo = crossRepo;
    +        }
    +
    +        @Override
    +        public List<Snippet> findAll() {
    +                return this.jdbcTemplate.query(SQL_FIND_ALL, new SnippetRowMapper());
    +        }
    +
    +        @Override
    +        public void saveAll(Collection<Snippet> items) {
    +                items.forEach(code -> this.saveAll(code));
    +        }
    +
    +        @Override
    +        public Snippet findById(String id) {
    +                try {
    +                        return this.jdbcTemplate.queryForObject(SQL_FIND_ONE, new Object[] { id }, new SnippetRowMapper());
    +                } catch (EmptyResultDataAccessException ex) {
    +                        return null;
    +                }
    +        }
    +
    +        @Override
    +        public Snippet saveAll(final Snippet item) {
    +                assert item.getId() != null;
    +                assert item.getTitle() != null;
    +                assert item.getLang() != null;
    +                assert item.getCode() != null;
    +
    +                Snippet snippet = this.findById(item.getId());
    +
    +                Date date = new Date();
    +                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    +
    +                if (snippet == null) {
    +
    +                        this.crossRepo.saveAll(new CrossSnippetLanguageCode(item.getId(), item.getLang().getId(), item.getCode().getId()));
    +                        this.langRepo.saveAll(item.getLang());
    +                        this.codeRepo.saveAll(item.getCode());
    +
    +                        this.jdbcTemplate.update(psc -> {
    +                                PreparedStatement ps = psc.prepareStatement(SQL_INSERT);
    +                                ps.setString(1, item.getId());
    +                                ps.setString(2, item.getTitle());
    +                                ps.setString(3, item.getKeywords());
    +                                ps.setString(4, item.getDescription());
    +                                ps.setString(5, dateFormat.format(date));
    +                                ps.setString(6, dateFormat.format(date));
    +                                return ps;
    +                        });
    +
    +                        return item;
    +
    +                } else {
    +                        this.crossRepo.saveAll(new CrossSnippetLanguageCode(snippet.getId(), snippet.getLang().getId(), snippet.getCode().getId()));
    +                        this.langRepo.saveAll(item.getLang());
    +                        this.codeRepo.saveAll(item.getCode());
    +
    +                        this.jdbcTemplate.update(psc -> {
    +                                PreparedStatement ps = psc.prepareStatement(SQL_UPDATE);
    +                                ps.setString(1, item.getTitle());
    +                                ps.setString(2, item.getKeywords());
    +                                ps.setString(3, item.getDescription());
    +                                ps.setString(4, dateFormat.format(date));
    +                                ps.setString(5, item.getId());
    +                                return ps;
    +                        });
    +
    +                        return snippet;
    +                }
    +        }
    +
    +
    +}
    +
    +
    +
    +

    Every single class is using the JdbcTemplate (with some methods like: queryForObject, query and update) and the RowMapper, analyze the code and review the SQL statements.

    +
    +
  22. +
  23. +

    As you already guess from the code above, the SnippetRepository class has on its constructor the CrossSnippetLanguageCodeRepository reference. Create this class:

    +
    +
    io.pivotal.workshop.snippet.repository.CrossSnippetLanguageCodeRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import java.sql.PreparedStatement;
    +import java.sql.ResultSet;
    +import java.util.Collection;
    +
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.dao.EmptyResultDataAccessException;
    +import org.springframework.jdbc.core.JdbcTemplate;
    +import org.springframework.jdbc.core.RowMapper;
    +import org.springframework.stereotype.Repository;
    +
    +import io.pivotal.workshop.snippet.domain.CrossSnippetLanguageCode;
    +
    +@Repository
    +public class CrossSnippetLanguageCodeRepository implements SimpleRepository<CrossSnippetLanguageCode> {
    +
    +        private final String SQL_FIND_ALL = "select * from cross_snippet_language_code";
    +        private final String SQL_FIND_ONE = "select * from cross_snippet_language_code where snippet_id = ?";
    +        private final String SQL_INSERT = "insert into cross_snippet_language_code(snippet_id,language_id,code_id) values(?,?,?)";
    +        private final String SQL_UPDATE = "update cross_snippet_language_code set langauge_id = ?, code_id = ? where snippet_id = ?";
    +        private JdbcTemplate jdbcTemplate;
    +
    +        private final RowMapper<CrossSnippetLanguageCode> rowMapper = (ResultSet rs, int row) -> {
    +                CrossSnippetLanguageCode cross = new CrossSnippetLanguageCode();
    +                cross.setSnippetId(rs.getString("snippet_id"));
    +                cross.setLanguageId(rs.getString("language_id"));
    +                cross.setCodeId(rs.getString("code_id"));
    +                return cross;
    +        };
    +
    +        @Autowired
    +        public CrossSnippetLanguageCodeRepository(JdbcTemplate jdbcTemplate) {
    +                this.jdbcTemplate = jdbcTemplate;
    +        }
    +
    +        @Override
    +        public Iterable<CrossSnippetLanguageCode> findAll() {
    +                return this.jdbcTemplate.query(SQL_FIND_ALL, rowMapper);
    +        }
    +
    +        @Override
    +        public void saveAll(Collection<CrossSnippetLanguageCode> items) {
    +                items.forEach(cross -> {
    +                        this.saveAll(cross);
    +                });
    +        }
    +
    +        @Override
    +        public CrossSnippetLanguageCode saveAll(CrossSnippetLanguageCode item) {
    +                assert item.getSnippetId() != null;
    +                assert item.getLanguageId() != null;
    +                assert item.getCodeId() != null;
    +
    +                CrossSnippetLanguageCode cross = this.findById(item.getSnippetId());
    +
    +                if (cross == null) {
    +                        this.jdbcTemplate.update(psc -> {
    +                                PreparedStatement ps = psc.prepareStatement(SQL_INSERT);
    +                                ps.setString(1, item.getSnippetId());
    +                                ps.setString(2, item.getLanguageId());
    +                                ps.setString(3, item.getCodeId());
    +                                return ps;
    +                        });
    +                        return item;
    +                } else {
    +                        this.jdbcTemplate.update(psc -> {
    +                                PreparedStatement ps = psc.prepareStatement(SQL_UPDATE);
    +                                ps.setString(1, item.getLanguageId());
    +                                ps.setString(2, item.getCodeId());
    +                                ps.setString(3, item.getSnippetId());
    +                                return ps;
    +                        });
    +                        return cross;
    +                }
    +
    +        }
    +
    +        @Override
    +        public CrossSnippetLanguageCode findById(String id) {
    +                try {
    +                        return this.jdbcTemplate.queryForObject(SQL_FIND_ONE, new Object[] { id }, rowMapper);
    +                } catch (EmptyResultDataAccessException ex) {
    +                        return null;
    +                }
    +        }
    +
    +}
    +
    +
    +
    +

    Have you noticed that the CrossSnippetLanguageCodeRepository is using a declared Java 8 lambda notation for the RowMapper?

    +
    +
  24. +
  25. +

    Talking about RowMapper, all the above code is using it to map the ResultSet with the domain class. Create the following mappers in the io.pivotal.workshop.snippet.repository.mapper package:

    +
    +
    io.pivotal.workshop.snippet.repository.mapper.LanguageRowMapper.java
    +
    +
    package io.pivotal.workshop.snippet.repository.mapper;
    +
    +import java.sql.ResultSet;
    +import java.sql.SQLException;
    +
    +import org.springframework.jdbc.core.RowMapper;
    +
    +import io.pivotal.workshop.snippet.domain.Language;
    +
    +public class LanguageRowMapper implements RowMapper<Language> {
    +
    +        @Override
    +        public Language mapRow(ResultSet rs, int rowNum) throws SQLException {
    +                 Language lang = new Language();
    +                 lang.setId(rs.getString("id"));
    +                 lang.setName(rs.getString("name"));
    +                 lang.setSyntax(rs.getString("syntax"));
    +                return lang;
    +        }
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.repository.mapper.CodeRowMapper.java
    +
    +
    package io.pivotal.workshop.snippet.repository.mapper;
    +
    +import java.sql.ResultSet;
    +import java.sql.SQLException;
    +
    +import org.springframework.jdbc.core.RowMapper;
    +
    +import io.pivotal.workshop.snippet.domain.Code;
    +
    +public class CodeRowMapper implements RowMapper<Code> {
    +
    +        @Override
    +        public Code mapRow(ResultSet rs, int rowNum) throws SQLException {
    +                Code code = new Code();
    +                code.setId(rs.getString("id"));
    +                code.setSource(rs.getString("source"));
    +                return code;
    +        }
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.repository.mapper.SnippetRowMapper.java
    +
    +
    package io.pivotal.workshop.snippet.repository.mapper;
    +
    +import java.sql.ResultSet;
    +import java.sql.SQLException;
    +
    +import org.springframework.jdbc.core.RowMapper;
    +
    +import io.pivotal.workshop.snippet.domain.Code;
    +import io.pivotal.workshop.snippet.domain.Language;
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +
    +public class SnippetRowMapper implements RowMapper<Snippet> {
    +
    +        @Override
    +        public Snippet mapRow(ResultSet rs, int rowNum) throws SQLException {
    +                Language lang = new Language();
    +                lang.setId(rs.getString("lang_id"));
    +                lang.setName(rs.getString("lang_name"));
    +                lang.setSyntax(rs.getString("lang_syntax"));
    +
    +                Code code = new Code();
    +                code.setId(rs.getString("code_id"));
    +                code.setSource(rs.getString("code_source"));
    +
    +                Snippet snippet = new Snippet();
    +                snippet.setId(rs.getString("id"));
    +                snippet.setTitle(rs.getString("title"));
    +                snippet.setDescription(rs.getString("description"));
    +                snippet.setKeywords(rs.getString("keywords"));
    +                snippet.setCreated(rs.getDate("created"));
    +                snippet.setModified(rs.getDate("modified"));
    +                snippet.setLang(lang);
    +                snippet.setCode(code);
    +
    +                return snippet;
    +        }
    +
    +}
    +
    +
    +
  26. +
  27. +

    The SnippetController and the SnippetConfiguration classes must be the same. Practically there is no change on any of them, they should work.

    +
  28. +
  29. +

    Next add the schema.sql in the src/main/resources/ folder.

    +
    +
    +
    DROP TABLE IF EXISTS snippet;
    +CREATE TABLE snippet
    +(
    +    id varchar(36) NOT NULL,
    +    title varchar(200) NOT NULL,
    +    keywords varchar(250) DEFAULT NULL,
    +    description varchar(500) DEFAULT NULL,
    +    created date NOT NULL,
    +    modified date NOT NULL,
    +    PRIMARY KEY (id)
    +);
    +
    +
    +DROP TABLE IF EXISTS language;
    +CREATE TABLE language
    +(
    +    id varchar(36) NOT NULL,
    +    name varchar(250) NOT NULL,
    +    syntax varchar(250) DEFAULT NULL,
    +    PRIMARY KEY (id)
    +);
    +
    +DROP TABLE IF EXISTS code;
    +CREATE TABLE code
    +(
    +    id varchar(36) NOT NULL,
    +    source varchar(5000) NOT NULL,
    +    PRIMARY KEY (id)
    +);
    +
    +
    +DROP TABLE IF EXISTS cross_snippet_language_code;
    +CREATE TABLE cross_snippet_language_code
    +(
    +    snippet_id varchar(36) NOT NULL,
    +    language_id varchar(36) NOT NULL,
    +    code_id varchar(36) NOT NULL,
    +    PRIMARY KEY (snippet_id)
    +);
    +
    +
    +
  30. +
+
+
+ + + + + +
+
Tip
+
+Before you run your application make sure to have copied resources/static and resources/templates files from the previous labs. +
+
+
+

Challenges

+
+
    +
  • +

    Run the application and make sure it works.

    +
  • +
  • +

    Do any necessary change to have the home page working.

    +
    +
    Figure 1.1: Code Snippet Manager JDBC - http://localhost:8080/
    +

    Code Snippet Manager

    +
    +
  • +
+
+
+
+
+
+

Code Snippet Manager JPA

+
+
+

You will continue with the Code Snippet Manager code, but this time using JPA. You will reuse the code from previous labs.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager JPA Project metadata with (See Figure 2.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 2. Code Snippet Manager JPA App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-jpa

    Name:

    code-snippet-manager-jpa

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Web, DevTools, Groovy Templates, JPA, H2, MySQL

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 2.0: Spring Initializr - http://start.spring.io/
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    type Web, DevTools, Groovy Templates, JPA, H2, and MySQL in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy all the packages (with code) into the new project.

    +
  16. +
  17. +

    The classes in io.pivotal.workshop.snippet.domain: Code, Language and Snippet will change because you will use the JPA features. Create/modify the following classes:

    +
    +
    io.pivotal.workshop.snippet.domain.Language.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +import javax.persistence.Entity;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.Id;
    +
    +import org.hibernate.annotations.GenericGenerator;
    +
    +@Entity
    +public class Language {
    +
    +        @Id
    +        @GeneratedValue(generator = "system-uuid")
    +        @GenericGenerator(name = "system-uuid", strategy = "uuid")
    +        private String id;
    +        private String name;
    +        private String syntax = "text";
    +
    +        public Language() {
    +        }
    +
    +        public Language(String name) {
    +                this();
    +                this.name = name;
    +        }
    +
    +        public Language(String name, String syntax) {
    +                this(name);
    +                this.syntax = syntax;
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getName() {
    +                return name;
    +        }
    +
    +        public void setName(String name) {
    +                this.name = name;
    +        }
    +
    +        public String getSyntax() {
    +                return syntax;
    +        }
    +
    +        public void setSyntax(String syntax) {
    +                this.syntax = syntax;
    +        }
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.domain.Code.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +import javax.persistence.Entity;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.Id;
    +
    +import org.hibernate.annotations.GenericGenerator;
    +
    +@Entity
    +public class Code {
    +
    +        @Id
    +        @GeneratedValue(generator = "system-uuid")
    +        @GenericGenerator(name = "system-uuid", strategy = "uuid")
    +        private String id;
    +        private String source;
    +
    +        public Code() {
    +        }
    +
    +        public Code(String source) {
    +                this();
    +                this.source = source;
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getSource() {
    +                return source;
    +        }
    +
    +        public void setSource(String source) {
    +                this.source = source;
    +        }
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.domain.Snippet.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +import java.util.Date;
    +
    +import javax.persistence.CascadeType;
    +import javax.persistence.Column;
    +import javax.persistence.Entity;
    +import javax.persistence.FetchType;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.Id;
    +import javax.persistence.OneToOne;
    +import javax.persistence.PrePersist;
    +import javax.persistence.PreUpdate;
    +
    +import org.hibernate.annotations.GenericGenerator;
    +
    +@Entity
    +public class Snippet {
    +
    +        @Id
    +        @GeneratedValue(generator = "system-uuid")
    +        @GenericGenerator(name = "system-uuid", strategy = "uuid")
    +        private String id;
    +        private String title;
    +        private String keywords = "";
    +        private String description = "";
    +
    +        @OneToOne(fetch=FetchType.EAGER, cascade = CascadeType.ALL)
    +        private Language lang;
    +
    +        @OneToOne(fetch=FetchType.EAGER, cascade = CascadeType.ALL)
    +        private Code code;
    +
    +        @Column(insertable = true, updatable = false)
    +        private Date created;
    +        private Date modified;
    +
    +        public Snippet() {
    +                this.id = java.util.UUID.randomUUID().toString().replaceAll("-", "");
    +                this.created = new Date();
    +                this.modified = new Date();
    +        }
    +
    +        public Snippet(String title, String keywords, String description, Language lang, Code code) {
    +                this();
    +                this.title = title;
    +                this.keywords = keywords;
    +                this.description = description;
    +                this.lang = lang;
    +                this.code = code;
    +        }
    +
    +        public Snippet(String title, Language lang, Code code) {
    +                this(title, "", "", lang, code);
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getTitle() {
    +                return title;
    +        }
    +
    +        public void setTitle(String title) {
    +                this.title = title;
    +        }
    +
    +        public Language getLang() {
    +                return lang;
    +        }
    +
    +        public void setLang(Language lang) {
    +                this.lang = lang;
    +        }
    +
    +        public String getDescription() {
    +                return description;
    +        }
    +
    +        public void setDescription(String description) {
    +                this.description = description;
    +        }
    +
    +        public String getKeywords() {
    +                return keywords;
    +        }
    +
    +        public void setKeywords(String keywords) {
    +                this.keywords = keywords;
    +        }
    +
    +        public Code getCode() {
    +                return code;
    +        }
    +
    +        public void setCode(Code code) {
    +                this.code = code;
    +        }
    +
    +        public Date getCreated() {
    +                return created;
    +        }
    +
    +        public void setCreated(Date created) {
    +                this.created = created;
    +        }
    +
    +        public Date getModified() {
    +                return modified;
    +        }
    +
    +        public void setModified(Date modified) {
    +                this.modified = modified;
    +        }
    +
    +        @PrePersist
    +        void onCreate() {
    +                this.setCreated(new Date());
    +            this.setModified(new Date());
    +        }
    +
    +        @PreUpdate
    +        void onUpdate() {
    +                this.setModified(new Date());
    +        }
    +}
    +
    +
    +
    +

    See that you are using now the JPA standard annotations: @Entity, @Id, @OneToOne, etc.

    +
    +
  18. +
  19. +

    The repositories will change becasue you will use the power of the spring-data and spring-data-jpa projects. Create/Modify the following classes:

    +
    +
    io.pivotal.workshop.snippet.repository.LanguageRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import org.springframework.data.repository.CrudRepository;
    +
    +import io.pivotal.workshop.snippet.domain.Language;
    +
    +public interface LanguageRepository extends CrudRepository<Language, String> {
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.repository.CodeRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import org.springframework.data.repository.CrudRepository;
    +
    +import io.pivotal.workshop.snippet.domain.Code;
    +
    +public interface CodeRepository extends CrudRepository<Code, String> {
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.repository.SnippetRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import org.springframework.data.repository.CrudRepository;
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +
    +public interface SnippetRepository extends CrudRepository<Snippet, String> {
    +
    +}
    +
    +
    +
    +

    As you can see from the code above, you don’t need to implement anything but just extend from the CrudRepository<T,ID> interface, which uses generics, meaning that you need to +pass the domain class and the unique identifies, in this case what is marked as @Id. All the operation from the CrudRepository<T,ID> interface will be implemented by the spring-data classes.

    +
    +
  20. +
  21. +

    The SnippetController and the SnippetConfiguration classes must be the same. Practically there is no change on any of them, they should work.

    +
  22. +
  23. +

    This time there is no need for any SQL schema, this time you will use the DDL auto-creation feature. In the src/main/resources/application.properties file add the following content:

    +
    +
    src/main/resources/application.properties
    +
    +
    ## JPA
    +spring.jpa.generate-ddl=true
    +spring.jpa.hibernate.ddl-auto=create-drop
    +
    +
    +
  24. +
+
+
+ + + + + +
+
Tip
+
+Before you run your application make sure to have copied resources/static and resources/templates files from the previous labs. +
+
+
+

Challenges

+
+
    +
  • +

    Run the application and make sure it works.

    +
  • +
  • +

    Do any necessary change to have the home page working.

    +
    +
    Figure 1.1: Code Snippet Manager JPA - http://localhost:8080/
    +

    Code Snippet Manager JPA

    +
    +
  • +
+
+
+
+
+
+

Code Snippet Manager JPA REST

+
+
+

You will continue with the Code Snippet Manager code, but this time using Data Rest module. You will reuse the code from previous labs.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager JPA REST Project metadata with (See Figure 3.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 3. Code Snippet Manager JPA REST App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-jpa-rest

    Name:

    code-snippet-manager-jpa-rest

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, MySQL

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 3.0: Spring Initializr - http://start.spring.io/
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    type Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, and MySQL in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy all the packages (with code) into the new project.

    +
  16. +
  17. +

    The only class that will change will be the SnippetController. Create/modify the folowing class:

    +
    +
    io.pivotal.workshop.snippet.controller.SnippetController.java
    +
    +
    package io.pivotal.workshop.snippet.controller;
    +
    +import java.util.HashMap;
    +import java.util.Map;
    +
    +import org.springframework.web.bind.annotation.RequestMapping;
    +import org.springframework.web.bind.annotation.RestController;
    +import org.springframework.web.servlet.ModelAndView;
    +
    +import io.pivotal.workshop.snippet.repository.LanguageRepository;
    +import io.pivotal.workshop.snippet.repository.SnippetRepository;
    +
    +@RestController
    +public class SnippetController {
    +
    +
    +        private SnippetRepository snippetRepository;
    +        private LanguageRepository languageRepository;
    +
    +        public SnippetController(SnippetRepository snippetRepository,LanguageRepository languageRepository){
    +                this.snippetRepository = snippetRepository;
    +                this.languageRepository = languageRepository;
    +        }
    +
    +        @RequestMapping("/")
    +        public ModelAndView home(){
    +                assert snippetRepository != null;
    +
    +                Map<String,Object> model = new HashMap<String,Object>();
    +                model.put("langs", languageRepository.findAll());
    +                model.put("snippets", snippetRepository.findAll());
    +
    +                return new ModelAndView("views/home",model);
    +        }
    +
    +}
    +
    +
    +
    +

    As you can see, only is necessary the home method, and this is because the spring-data-rest module will take care of creating all the web controllers for accepting any request regarding to the domain repositories.

    +
    +
  18. +
  19. +

    When using the spring-data-rest libraries, the default path for the controllers is the root: /, but in this case you are using the root as home page, that’s why is necessary to override the spring-data-rest context path defaults, +in the src/main/resources/application.properties file add the following:

    +
    +
    src/main/resources/application.properties
    +
    +
    ## JPA
    +spring.jpa.generate-ddl=true
    +spring.jpa.hibernate.ddl-auto=create-drop
    +
    +## REST
    +spring.data.rest.base-path=api
    +
    +
    +
    +

    Now you should be able to use the http://localhost:8080/api to access the REST repositories.

    +
    +
  20. +
+
+
+ + + + + +
+
Tip
+
+Before you run your application make sure to have copied resources/static and resources/templates files from the previous labs. +
+
+
+

Challenges

+
+
    +
  • +

    Run the application and make sure it works.

    +
    + + + + + +
    +
    Tip
    +
    +See the logs and take a look at all the mapping that is being generated. +
    +
    +
  • +
  • +

    Do any necessary change to have the home page working.

    +
    +
    Figure 3.1: Code Snippet Manager JPA REST - http://localhost:8080/
    +

    Code Snippet Manager

    +
    +
  • +
  • +

    Go to the http://localhost:8080/api, you should see the following image:

    +
    +
    Figure 3.2: Code Snippet Manager JPA REST - http://localhost:8080/api
    +

    Code Snippet Manager API

    +
    +
    +

    and if you click the http://localhost:8080/api/snippets you should see the following image:

    +
    +
    +
    Figure 3.2: Code Snippet Manager JPA REST - http://localhost:8080/api/snippets
    +

    Code Snippet Manager API

    +
    +
  • +
  • +

    Review all the links and see that the all the information is being translated into a JSON/HAL.

    +
  • +
  • +

    So far we are using the H2 engine, do the necessary changes to use now MySQL, how can you accomplish this?

    +
  • +
+
+
+
+
+
+

HOMEWORK

+
+
+
    +
  • +

    Use multiple databases like MySQL and MongoDB (multiple Datasources).

    +
  • +
  • +

    Use Flyway or Liquibase to do a Database migration. Add an executeCommand field to the Code domain class. This field will tell how to execute the source code provided.

    +
  • +
  • +

    Use jOOQ framework.

    +
  • +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/07-spring-boot-testing.html b/src/main/UploadedContent/student-lab-instructions/07-spring-boot-testing.html new file mode 100644 index 0000000..9a27e9b --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/07-spring-boot-testing.html @@ -0,0 +1,825 @@ + + + + + + + +Spring Boot Testing + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with the Spring Testing and the Spring Boot Testing features.

+
+
+

Time: 20 minutes.

+
+ +
+
+
+

Code Snippet Manager JPA REST Testing

+
+
+

In this lab you will use the code from the Code Snippet Manager JPA Rest project. You will adding some tests.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager JPA REST Test Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Code Snippet Manager JPA REST Test App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-jpa-rest-test

    Name:

    code-snippet-manager-jpa-rest-test

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, MySQL

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io/
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, and MySQL in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy all the packages (with code) into the new project. None of the code will be changed.

    +
  16. +
  17. +

    In this lab you will use also a 3rd Party library called: Rest Assured. Add the following dependency to your pom.xml or build.gradle

    +
    +
    pom.xml
    +
    +
    <dependency>
    +        <groupId>io.rest-assured</groupId>
    +        <artifactId>spring-mock-mvc</artifactId>
    +        <version>3.0.0</version>
    +        <scope>test</scope>
    +</dependency>
    +
    +
    +
    +
    build.gradle
    +
    +
    testCompile('io.rest-assured:spring-mock-mvc:3.0.0')
    +
    +
    +
  18. +
  19. +

    Add in the src/test/java folder in the package io.pivotal.workshop.snippet the following integration test:

    +
    +
    io.pivotal.workshop.snippet.IntegrationTest.java
    +
    +
    package io.pivotal.workshop.snippet;
    +
    +import static io.restassured.module.mockmvc.RestAssuredMockMvc.when;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.hamcrest.CoreMatchers.equalTo;
    +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    +
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.stream.StreamSupport;
    +
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.boot.test.context.SpringBootTest;
    +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
    +import org.springframework.boot.test.web.client.TestRestTemplate;
    +import org.springframework.core.ParameterizedTypeReference;
    +import org.springframework.hateoas.Resource;
    +import org.springframework.hateoas.Resources;
    +import org.springframework.http.HttpMethod;
    +import org.springframework.http.HttpStatus;
    +import org.springframework.http.ResponseEntity;
    +import org.springframework.test.context.junit4.SpringRunner;
    +import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    +import org.springframework.web.context.WebApplicationContext;
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +
    +@RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
    +public class IntegrationTest {
    +
    +        @Autowired
    +        private TestRestTemplate restTemplate;
    +
    +        @Autowired
    +        private WebApplicationContext context;
    +
    +        @Before
    +        public void setUp() {
    +                RestAssuredMockMvc.mockMvc(MockMvcBuilders.webAppContextSetup(context).build());
    +        }
    +
    +        @Test
    +        public void homePageTest() {
    +                String body = this.restTemplate.getForObject("/", String.class);
    +                assertThat(body).contains("Hello World");
    +        }
    +
    +        @Test
    +        public void linksTests() {
    +                when().get("/api/snippets").then().assertThat(status().isOk()).body("_links.self.href",
    +                                equalTo("http://localhost/api/snippets"));
    +        }
    +}
    +
    +
    +
    +

    Take a look at the code and see that is exposing the RestAssuredMockMvc and also the usage of the TestRestTemplate.

    +
    +
  20. +
+
+
+

Challenges: Integration Tests

+
+
    +
  • +

    Analyze the code and run the test, either using your IDE, maven or gradle.

    +
  • +
  • +

    Add a new test method restControllerTest that asserts the HAL/JSON response.

    +
    + + + + + +
    +
    Tip
    +
    +Use the ResponseEntity<Resources<Resource<Snippet>>> instance and the restTemplate.exchange method. +
    +
    +
  • +
+
+
+
+

Challenges: Slices

+
+
    +
  • +

    Add a new JsonTest class and use the @JsonTest annotation. You will use the JacksonTester<T> class and the assertj library to do the assertions.

    +
  • +
  • +

    Add a new JpaTests class and use the @DataJpaTest annotation. You will use the TestEntityManager class and the hamcrest library to do the assertions.

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/08-spring-boot-actuator.html b/src/main/UploadedContent/student-lab-instructions/08-spring-boot-actuator.html new file mode 100644 index 0000000..2ac8a58 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/08-spring-boot-actuator.html @@ -0,0 +1,932 @@ + + + + + + + +Spring Boot Actuator + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with the Spring Boot Actuator and its features.

+
+
+

Time: 20 minutes.

+
+ +
+
+
+

Code Snippet Manager Actuator

+
+
+

You will continue with the Code Snippet Manager code. For this lab you will need the snippet-code-manager-jpa-rest project code. You will reuse the code from previous labs.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager Actuator Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Code Snippet Manager Actuator - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-actuator

    Name:

    code-snippet-manager-actuator

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, MySQL, Actuator, HATEOAS

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io/
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, MySQL, Actuator, HATEOAS in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy all the packages (with code) into the new project.

    +
  16. +
  17. +

    The packages: domain, repository and config will remain with no change.

    +
  18. +
+
+
+

Spring Boot Actuator: Review Endpoints

+
+
    +
  1. +

    In the src/main/resources/application.properties:

    +
    +
      +
    • +

      Enable all the endpoints

      +
    • +
    • +

      Change the default path /actuator, instead use the path /admin

      +
      +
      application.properties
      +
      +
      management.endpoints.web.base-path=/admin
      +management.endpoints.web.expose=*
      +
      +
      +
    • +
    +
    +
  2. +
  3. +

    Run the application and make sure all the endpoints are enable. Review all of them.

    +
  4. +
+
+
+
+

Spring Boot Actuator: HealthIndicator

+
+

Using the same code, imagine that we are going to use a FileSystem to save some information. A specific path, by using the snippet.path property. We need to know:

+
+
+
    +
  • +

    If the path doesn’t exists, report out of service.

    +
  • +
  • +

    If the path exists but doesn’t have writing permissions, report system down.

    +
  • +
  • +

    If the path exists and it has write permissions, report up.

    +
  • +
+
+
+
    +
  1. +

    Let’s create a properties holder class. Add the SnippetProperties class in the io.pivotal.workshop.snippet.config package:

    +
    +
    io.pivotal.workshop.snippet.config.SnippetProperties.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +
    +@ConfigurationProperties(prefix = "snippet")
    +public class SnippetProperties {
    +
    +        private String path;
    +
    +        public String getPath() {
    +                return path;
    +        }
    +
    +        public void setPath(String path) {
    +                this.path = path;
    +        }
    +
    +}
    +
    +
    +
    +

    The above code will hold the information about the path with the property: snippet.path.

    +
    +
  2. +
  3. +

    Create the SnippetHealthCheck class in the io.pivotal.workshop.snippet.actuator package:

    +
    +
    io.pivotal.workshop.snippet.actuator.SnippetHealthCheck.java
    +
    +
    package io.pivotal.workshop.snippet.actuator;
    +
    +import java.io.File;
    +
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.actuate.health.Health;
    +import org.springframework.boot.actuate.health.HealthIndicator;
    +import org.springframework.stereotype.Component;
    +
    +@Component
    +public class SnippetHealthCheck implements HealthIndicator {
    +
    +        private String path;
    +
    +        public SnippetHealthCheck(@Value("${snippet.path:/tmp}")String path){
    +                this.path = path;
    +        }
    +
    +        @Override
    +        public Health health() {
    +
    +                try {
    +
    +                        File file = new File(path);
    +                        if(file.exists()){
    +
    +                                if(file.canWrite())
    +                                        return Health.up().build();
    +
    +                                return Health.down().build();
    +
    +                        }else{
    +                                return Health.outOfService().build();
    +                        }
    +                }catch(Exception ex) {
    +                        return Health.down(ex).build();
    +                }
    +        }
    +
    +}
    +
    +
    +
    +

    Take a look at the constructor. If the snippet.path is not found, it will use the /tmp as default path.

    +
    +
  4. +
  5. +

    In the src/main/resources/application.properties file add the following property:

    +
    +
    +
    ## Snippet
    +snippet.path=/tmp/snippets
    +
    +
    +
    + + + + + +
    +
    Tip
    +
    +Do not create this folder yet, we will make sure our custom health indicator works. +
    +
    +
    + + + + + +
    +
    Tip
    +
    +If you are using Windows try to use the C:/tmp/snippets or C:\\tmp\\snippets format. +
    +
    +
  6. +
  7. +

    If you run the application and go to the http://localhost:8080/admin/health, you should have something like the following Figure 3.0:

    +
    +
    Figure 3.0: Spring Boot Actuator - http://localhost:8080/admin/health
    +

    Spring Boot Actuator

    +
    +
    +

    Here you can see the SnippetHealthCheck value: OUT_OF_SERVICE.

    +
    +
  8. +
  9. +

    Create the /tmp/snippets (or C:\tmp\snippets for Windows) path but add/modify only read access, and then you can refresh the page, you should see the same as Figure 4.0:

    +
    +
    Figure 4.0: Spring Boot Actuator - http://localhost:8080/admin/health
    +

    Spring Boot Actuator

    +
    +
    +

    Here you can see the SnippetHealthCheck value: DOWN.

    +
    +
  10. +
  11. +

    Modify the path to write access and refresh, you should have the following, see Figure 5.0.

    +
    +
    Figure 5.0: Spring Boot Actuator - http://localhost:8080/admin/health
    +

    Spring Boot Actuator

    +
    +
    +

    Here you can see the SnippetHealthCheck value: UP.

    +
    +
  12. +
+
+
+
+

CHALLENGES

+
+
    +
  1. +

    Add a graceful shutdown to the application by executing the following command:

    +
    +
    +
    curl -X POST localhost:8080/admin/shutdown
    +
    +
    +
    + + + + + +
    +
    Tip
    +
    +If you are a Windows user, you can use the POSTMAN (https://www.getpostman.com/) application to execute the POST. +
    +
    +
  2. +
+
+
+
+

HOMEWORK

+
+

Review the Micrometer technology (http://micrometer.io) and apply it to the Code Snippet Manager.

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/09-spring-boot-security.html b/src/main/UploadedContent/student-lab-instructions/09-spring-boot-security.html new file mode 100644 index 0000000..8f17b69 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/09-spring-boot-security.html @@ -0,0 +1,1293 @@ + + + + + + + +Spring Boot Security + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with Spring Security and the Spring Boot Security features.

+
+
+

Time: 35 minutes.

+
+ +
+
+
+

Directory Web Security App

+
+
+

Remember this lab? Where we have a persons directory? We are going to re-take part of the code and make this project more secure. +You saw in the demo how easy is to set up the security using the JDBC but it was using a pre-configured schema (users and authorities). +In this lab we will use our own schema (our own data) so you see how easy is to implement spring-security in a web project.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Directory Web App Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Directory Web Security App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    directory-web-security

    Name:

    directory-web-security

    Package Name:

    io.pivotal.workshop.directory

    Dependencies:

    Web, DevTools, H2, MySQL, Security, JPA, Rest Repositories, Actuator, HATEOAS, Groovy Templates

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web, DevTools, H2, MySQL, Security, JPA, Rest Repositories, Actuator, HATEOAS and Groovy Templates in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    You can copy the code from the first labs (Spring Boot Overview).

    +
  16. +
  17. +

    Because we are using JPA and Rest Repositories dependencies, lets convert the Person as entity. Create/Modify the Person class:

    +
    +
    io.pivotal.workshop.directory.domain.Person.java
    +
    +
    package io.pivotal.workshop.directory.domain;
    +
    +import java.text.ParseException;
    +import java.text.SimpleDateFormat;
    +import java.util.Date;
    +
    +import javax.persistence.Column;
    +import javax.persistence.Entity;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.Id;
    +import javax.persistence.PrePersist;
    +import javax.persistence.PreUpdate;
    +import javax.persistence.Transient;
    +
    +import org.hibernate.annotations.GenericGenerator;
    +
    +@Entity
    +public class Person {
    +
    +        @Transient
    +        private SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd");
    +
    +        @Id
    +        @GeneratedValue(generator = "system-uuid")
    +        @GenericGenerator(name = "system-uuid", strategy = "uuid")
    +        private String id;
    +        @Column(unique = true)
    +        private String email;
    +        private String name;
    +        private String password;
    +        private String role = "USER";
    +        private boolean enabled = true;
    +        private Date birthday;
    +
    +        @Column(insertable = true, updatable = false)
    +        private Date created;
    +        private Date modified;
    +
    +        public Person() {
    +                this.created = new Date();
    +                this.modified = new Date();
    +        }
    +
    +        public Person(String email, String name, String password, String birthday) {
    +                this();
    +                this.email = email;
    +                this.name = name;
    +                this.password = password;
    +
    +                try {
    +                        this.birthday = date.parse(birthday);
    +                } catch (ParseException e) {
    +                        this.birthday = null;
    +                }
    +        }
    +
    +        public Person(String email, String name, String password, Date birthday) {
    +                this();
    +                this.email = email;
    +                this.name = name;
    +                this.password = password;
    +                this.birthday = birthday;
    +        }
    +
    +        public Person(String email, String name, String password, String birthday, String role, boolean enabled) {
    +                this(email, name, password, birthday);
    +                this.role = role;
    +                this.enabled = enabled;
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getEmail() {
    +                return email;
    +        }
    +
    +        public void setEmail(String email) {
    +                this.email = email;
    +        }
    +
    +        public String getName() {
    +                return name;
    +        }
    +
    +        public void setName(String name) {
    +                this.name = name;
    +        }
    +
    +        public String getPassword() {
    +                return password;
    +        }
    +
    +        public void setPassword(String password) {
    +                this.password = password;
    +        }
    +
    +        public Date getBirthday() {
    +                return birthday;
    +        }
    +
    +        public void setBirthday(Date birthday) {
    +                this.birthday = birthday;
    +        }
    +
    +        public Date getCreated() {
    +                return created;
    +        }
    +
    +        public Date getModified() {
    +                return modified;
    +        }
    +
    +        public String getRole() {
    +                return role;
    +        }
    +
    +        public void setRole(String role) {
    +                this.role = role;
    +        }
    +
    +        public boolean isEnabled() {
    +                return enabled;
    +        }
    +
    +        public void setEnabled(boolean enabled) {
    +                this.enabled = enabled;
    +        }
    +
    +        @PrePersist
    +        void onCreate() {
    +                this.created = new Date();
    +                this.modified = new Date();
    +        }
    +
    +        @PreUpdate
    +        void onUpdate() {
    +                this.modified = new Date();
    +        }
    +}
    +
    +
    +
    +

    See that we are using the @Entity and @Id annotations from JPA. What is new in this class is the two new fields: role and enabled, that we are going to use later on.

    +
    +
  18. +
  19. +

    Next, create/modify the PersonRepository class:

    +
    +
    io.pivotal.workshop.directory.repository.PersonRepository.java
    +
    +
    package io.pivotal.workshop.directory.repository;
    +
    +import org.springframework.data.repository.CrudRepository;
    +
    +import io.pivotal.workshop.directory.domain.Person;
    +import org.springframework.data.repository.query.Param;
    +
    +public interface PersonRepository extends CrudRepository<Person,String>{
    +
    +        public Person findByEmailIgnoreCase(@Param("email") String email);
    +}
    +
    +
    +
    +

    This is part of the spring-data project, where only by extending from the CrudRepository<T,ID> interface we get all the persistence functionality. Also take a look that we are defining a findBy named method, that will be also implemented for us.

    +
    +
  20. +
  21. +

    Next, let create a configuration that will initialize our database:

    +
    +
    io.pivotal.workshop.directory.config.DirectoryConfig.java
    +
    +
    @Configuration
    +public class DirectoryConfig extends WebMvcConfigurerAdapter {
    +
    +        @Override
    +        public void addViewControllers(ViewControllerRegistry registry) {
    +                registry.addViewController("/").setViewName("views/home");
    +        }
    +
    +        @Bean
    +        public CommandLineRunner directoryProcess(PersonRepository repo) {
    +                return args -> {
    +                        repo.save(new Person("admin", "Administrator", "admin", "1980-08-22", "ADMIN", true));
    +                        repo.save(new Person("john@email.com", "John C.", "simplepwd", "1980-08-03", "USER", true));
    +                        repo.save(new Person("mike@email.com", "Mike H.", "simplepwd", "1980-04-10", "USER", true));
    +                        repo.save(new Person("mark@email.com", "Mark S.", "simplepwd", "1981-10-08", "USER", true));
    +            repo.save(new Person("dan@email.com", "Dan B.", "simplepwd", "1981-10-08", "ACTUATOR", true));
    +                };
    +        }
    +}
    +
    +
    +
    +

    As you can see we are extending from WebMvcConfigurerAdapter and the purpose of this is to configure our home page (or view) by overriding the addViewControllers method (this is another way to configure a web controller).

    +
    +
  22. +
  23. +

    We need to add our own security based on the Person class. Let’s add the security configuration. Create the DirectorySecurityConfig class:

    +
    +
    io.pivotal.workshop.directory.config.DirectorySecurityConfig.java
    +
    +
    package io.pivotal.workshop.directory.config;
    +
    +import io.pivotal.workshop.directory.security.DirectoryUserDetailsService;
    +import org.springframework.boot.actuate.autoconfigure.security.EndpointRequest;
    +import org.springframework.boot.autoconfigure.security.StaticResourceRequest;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    +
    +@Configuration
    +public class DirectorySecurityConfig extends WebSecurityConfigurerAdapter{
    +
    +
    +        private DirectoryUserDetailsService userDetailsService;
    +
    +        public DirectorySecurityConfig(DirectoryUserDetailsService userDetailsService){
    +                this.userDetailsService = userDetailsService;
    +        }
    +
    +        @Override
    +        protected void configure(HttpSecurity http) throws Exception {
    +        http
    +            .authorizeRequests()
    +                    .requestMatchers(EndpointRequest.to("status", "info"))
    +                    .permitAll()
    +
    +                .requestMatchers(EndpointRequest.toAnyEndpoint())
    +                    .hasRole("ACTUATOR")
    +
    +                .requestMatchers(StaticResourceRequest.toCommonLocations())
    +                    .permitAll()
    +
    +                .antMatchers("/api/**").hasRole("ADMIN")
    +                .antMatchers("/").permitAll()
    +
    +            .and()
    +                .formLogin();
    +        }
    +
    +        @Override
    +        public void configure(AuthenticationManagerBuilder auth) throws Exception {
    +                auth.userDetailsService(this.userDetailsService);
    +        }
    +
    +}
    +
    +
    +
    +

    As you can see we are extending from WebSecurityConfigurerAdapter and it give us a way to override some methods, +in this case the configure(HttpSecurity) (that provides an easy way to configure the request access) +and configure(AuthenticationManagerBuilder (where we are adding our custom secured service, in this case the UserDetailsService).

    +
    +
  24. +
  25. +

    Next, create the DirectoryUserDetailsService class that will have our custom access to our own schema:

    +
    +
    io.pivotal.workshop.directory.security.DirectoryUserDetailsService.java
    +
    +
    package io.pivotal.workshop.directory.security;
    +
    +import io.pivotal.workshop.directory.domain.Person;
    +import io.pivotal.workshop.directory.repository.PersonRepository;
    +import org.springframework.security.core.userdetails.User;
    +import org.springframework.security.core.userdetails.UserDetails;
    +import org.springframework.security.core.userdetails.UserDetailsService;
    +import org.springframework.security.core.userdetails.UsernameNotFoundException;
    +import org.springframework.stereotype.Component;
    +
    +@Component
    +public class DirectoryUserDetailsService  implements UserDetailsService {
    +
    +        private PersonRepository repo;
    +
    +        public DirectoryUserDetailsService(PersonRepository repo) {
    +                this.repo = repo;
    +        }
    +
    +        @Override
    +        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    +            try {
    +            final Person person = this.repo.findByEmailIgnoreCase(username);
    +            return User.withDefaultPasswordEncoder().username(person.getEmail()).password(person.getPassword()).roles(person.getRole()).build();
    +        }catch(Exception ex){
    +                ex.printStackTrace();
    +                throw new UsernameNotFoundException(username);
    +        }
    +        }
    +}
    +
    +
    +
    +

    In this class we are including the PersonRepository and we are using the findByEmail method. +See that we are implementing the UserDetailsService interface and we are implementing the loadUserByUsername that returns a UserDetails.

    +
    +
  26. +
  27. +

    Next, open the src/main/resources/application.properties file and add/modify it to look like the following:

    +
    +
    src/main/resources/application.properties
    +
    +
    ## Server
    +server.port=${port:8585}
    +
    +## REST
    +spring.data.rest.base-path=api
    +
    +## ACTUATOR
    +management.context-path=/admin
    +
    +## JPA
    +spring.jpa.generate-ddl=true
    +spring.jpa.hibernate.ddl-auto=create-drop
    +
    +
    +
    +

    As you can see, all these properties are well known from previous labs. The Rest repository is exposed in the /api endpoint and the spring-boot-actuator endpoint at the /admin context-path.

    +
    +
  28. +
  29. +

    Add the necessary UI, remember where? Here are the files you need:

    +
    + +
    +
  30. +
  31. +

    Don’t forget to add the necessary dependencies in your pom.xml or build.gradle.

    +
    +
    pom.xml
    +
    +
    <dependency>
    +        <groupId>org.webjars</groupId>
    +        <artifactId>jquery</artifactId>
    +        <version>2.2.4</version>
    +</dependency>
    +<dependency>
    +        <groupId>org.webjars</groupId>
    +        <artifactId>bootstrap</artifactId>
    +        <version>3.3.6</version>
    +</dependency>
    +<dependency>
    +        <groupId>org.webjars</groupId>
    +        <artifactId>angularjs</artifactId>
    +        <version>1.5.7</version>
    +</dependency>
    +
    +
    +
    +
    build.gradle
    +
    +
    compile('org.webjars:jquery:2.2.4')
    +compile('org.webjars:bootstrap:3.3.6')
    +compile('org.webjars:angularjs:1.5.7')
    +
    +
    +
  32. +
  33. +

    Run the application, either command line or through your IDE. If you go to the http://localhost:8585 in your browser, you should get the same as the following Figure 2.0:

    +
    +
    Figure 2.0: Directory Web Security App - http://localhost:8585
    +

    Directory Web Security App

    +
    +
  34. +
  35. +

    If you try to go to the http://localhost:8585/api, you should get the following Figure 3.0:

    +
    +
    Figure 3.0: Directory Web Security App Login - http://localhost:8585/api
    +

    Directory Web Security App

    +
    +
    +

    You can now use one of the persons we added in the configurations, for example use: admin and admin as password, and you should get now the Person Repository Rest API response.

    +
    +
  36. +
+
+
+ + + + + +
+
Tip
+
+If by any reason during the testing in your browser you get the "403 - Forbidden error", try to remove the CACHE from your browser. Remember that you can still use a curl command or if you are a Windows user, you can use POSTMAN https://www.getpostman.com/. +
+
+
+
+
+

Code Snippet Manager Security

+
+
+

The purpose of this Lab is to add security to your Code Snippet Manager and prepare it for the Challenges. The idea is that the Code Snippet Manager will use the directory-web-security to authenticate to use the snippet /api.

+
+
+

You will reuse the code from the code-snippet-manager-actuator project.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager Security Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 2. Code Snippet Manager Security App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-security

    Name:

    code-snippet-manager-security

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Web, DevTools, H2, MySQL, Security, JPA, Rest Repositories, Actuator, HATEOAS, Groovy Templates

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web, DevTools, H2, MySQL, Security, JPA, Rest Repositories, Actuator, HATEOAS and Groovy Templates in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy all the code from code-snippet-manager-actuator

    +
  16. +
  17. +

    Create a io.pivotal.workshop.snippet.domain.Person class. This will be use for using the directory-web-security domain.

    +
    +
    /src/main/java/io/pivota/workshop/snippet/domain/Person.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +
    +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    +
    +@JsonIgnoreProperties(ignoreUnknown = true)
    +public class Person {
    +
    +    private String email;
    +    private String password;
    +    private String role;
    +
    +    public String getEmail() {
    +        return email;
    +    }
    +
    +    public void setEmail(String email) {
    +        this.email = email;
    +    }
    +
    +    public String getPassword() {
    +        return password;
    +    }
    +
    +    public void setPassword(String password) {
    +        this.password = password;
    +    }
    +
    +    public String getRole() {
    +        return role;
    +    }
    +
    +    public void setRole(String role) {
    +        this.role = role;
    +    }
    +}
    +
    +
    +
    + + + + + +
    +
    Tip
    +
    +You can get all the templates, css and js files from other projects. +
    +
    +
  18. +
  19. +

    That’s it, just preparing the code-snippet-manager for the Challenges

    +
  20. +
+
+
+

Challenges

+
+
    +
  • +

    Make sure you have access to the /admin/health actuator endpoint in the directory-web-security project. Use the user that has the role ACTUATOR.

    +
  • +
  • +

    Make the HOME PAGE is only reachable to users with role USER in the directory-web-security project*.

    +
  • +
  • +

    Modify the code-snippet-manager-security project and use the directory-web-security project as authentication authority.

    +
  • +
+
+
+
+
+
+

HOMEWORK

+
+
+
    +
  • +

    Add SSL to both projects.

    +
  • +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/10-spring-boot-messaging.html b/src/main/UploadedContent/student-lab-instructions/10-spring-boot-messaging.html new file mode 100644 index 0000000..6471844 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/10-spring-boot-messaging.html @@ -0,0 +1,1467 @@ + + + + + + + +Spring Boot AMQP + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with the RabbitMQ and Spring Boot AMQP and its features.

+
+
+

Time: 20 minutes.

+
+ +
+
+
+

Part 1: Code Snippet Manager AMQP

+
+
+

You will continue with the Code Snippet Manager code. For this lab you will need the snippet-code-manager-actuator project code.

+
+
+

There are new requirements for the Code Snippet Manager:

+
+
+
    +
  • +

    Receive code snippets using RabbitMQ:

    +
    +
      +
    • +

      The communication should be as RPC (emulate synchronous communication) from/to a client.

      +
    • +
    • +

      Accept messages for save, update and delete code snippets.

      +
    • +
    • +

      Create Listeners for two queues: snippet.upsert and snippet.remove.

      +
    • +
    • +

      For every message that process, send a Snippet Notification to the snippet.notifications queue.

      +
    • +
    +
    +
  • +
  • +

    Should you be able to create the next topology:

    +
    +
    RabbitMQ Topology
    +

    Topology

    +
    +
  • +
+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager AMQP Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Code Snippet Manager AMQP - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-amqp

    Name:

    code-snippet-manager-amqp

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, MySQL, Actuator, HATEOAS, AMQP

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io/
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, MySQL, Actuator, HATEOAS and AMQP in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy all the packages (with code) into the new project.

    +
  16. +
  17. +

    The packages: domain, actuator, controller, repository and config will remain with no change.

    +
  18. +
  19. +

    Next, start by adding the Listeners that will be using the queues, create the SnippetAmqpListener class in the io.pivotal.workshop.snippet.amqp package:

    +
    +
    io.pivotal.workshop.snippet.amqp.SnippetAmqpListener.java
    +
    +
    package io.pivotal.workshop.snippet.amqp;
    +
    +import java.util.Date;
    +
    +import org.springframework.amqp.rabbit.annotation.RabbitListener;
    +import org.springframework.messaging.handler.annotation.SendTo;
    +import org.springframework.stereotype.Component;
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import io.pivotal.workshop.snippet.domain.SnippetNotification;
    +import io.pivotal.workshop.snippet.repository.SnippetRepository;
    +
    +@Component
    +public class SnippetAmqpListener {
    +
    +        private SnippetRepository repo;
    +        public SnippetAmqpListener(SnippetRepository repo){
    +                this.repo = repo;
    +        }
    +
    +
    +        @RabbitListener(queues={"snippet.upsert"})
    +        @SendTo
    +        public SnippetNotification save(Snippet snippet){
    +                Snippet result = this.repo.save(snippet);
    +                return new SnippetNotification(result.getId(),"SAVE",new Date(),null);
    +        }
    +
    +        @RabbitListener(queues={"snippet.remove"})
    +        @SendTo
    +        public SnippetNotification delete(Snippet snippet){
    +                this.repo.deleteById(snippet.getId());
    +                return new SnippetNotification(snippet.getId(),"DELETE",new Date(),null);
    +        }
    +
    +}
    +
    +
    +
    +

    In the code above we are using the @RabbitListener and @SendTo annotations. There are two methods that handle the messages (receiving a Snippet) from the two queues. +Also notice that both methods return a new SnippetNotification. Actually the @SendTo annotation will be the mechanism to reply that notification to the client.

    +
    +
  20. +
  21. +

    Create in the domain package the SnippetNotification and SnippetError classes:

    +
    +
    io.pivotal.workshop.snippet.domain.SnippetNotification.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +import java.util.Date;
    +
    +public class SnippetNotification {
    +
    +        private String snippetId;
    +        private String action;
    +        private Date processed;
    +        private SnippetError error;
    +
    +        public SnippetNotification() {
    +        }
    +
    +        public SnippetNotification(String snippetId, String action, Date processed, SnippetError error) {
    +                super();
    +                this.snippetId = snippetId;
    +                this.action = action;
    +                this.processed = processed;
    +                this.error = error;
    +        }
    +
    +        public String getSnippetId() {
    +                return snippetId;
    +        }
    +
    +        public void setSnippetId(String snippetId) {
    +                this.snippetId = snippetId;
    +        }
    +
    +        public String getAction() {
    +                return action;
    +        }
    +
    +        public void setAction(String action) {
    +                this.action = action;
    +        }
    +
    +        public Date getProcessed() {
    +                return processed;
    +        }
    +
    +        public void setProcessed(Date processed) {
    +                this.processed = processed;
    +        }
    +
    +        public SnippetError getError() {
    +                return error;
    +        }
    +
    +        public void setError(SnippetError error) {
    +                this.error = error;
    +        }
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.domain.SnippetError.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +public class SnippetError {
    +
    +        private String message;
    +        private String errorCode;
    +
    +        public SnippetError() {
    +        }
    +
    +        public SnippetError(String message, String errorCode) {
    +                this.message = message;
    +                this.errorCode = errorCode;
    +        }
    +
    +        public String getMessage() {
    +                return message;
    +        }
    +
    +        public void setMessage(String message) {
    +                this.message = message;
    +        }
    +
    +        public String getErrorCode() {
    +                return errorCode;
    +        }
    +
    +        public void setErrorCode(String errorCode) {
    +                this.errorCode = errorCode;
    +        }
    +}
    +
    +
    +
  22. +
  23. +

    Next let’s create the configuration that will be use to convert the incoming messages into a Snippet instances and also let’s wire up the topology we are going to need. +Create the SnippetAmqpConfig class in the io.pivotal.workshop.snippet.config package:

    +
    +
    io.pivotal.workshop.snippet.config.SnippetAmqpConfig.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +import org.springframework.amqp.core.AmqpAdmin;
    +import org.springframework.amqp.core.Binding;
    +import org.springframework.amqp.core.Binding.DestinationType;
    +import org.springframework.amqp.core.DirectExchange;
    +import org.springframework.amqp.core.Queue;
    +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
    +import org.springframework.amqp.rabbit.connection.ConnectionFactory;
    +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    +import org.springframework.boot.CommandLineRunner;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +
    +@Configuration
    +public class SnippetAmqpConfig {
    +
    +        private final String SNIPPET_EXCHANGE = "snippet.manager";
    +
    +        @Bean
    +        public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
    +                SimpleRabbitListenerContainerFactory container = new SimpleRabbitListenerContainerFactory();
    +                container.setConnectionFactory(connectionFactory);
    +                container.setMessageConverter(new Jackson2JsonMessageConverter());
    +                return container;
    +        }
    +
    +        @Bean
    +        public DirectExchange directExchange(){
    +                return new DirectExchange(SNIPPET_EXCHANGE, true, false);
    +        }
    +
    +        @Bean
    +        public Queue upsert(){
    +                return new Queue("snippet.upsert");
    +        }
    +
    +        @Bean
    +        public Queue remove(){
    +                return new Queue("snippet.remove");
    +        }
    +
    +        @Bean
    +        public CommandLineRunner queuesAndBindings(AmqpAdmin admin){
    +                return args -> {
    +                    admin.declareBinding(new Binding("snippet.upsert", DestinationType.QUEUE, SNIPPET_EXCHANGE, "snippet.save", null));
    +                        admin.declareBinding(new Binding("snippet.remove", DestinationType.QUEUE, SNIPPET_EXCHANGE, "snippet.delete", null));
    +                        admin.declareBinding(new Binding("snippet.upsert", DestinationType.QUEUE, SNIPPET_EXCHANGE, "snippet.update", null));
    +                };
    +        }
    +}
    +
    +
    +
    +

    In the above code we need to create a SimpleRabbitListenerContainerFactory so it can any JSON format into our Snippet class. In other words, the queues will receive a snippet as JSON object. +Also we are creating the Exchange and the Queues, and after the application is about to start we Bind the exchange with all the queues by specifying the Routing Keys.

    +
    +
  24. +
  25. +

    Now you can run the code and take a look at the RabbitMQ Web Console by going to http://localhost:15672 and see the exchange and queues created and the consumers listening to the queues.

    +
  26. +
+
+
+ + + + + +
+
Tip
+
+The username/password for the RabbitMQ console is: guest/guest. +
+
+
+
+
+

Part 2: Snippet AMQP Client

+
+
+

The idea of this application is be able to send snippets to be saved or delete, and receive a notification.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Snippet AMQP Client Project metadata with (See Figure 2.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 2. Snippet AMQP Client - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    snippet-amqp-client

    Name:

    snippet-amqp-client

    Package Name:

    io.pivotal.workshop.snippet.client

    Dependencies:

    DevTools, AMQP

    +
    +
    Figure 2.0: Spring Initializr - http://start.spring.io/
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Web, DevTools, Groovy Templates, JPA, Rest Repositories, H2, MySQL, Actuator, HATEOAS and AMQP in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Lets start by creating the domains will use, create the following classes in the io.pivotal.workshop.snippet.client.domain package:

    +
    +
    io.pivotal.workshop.snippet.client.domain.Code.java
    +
    +
    package io.pivotal.workshop.snippet.client.domain;
    +
    +public class Code {
    +
    +        private String source;
    +
    +        public Code() {
    +        }
    +
    +        public Code(String source) {
    +                this.source = source;
    +        }
    +
    +        public String getSource() {
    +                return source;
    +        }
    +
    +        public void setSource(String source) {
    +                this.source = source;
    +        }
    +
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.client.domain.Language.java
    +
    +
    package io.pivotal.workshop.snippet.client.domain;
    +
    +public class Language {
    +
    +        private String name;
    +        private String syntax = "text";
    +
    +        public Language() {
    +        }
    +
    +        public Language(String name) {
    +                this();
    +                this.name = name;
    +        }
    +
    +        public Language(String name, String syntax) {
    +                this(name);
    +                this.syntax = syntax;
    +        }
    +
    +        public String getName() {
    +                return name;
    +        }
    +
    +        public void setName(String name) {
    +                this.name = name;
    +        }
    +
    +        public String getSyntax() {
    +                return syntax;
    +        }
    +
    +        public void setSyntax(String syntax) {
    +                this.syntax = syntax;
    +        }
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.client.domain.Snippet.java
    +
    +
    package io.pivotal.workshop.snippet.client.domain;
    +
    +public class Snippet {
    +
    +        private String title;
    +        private String keywords = "";
    +        private Language lang;
    +        private Code code;
    +
    +        public Snippet() {
    +        }
    +
    +        public Snippet(String title, String keywords, String description, Language lang, Code code) {
    +                this.title = title;
    +                this.keywords = keywords;
    +                this.lang = lang;
    +                this.code = code;
    +        }
    +
    +        public Snippet(String title, Language lang, Code code) {
    +                this(title, "", "", lang, code);
    +        }
    +
    +        public String getTitle() {
    +                return title;
    +        }
    +
    +        public void setTitle(String title) {
    +                this.title = title;
    +        }
    +
    +        public Language getLang() {
    +                return lang;
    +        }
    +
    +        public void setLang(Language lang) {
    +                this.lang = lang;
    +        }
    +
    +        public String getKeywords() {
    +                return keywords;
    +        }
    +
    +        public void setKeywords(String keywords) {
    +                this.keywords = keywords;
    +        }
    +
    +        public Code getCode() {
    +                return code;
    +        }
    +
    +        public void setCode(Code code) {
    +                this.code = code;
    +        }
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.client.domain.SnippetError.java
    +
    +
    package io.pivotal.workshop.snippet.client.domain;
    +
    +public class SnippetError {
    +
    +        private String message;
    +        private String errorCode;
    +
    +        public SnippetError() {
    +        }
    +
    +        public SnippetError(String message, String errorCode) {
    +                this.message = message;
    +                this.errorCode = errorCode;
    +        }
    +
    +        public String getMessage() {
    +                return message;
    +        }
    +
    +        public void setMessage(String message) {
    +                this.message = message;
    +        }
    +
    +        public String getErrorCode() {
    +                return errorCode;
    +        }
    +
    +        public void setErrorCode(String errorCode) {
    +                this.errorCode = errorCode;
    +        }
    +}
    +
    +
    +
    +
    io.pivotal.workshop.snippet.client.domain.SnippetNotification.java
    +
    +
    package io.pivotal.workshop.snippet.client.domain;
    +
    +import java.util.Date;
    +
    +public class SnippetNotification {
    +
    +        private String snipetId;
    +        private String action;
    +        private Date processed;
    +        private SnippetError error;
    +
    +        public SnippetNotification() {
    +        }
    +
    +        public SnippetNotification(String snipetId, String action, Date processed, SnippetError error) {
    +                super();
    +                this.snipetId = snipetId;
    +                this.action = action;
    +                this.processed = processed;
    +                this.error = error;
    +        }
    +
    +        public String getSnipetId() {
    +                return snipetId;
    +        }
    +
    +        public void setSnipetId(String snipetId) {
    +                this.snipetId = snipetId;
    +        }
    +
    +        public String getAction() {
    +                return action;
    +        }
    +
    +        public void setAction(String action) {
    +                this.action = action;
    +        }
    +
    +        public Date getProcessed() {
    +                return processed;
    +        }
    +
    +        public void setProcessed(Date processed) {
    +                this.processed = processed;
    +        }
    +
    +        public SnippetError getError() {
    +                return error;
    +        }
    +
    +        public void setError(SnippetError error) {
    +                this.error = error;
    +        }
    +
    +        @Override
    +        public String toString() {
    +                return "SnippetNotification [snipetId=" + snipetId + ", action=" + action + ", processed=" + processed
    +                                + ", error=" + error + "]";
    +        }
    +
    +}
    +
    +
    +
  16. +
  17. +

    Next, lets create a producer that will send the messages to the exchange, create the SnippetProducer class in the io.pivotal.workshop.snippet.client.amqp package:

    +
    +
    io.pivotal.workshop.snippet.client.amqp.SnippetProducer.java
    +
    +
    package io.pivotal.workshop.snippet.client.amqp;
    +
    +import org.springframework.amqp.rabbit.core.RabbitTemplate;
    +import org.springframework.stereotype.Component;
    +
    +import io.pivotal.workshop.snippet.client.domain.Snippet;
    +
    +@Component
    +public class SnippetProducer {
    +
    +        public enum SnippetAction {
    +                SAVE("snippet.save"), DELETE("snippet.delete"), UPDATE("snippet.update");
    +
    +                private final String routingKey;
    +                private SnippetAction(String routingKey){
    +                        this.routingKey = routingKey;
    +                }
    +
    +                public String getRoutingKey(){
    +                        return this.routingKey;
    +                }
    +        }
    +
    +        private RabbitTemplate template;
    +
    +        public SnippetProducer(RabbitTemplate template){
    +                this.template = template;
    +        }
    +
    +        public Object send(SnippetAction action,Snippet snippet){
    +                return this.template.convertSendAndReceive(action.getRoutingKey(),snippet);
    +        }
    +
    +}
    +
    +
    +
    +

    Here we are using the RabbitTemplate and it’s method convertSendAndReceive (here we are doing the RPC - emulating the synchronous communication).

    +
    +
  18. +
  19. +

    Because we are doing an RPC, we are getting back from the Code Snippet Manager AMQP app the SnippetNotification (as JSON), so we need to configure the RabbitTemplate to support this conversion. +Create the SnippetClientAmqpConfig in the io.pivotal.workshop.snippet.client.config package:

    +
    +
    io.pivotal.workshop.snippet.client.config.SnippetClientAmqpConfig.java
    +
    +
    package io.pivotal.workshop.snippet.client.config;
    +
    +import org.springframework.amqp.core.MessageProperties;
    +import org.springframework.amqp.rabbit.connection.ConnectionFactory;
    +import org.springframework.amqp.rabbit.core.RabbitTemplate;
    +import org.springframework.amqp.support.converter.DefaultJackson2JavaTypeMapper;
    +import org.springframework.amqp.support.converter.Jackson2JavaTypeMapper;
    +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +
    +import com.fasterxml.jackson.databind.JavaType;
    +
    +@Configuration
    +public class SnippetClientAmqpConfig {
    +
    +        private final String SNIPPET_EXCHANGE = "snippet.manager";
    +
    +        @Bean
    +        public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
    +                Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
    +                Jackson2JavaTypeMapper mapper = new DefaultJackson2JavaTypeMapper() {
    +
    +                        @Override
    +                        public JavaType toJavaType(MessageProperties properties) {
    +                                properties.setHeader("__TypeId__", "io.pivotal.workshop.snippet.client.domain.SnippetNotification");
    +                                return super.toJavaType(properties);
    +                        }
    +
    +                };
    +                converter.setJavaTypeMapper(mapper);
    +
    +                RabbitTemplate template = new RabbitTemplate(connectionFactory);
    +                template.setMessageConverter(converter);
    +                template.setExchange(SNIPPET_EXCHANGE);
    +                return template;
    +        }
    +
    +}
    +
    +
    +
    +

    Its important to notice that we are using the same converter as before, the Jackson2JsonMessageConverter, that practically will map the JSON object to the SnippetNotification. +Here we are adding a little trick, a special header: TypeId that will be use as reference for the mapping, our own SnippetNotification.

    +
    +
    + + + + + +
    +
    Tip
    +
    +If you want to know more about this solution, ask the instructor to explain a little further. Also you can take a look at the documentation: http://docs.spring.io/spring-amqp/reference/html/_reference.html#message-converters +
    +
    +
  20. +
  21. +

    Now its time to send some Snippet. Create the SnippetClientConfig class in the io.pivotal.workshop.snippet.client.config package:

    +
    +
    io.pivotal.workshop.snippet.client.config.SnippetClientConfig.java
    +
    +
    package io.pivotal.workshop.snippet.client.config;
    +
    +import java.nio.file.Files;
    +import java.nio.file.Paths;
    +
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +import org.springframework.boot.CommandLineRunner;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +
    +import io.pivotal.workshop.snippet.client.amqp.SnippetProducer;
    +import io.pivotal.workshop.snippet.client.amqp.SnippetProducer.SnippetAction;
    +import io.pivotal.workshop.snippet.client.domain.Code;
    +import io.pivotal.workshop.snippet.client.domain.Language;
    +import io.pivotal.workshop.snippet.client.domain.Snippet;
    +
    +@Configuration
    +public class SnippetClientConfig {
    +
    +        private final Logger log = LoggerFactory.getLogger("SNIPPET-SENDER");
    +
    +        @Bean
    +        public CommandLineRunner sendSnippets(SnippetProducer producer){
    +                return args -> {
    +                        Object obj = producer.send(SnippetAction.SAVE, new Snippet("Hello World", new Language("Kotlin", "java"),new Code(new String(Files.readAllBytes(Paths.get("code/Hello.kt"))))));
    +                        log.info(obj.toString());
    +                };
    +        }
    +}
    +
    +
    +
    +

    Here we are using the Snippet producer to send the Snippet message.

    +
    +
  22. +
  23. +

    Now you can run the application, and you should get your response back, a SnippetNotification object.

    +
  24. +
+
+
+
+
+

Challenges

+
+
+

As you already know we havent implemented some of the requirements:

+
+
+
    +
  • +

    Create the snippet.notifications queue and bind it to the snippet.manager exchange.

    +
  • +
  • +

    The Code Snippet Manager AMQP app needs to send a notification every time it receives a Snippet:

    +
    +
      +
    • +

      Without modifying the SnippetAmqpListener add the behavior to send a SnippetNotification to the snippet.notifications queue.

      +
      + + + + + +
      +
      Tip
      +
      +Use AOP for this particular concern and use the RabbitTemplate to send a asynchronous notification. +
      +
      +
    • +
    +
    +
  • +
  • +

    Make the Snippet AMQP Client app listen for any notification.

    +
  • +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/11-spring-boot-webflux.html b/src/main/UploadedContent/student-lab-instructions/11-spring-boot-webflux.html new file mode 100644 index 0000000..db25f7b --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/11-spring-boot-webflux.html @@ -0,0 +1,1223 @@ + + + + + + + +Spring Boot WebFlux + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with Spring Boot and WebFlux features. +Time: 20 minutes.

+
+ +
+
+
+

Code Snippet Manager App

+
+
+

We are going to make our Code Snippet Manager reactive. You will use the spring-boot-starter-webflux starter. Remember that Spring WebFlux provides two ways to create a reactive apps: Functional and Annotation-Based; in this lab will explore both.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager App Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Code Snippet Manager App App - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-flux

    Name:

    code-snippet-manager-flux

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Reactive Web, DevTools, MongoDB, Actuator

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Reactive Web, DevTools, MongoDB, Reactive MongoDB and Actuator in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Add the following domain classes: io.pivotal.workshop.snippet.domain.Language and io.pivotal.workshop.snippet.domain.Snippet, practically are the same as before, just a small modifications:

    +
    +
    src/main/java/io/pivotal/workshop/snippet/domain/Language.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +import org.bson.types.ObjectId;
    +import org.springframework.data.annotation.Id;
    +import org.springframework.data.mongodb.core.mapping.Document;
    +
    +@Document
    +public class Language {
    +
    +        @Id
    +        private String id;
    +        private String name;
    +        private String syntax = "text";
    +
    +        public Language() {
    +        }
    +
    +        public Language(String name) {
    +                this();
    +                this.name = name;
    +        }
    +
    +        public Language(String name, String syntax) {
    +                this(name);
    +                this.syntax = syntax;
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getName() {
    +                return name;
    +        }
    +
    +        public void setName(String name) {
    +                this.name = name;
    +        }
    +
    +        public String getSyntax() {
    +                return syntax;
    +        }
    +
    +        public void setSyntax(String syntax) {
    +                this.syntax = syntax;
    +        }
    +
    +    @Override
    +    public String toString() {
    +        return "Language{" +
    +                "id='" + id + '\'' +
    +                ", name='" + name + '\'' +
    +                ", syntax='" + syntax + '\'' +
    +                '}';
    +    }
    +}
    +
    +
    +
    +
    src/main/java/io/pivotal/workshop/snippet/domain/Snippet.java
    +
    +
    package io.pivotal.workshop.snippet.domain;
    +
    +import org.bson.types.ObjectId;
    +import org.springframework.data.annotation.Id;
    +import org.springframework.data.mongodb.core.mapping.DBRef;
    +import org.springframework.data.mongodb.core.mapping.Document;
    +
    +import java.util.Date;
    +
    +@Document
    +public class Snippet {
    +
    +        @Id
    +        private String id;
    +        private String title;
    +        private String keywords = "";
    +        private String description = "";
    +
    +    @DBRef
    +        private Language lang;
    +    private String code;
    +
    +    private Date created;
    +        private Date modified;
    +
    +        public Snippet() {
    +                this.created = new Date();
    +                this.modified = new Date();
    +        }
    +
    +        public Snippet(String title, String keywords, String description, Language lang, String code) {
    +                this();
    +                this.title = title;
    +                this.keywords = keywords;
    +                this.description = description;
    +                this.lang = lang;
    +                this.code = code;
    +        }
    +
    +        public Snippet(String title, Language lang, String code) {
    +                this(title, "", "", lang, code);
    +        }
    +
    +        public String getId() {
    +                return id;
    +        }
    +
    +        public void setId(String id) {
    +                this.id = id;
    +        }
    +
    +        public String getTitle() {
    +                return title;
    +        }
    +
    +        public void setTitle(String title) {
    +                this.title = title;
    +        }
    +
    +        public Language getLang() {
    +                return lang;
    +        }
    +
    +        public void setLang(Language lang) {
    +                this.lang = lang;
    +        }
    +
    +        public String getDescription() {
    +                return description;
    +        }
    +
    +        public void setDescription(String description) {
    +                this.description = description;
    +        }
    +
    +        public String getKeywords() {
    +                return keywords;
    +        }
    +
    +        public void setKeywords(String keywords) {
    +                this.keywords = keywords;
    +        }
    +
    +        public String getCode() {
    +                return code;
    +        }
    +
    +        public void setCode(String code) {
    +                this.code = code;
    +        }
    +
    +        public Date getCreated() {
    +                return created;
    +        }
    +
    +        public void setCreated(Date created) {
    +                this.created = created;
    +        }
    +
    +        public Date getModified() {
    +                return modified;
    +        }
    +
    +        public void setModified(Date modified) {
    +                this.modified = modified;
    +        }
    +
    +    @Override
    +    public String toString() {
    +        return "Snippet{" +
    +                "id='" + id + '\'' +
    +                ", title='" + title + '\'' +
    +                ", keywords='" + keywords + '\'' +
    +                ", description='" + description + '\'' +
    +                ", lang=" + lang +
    +                ", code='" + code + '\'' +
    +                ", created=" + created +
    +                ", modified=" + modified +
    +                '}';
    +    }
    +}
    +
    +
    +
  16. +
  17. +

    Add the Repositories. Create/Copy the io.pivotal.workshop.snippet.repository.LanguageRespository.java and io.pivotal.workshop.snippet.repository.SnippetRespository.java interfaces.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/repository/LanguageRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import io.pivotal.workshop.snippet.domain.Language;
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import org.springframework.data.mongodb.repository.MongoRepository;
    +import reactor.core.publisher.Mono;
    +
    +public interface LanguageRepository extends MongoRepository<Language, String> {
    +
    +    Language findByName(String name);
    +
    +}
    +
    +
    +
    +
    src/main/java/io/pivotal/workshop/snippet/repository/SnippetRepository.java
    +
    +
    package io.pivotal.workshop.snippet.repository;
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import org.springframework.data.mongodb.repository.MongoRepository;
    +import org.springframework.data.mongodb.repository.Query;
    +
    +import java.util.concurrent.CompletableFuture;
    +import java.util.stream.Stream;
    +
    +public interface SnippetRepository extends MongoRepository<Snippet, String> {
    +
    +    @Query("{}")
    +    Stream<Snippet> findAllAsStream();
    +}
    +
    +
    +
    +

    As you can see in the SnippetRepository interface declares a findAllAsStream method that returns all the snippets from MongoDB.

    +
    +
  18. +
  19. +

    Next, lets create the Reative Server and the Functional Router for the app. Create the io.pivotal.workshop.snippet.config.SnippetReactive class:

    +
    +
    src/main/java/io/pivotal/workshop/snippet/config/SnippetReactiveConfig.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import io.pivotal.workshop.snippet.reactive.SnippetHandler;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
    +import org.springframework.web.reactive.function.server.RouterFunction;
    +import org.springframework.web.reactive.function.server.RouterFunctions;
    +import org.springframework.web.reactive.function.server.ServerResponse;
    +import reactor.core.publisher.EmitterProcessor;
    +import reactor.ipc.netty.http.server.HttpServer;
    +
    +import static org.springframework.http.MediaType.APPLICATION_JSON;
    +import static org.springframework.web.reactive.function.server.RequestPredicates.*;
    +
    +
    +@Configuration
    +public class SnippetReactiveConfig {
    +
    +
    +    @Bean
    +    public HttpServer httpServer(RouterFunction<ServerResponse> router){        //(1)
    +        HttpServer server = HttpServer.create("localhost", 8080);
    +        server.newHandler(new ReactorHttpHandlerAdapter(RouterFunctions.toHttpHandler(router))).block();
    +        return server;
    +    }
    +
    +    @Bean
    +    public RouterFunction<ServerResponse> router(SnippetHandler handler){       //(2)
    +        return RouterFunctions
    +                .route(GET("/snippets").and(accept(APPLICATION_JSON)), handler::findAll)
    +                .andRoute(GET("/snippets/{id}").and(accept(APPLICATION_JSON)),handler::findById)
    +                .andRoute(POST("/snippets").and(accept(APPLICATION_JSON)),handler::createSnippet);
    +    }
    +
    +    @Bean
    +    public EmitterProcessor snippetStream(){                                   //(3)
    +        return EmitterProcessor.<Snippet>create();
    +    }
    +}
    +
    +
    +
    +
      +
    1. +

      This method returns a HttpServer bean class that belongs to the Netty package, meaning that the app will run in a Reactive Server. This method is receiving a router function.

      +
    2. +
    3. +

      This method returns a RouterFunction bean as a ServerResponse type. It defines what are the endpoints and how they will be handled. In this case we are using the SnippetHandler class that will have all the logic for handling all incoming requests.

      +
    4. +
    5. +

      This method returns a EmitterProcessor bean that will be use to stream all the new Snippets created.

      +
    6. +
    +
    +
  20. +
  21. +

    Let’s continue with the SnippetHandler. Create the io.pivotal.workshop.snippet.reactive.SnippetHandler class.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/reactive/SnippetHandler.java
    +
    +
    package io.pivotal.workshop.snippet.reactive;
    +
    +import io.pivotal.workshop.snippet.domain.Language;
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import io.pivotal.workshop.snippet.repository.LanguageRepository;
    +import io.pivotal.workshop.snippet.repository.SnippetRepository;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.stereotype.Component;
    +import org.springframework.web.reactive.function.BodyInserters;
    +import org.springframework.web.reactive.function.server.ServerRequest;
    +import org.springframework.web.reactive.function.server.ServerResponse;
    +import reactor.core.publisher.EmitterProcessor;
    +import reactor.core.publisher.Flux;
    +import reactor.core.publisher.Mono;
    +
    +import java.util.Optional;
    +
    +import static org.springframework.web.reactive.function.server.ServerResponse.notFound;
    +import static org.springframework.web.reactive.function.server.ServerResponse.ok;
    +
    +@Component
    +public class SnippetHandler {
    +
    +    private EmitterProcessor<Snippet> snippetEmitterProcessor;
    +    private SnippetRepository snippetRepository;
    +    private LanguageRepository languageRepository;
    +
    +    @Autowired  //(1)
    +    public SnippetHandler(SnippetRepository snippetRepository, LanguageRepository languageRepository, EmitterProcessor<Snippet> snippetEmitterProcessor) {
    +        this.snippetRepository = snippetRepository;
    +        this.languageRepository = languageRepository;
    +        this.snippetEmitterProcessor = snippetEmitterProcessor;
    +    }
    +
    +    public Mono<ServerResponse> findAll(ServerRequest request){     //(2)
    +        return ok().body(BodyInserters.fromPublisher(Flux.fromStream(this.snippetRepository.findAllAsStream()),Snippet.class));
    +    }
    +
    +    public Mono<ServerResponse> findById(ServerRequest request) {
    +        String snippetId = request.pathVariable("id");
    +        Optional<Snippet> result = this.snippetRepository.findById(snippetId);
    +        if(result.isPresent())
    +            return ok().body(BodyInserters.fromPublisher(Mono.just(result.get()),Snippet.class));
    +        else
    +            return notFound().build();
    +    }
    +
    +    public Mono<ServerResponse> createSnippet(ServerRequest request) {
    +        Mono<Snippet> snippetMono = request.bodyToMono(Snippet.class);
    +        return ok().build(snippetMono.doOnNext(snippetConsumer -> {
    +            Language language = new Language("Unknown","txt");
    +            if(snippetConsumer.getLang()!=null){
    +                language = this.languageRepository.findByName(snippetConsumer.getLang().getName());
    +                if(language == null) {
    +                    language = this.languageRepository.save(snippetConsumer.getLang());
    +                }
    +            }else
    +                language = this.languageRepository.save(language);
    +
    +            snippetConsumer.setLang(language);
    +            Snippet result = this.snippetRepository.save(snippetConsumer);
    +            this.snippetEmitterProcessor.onNext(result);   //(3)
    +        }).then());
    +    }
    +
    +
    +}
    +
    +
    +
    +
      +
    1. +

      The constructor will autowired all the repositories and the EmitterProcessor.

      +
    2. +
    3. +

      Take a look that all the methods receive a ServerRequest and the response is a Mono<ServerResponse> type.

      +
    4. +
    5. +

      The createSnippet method will handle all the POST requests (/snippets) and create the necessary Language and Snippet, but the most important part here is that it will emit the snippet saved as stream.

      +
    6. +
    +
    +
  22. +
  23. +

    Now, let’s create a rest controller that will stream all the new snippets. Create the io.pivotal.workshop.snippet.reactive.SnippetController class.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/reactive/SnippetController.java
    +
    +
    package io.pivotal.workshop.snippet.reactive;
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import org.springframework.http.MediaType;
    +import org.springframework.web.bind.annotation.GetMapping;
    +import org.springframework.web.bind.annotation.RestController;
    +import org.springframework.web.reactive.function.BodyInserters;
    +import org.springframework.web.reactive.function.server.ServerRequest;
    +import org.springframework.web.reactive.function.server.ServerResponse;
    +import reactor.core.publisher.EmitterProcessor;
    +import reactor.core.publisher.Flux;
    +import reactor.core.publisher.Mono;
    +
    +import static org.springframework.web.reactive.function.server.ServerResponse.ok;
    +
    +@RestController
    +public class SnippetController {
    +
    +    private EmitterProcessor snippetStream;
    +
    +    public SnippetController(EmitterProcessor snippetStream) {  //(1)
    +        this.snippetStream = snippetStream;
    +    }
    +
    +    @GetMapping(path = "/logs", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    +    public Flux<Snippet> snippetLog(){                          //(2)
    +        return snippetStream.doOnSubscribe(subscription -> { snippetStream.next().subscribe(); });
    +
    +    }
    +}
    +
    +
    +
    +
      +
    1. +

      The constructor injects the EmitterProcessor.

      +
    2. +
    3. +

      This method will handle the stream publish in /logs endpoint. See that it’s subscribe to the stream.

      +
    4. +
    +
    +
  24. +
  25. +

    In the src/main/resources/application.properties file add the following properties:

    +
    +
    src/main/resources/application.properties
    +
    +
    spring.data.mongodb.database=snippets-[YOUR INITIALS]
    +spring.data.mongodb.host=[PROVIDED BY THE INSTRUCTOR]
    +spring.data.mongodb.port=27017
    +spring.data.mongodb.username=[PROVIDED BY THE INSTRUCTOR]
    +spring.data.mongodb.password=[PROVIDED BY THE INSTRUCTOR]
    +
    +
    +
    +

    REPLACE the values accordingly.

    +
    +
  26. +
  27. +

    Run the application and go to your browser and hit the http://localhost:8080/logs and browser should be waiting for incoming data.

    +
  28. +
  29. +

    Open a new browser tab or window and point to the http://localhost:8080/snippets and you will see an empty json collection.

    +
  30. +
  31. +

    Open a Terminal window and add some snippets using the cUrl command:

    +
    +
    +
    curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{"title":"Hello World","code":"println \"This is awesome\".upperCase()","lang":{"name": "Groovy", "syntax":"groovy"}}' http://localhost:8080/snippets
    +
    +
    +
    + + + + + +
    +
    Tip
    +
    +If you are a Windows user, you can use the POSTMAN (https://www.getpostman.com/) application to execute the POST. +
    +
    +
    +

    Add more snippets and see how the http://localhost:8080/logs start receiving some Snippet streams.

    +
    +
    +

    Logs

    +
    +
  32. +
  33. +

    Review the snippets by hitting again the http://localhost:8080/snippets

    +
    +

    Snippets

    +
    +
  34. +
+
+
+

Challenges

+
+
    +
  • +

    Add an Embedded MongoDB and run the same application. You must add the following dependency to your pom.xml or gradle:

    +
    +
    pom.xml
    +
    +
    <dependency>
    +        <groupId>de.flapdoodle.embed</groupId>
    +        <artifactId>de.flapdoodle.embed.mongo</artifactId>
    +        <scope>runtime</scope>
    +</dependency>
    +
    +
    +
    +

    or

    +
    +
    +
    build.gradle
    +
    +
    runtime('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
    +
    +
    +
  • +
+
+
+
+
+
+

HOMEWORK

+
+
+
    +
  • +

    Use the annotation-based WebFlux variant for the Code Snippet Manager App for all the endpoints.

    +
  • +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/12-spring-boot-cloud-stream.html b/src/main/UploadedContent/student-lab-instructions/12-spring-boot-cloud-stream.html new file mode 100644 index 0000000..63fcaf2 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/12-spring-boot-cloud-stream.html @@ -0,0 +1,1242 @@ + + + + + + + +Spring Boot - Spring Integration and Spring Cloud Stream + + + + + + + + + + + + + + + +
+
+
+
+

Get familiar with Spring Integration and Spring Cloud Stream using Spring Boot and learn how Microservices can communicate with each other. +Time: 30 minutes.

+
+
+
+
+

Requirements

+
+
+
    +
  • +

    You will need RabbitMQ up and running.

    +
  • +
+
+ +
+
+
+

Code Snippet Manager Stream App - Source

+
+
+

We are going create a Source (our Code Snippet Manager). This Source will use Spring Integration to read snippets from the File System and it will forward the snippet to a Sink application.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager Stream App Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Code Snippet Manager Stream App Source - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-stream

    Name:

    code-snippet-manager-stream

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    Reactive Web, DevTools, MongoDB, Actuator, Stream Rabbit, Embedded MongoDB, Integration

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type Reactive Web, DevTools, MongoDB, Reactive MongoDB, Actuator, Stream Rabbit, Embedded MongoDB and Integration in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy ONLY the classes from the previous Lab (WebFlux), we are going to reuse them.

    +
  16. +
  17. +

    Is necessary to do several mofications to the pom.xml or build.gradle files. First modify the spirng-cloud-starter-stream-rabbit dependency to look like this:

    +
    +
    pom.xml
    +
    +
    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>org.springframework.boot</groupId>
    +            <artifactId>spring-boot-starter-tomcat</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>
    +
    +
    +
    +

    or

    +
    +
    +
    buildf.gradle
    +
    +
    compile('org.springframework.cloud:spring-cloud-starter-stream-rabbit'){
    +    exclude module: "spring-boot-starter-tomcat"
    +}
    +
    +
    +
    +

    This dependency is part of the Spring Cloud Stream; the Binder that we are going to use: RabbitMQ. This dependency brings Tomcat, something that we don’t need because we are using Netty instead for the reactive part.

    +
    +
  18. +
  19. +

    Next, modify the pom.xml or build.gradle so the Embedded MongoDB is used at runtime.

    +
    +
    pom.xml
    +
    +
    <dependency>
    +        <groupId>de.flapdoodle.embed</groupId>
    +    <artifactId>de.flapdoodle.embed.mongo</artifactId>
    +    <scope>runtime</scope>
    +</dependency>
    +
    +
    +
    +

    or

    +
    +
    +
    build.gradle
    +
    +
    runtime('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
    +
    +
    +
  20. +
  21. +

    Because we are going to use Spring Integration to read from the File System we need to add the following dependencies:

    +
    +
    pom.xml
    +
    +
    <dependency>
    +    <groupId>org.springframework.integration</groupId>
    +    <artifactId>spring-integration-file</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.integration</groupId>
    +    <artifactId>spring-integration-java-dsl</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.yaml</groupId>
    +    <artifactId>snakeyaml</artifactId>
    +</dependency>
    +
    +
    +
    +

    or

    +
    +
    +
    build.gradle
    +
    +
    compile('org.springframework.integration:spring-integration-file')
    +compile('org.springframework.integration:spring-integration-java-dsl')
    +compile('org.yaml:snakeyaml')
    +
    +
    +
  22. +
  23. +

    This application will have its own MongoDB (embedded) running in a random port, so it’s necessary to configure the client so it can connect. Create a io.pivotal.workshop.snippet.config.SnippetConfiguration class.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/config/SnippetConfig.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +import com.mongodb.MongoClient;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.DependsOn;
    +import org.springframework.core.env.Environment;
    +
    +@Configuration
    +public class SnippetConfiguration {
    +
    +
    +    private Environment environment;
    +
    +    public SnippetConfiguration(Environment environment) {
    +        this.environment = environment;
    +    }
    +
    +
    +    @Bean
    +    @DependsOn("embeddedMongoServer")           //(1)
    +    public MongoClient reactiveMongoClient() {
    +        int port = this.environment.getProperty("local.mongo.port", Integer.class);
    +        return new MongoClient("localhost",port);
    +    }
    +
    +
    +}
    +
    +
    +
    +
      +
    1. +

      Take a look that the MongoClient that it will be created after the embeddedMongoServer bean (due the @DependsOn annotation).

      +
    2. +
    +
    +
  24. +
  25. +

    If you run your application, it should work with out any issues, the only difference is that you are using an Embedded MongoDB.

    +
  26. +
  27. +

    Create (or copy/reuse from another Lab) a io.pivotal.workshop.snippet.config.SnippetProperties class. This class will have just the path where the snippet will be read.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/config/SnippetProperties.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +
    +@ConfigurationProperties(prefix = "snippet")
    +public class SnippetProperties {
    +
    +        private String path = "/tmp";
    +        private String extension = "*.code";
    +
    +        public String getPath() {
    +                return path;
    +        }
    +
    +        public void setPath(String path) {
    +                this.path = path;
    +        }
    +
    +    public String getExtension() {
    +        return extension;
    +    }
    +
    +    public void setExtension(String extension) {
    +        this.extension = extension;
    +    }
    +}
    +
    +
    +
  28. +
  29. +

    Next lets create the Stream Source. Create the io.pivotal.workshop.snippet.stream.SnippetFileSystemIntegration class, in this class we are going to use Spring Integration with Spring Cloud Stream:

    +
    +
    src/main/java/io/pivotal/workshop/snippet/stream/SnippetFileSystemIntegration.java
    +
    +
    package io.pivotal.workshop.snippet.stream;
    +
    +import io.pivotal.workshop.snippet.config.SnippetProperties;
    +import io.pivotal.workshop.snippet.integration.SnippetTransformer;
    +import org.springframework.boot.context.properties.EnableConfigurationProperties;
    +import org.springframework.cloud.stream.annotation.EnableBinding;
    +import org.springframework.cloud.stream.messaging.Source;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.integration.dsl.IntegrationFlow;
    +import org.springframework.integration.dsl.IntegrationFlows;
    +import org.springframework.integration.dsl.Pollers;
    +import org.springframework.integration.dsl.Transformers;
    +import org.springframework.integration.file.dsl.Files;
    +import org.springframework.integration.file.transformer.FileToStringTransformer;
    +
    +import java.io.File;
    +
    +@EnableConfigurationProperties(SnippetProperties.class)
    +@EnableBinding(Source.class)
    +public class SnippetFileSystemIntegration {
    +
    +    private SnippetProperties snippetProperties;
    +    private SnippetTransformer snippetTransformer;
    +
    +    public SnippetFileSystemIntegration(SnippetProperties snippetProperties, SnippetTransformer snippetTransformer) {  //(1)
    +        this.snippetProperties = snippetProperties;
    +        this.snippetTransformer = snippetTransformer;
    +    }
    +
    +    @Bean
    +    public IntegrationFlow snippetFlow(){   //(2)
    +        return
    +                IntegrationFlows.from(
    +                        Files
    +                            .inboundAdapter(new File(this.snippetProperties.getPath()))   //(3)
    +                            .preventDuplicates(true)
    +                            .patternFilter(this.snippetProperties.getExtension()),
    +                        e -> e.poller(Pollers.fixedDelay(5000L)))                  //(4)
    +                .transform(new FileToStringTransformer())                                 //(5)
    +                .transform(Transformers.converter(this.snippetTransformer))               //(6)
    +                .channel(Source.OUTPUT)                                                   //(7)
    +                .get();
    +
    +    }
    +
    +}
    +
    +
    +
    +
      +
    1. +

      The constructor is using the SnippetProperties and the SnippetTransformer (that we are going create later) instances.

      +
    2. +
    3. +

      We are declaring the IntegrationFlow (this is part of Spring Integration) that will read files from the directory (from the snippet.path property).

      +
    4. +
    5. +

      Spring Integration provides an Inbound Adapter that will read the files with the extension provided (from the snippet.extension property).

      +
    6. +
    7. +

      The Inbound Adapter will polling from file very 5 seconds (Poller).

      +
    8. +
    9. +

      We are going to use a transform that will read the file and convert it into a String. The class FileToStringTransformer is part of the Spring Integration out-of-the-box utility classes.

      +
    10. +
    11. +

      This transformer uses a custom transformer (SnippetTransformer), the previous transformer will send the text in the file into this new transformer.

      +
    12. +
    13. +

      This will be the final step, where to send the transformed message (a Snippet object), in this case it will use a channel named: OUTPUT.

      +
    14. +
    +
    +
  30. +
  31. +

    Create the io.pivotal.workshop.snippet.integration.SnippetTransformer class, this class will transform the String into an Snippet object.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/integration/SnippetTransformer.java
    +
    +
    package io.pivotal.workshop.snippet.integration;
    +
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import org.springframework.core.convert.converter.Converter;
    +import org.springframework.lang.Nullable;
    +import org.springframework.stereotype.Component;
    +import org.yaml.snakeyaml.Yaml;
    +
    +@Component
    +public class SnippetTransformer implements Converter<String,Snippet> { //(1)
    +
    +    @Nullable
    +    @Override
    +    public Snippet convert(String snippet) {
    +        return new Yaml().loadAs(snippet,Snippet.class);                //(2)
    +    }
    +}
    +
    +
    +
    +
      +
    1. +

      Here we need to be compliant witht the Converter interface. This a generic interface, its first type is what it receives (the String, the file’s content) and the second, the object converted to (the Snippet object).

      +
    2. +
    3. +

      We are using the Yaml object (meaning that our file will have a YAML format) to convert it to Snippet.

      +
    4. +
    +
    +
  32. +
  33. +

    Next, open the src/main/resources/application.properties file a modify it to look like this:

    +
    +
    src/main/resources/application.properties
    +
    +
    # Snippet
    +snippet.path=/tmp/snippets
    +snippet.extension=*.code
    +
    +spring.cloud.stream.bindings.output.destination=snippet
    +
    +
    +
    +

    The spring.cloud.stream.bindings.output.destination is the property that specifies the output channel where the message will be sent (in this case the Snippet object)

    +
    +
  34. +
  35. +

    Now we are ready, but before running or testing it, we are going to create another application that will receive the Snippet object and just print it out into the console.

    +
  36. +
+
+
+
+
+

Code Snippet Manager Stream Log App - Sink

+
+
+

We are going create now a Sink. This Sink will just print out the Snippet object that was sent by the previous app.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Code Snippet Manager Log App Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 2. Code Snippet Manager Stream Log App Sink - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    code-snippet-manager-stream-log

    Name:

    code-snippet-manager-stream-log

    Package Name:

    io.pivotal.workshop.snippet

    Dependencies:

    DevTools, Actuator, Stream Rabbit, Integration

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
    + + + + + +
    +
    Tip
    +
    +You can choose either Maven or Gradle project types. +
    +
    +
  6. +
  7. +

    Type DevTools, Actuator, Stream Rabbit and Integration in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    Unzip the file in any directory you want.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Copy only the Language and Snippet classes over. REMOVE the @Document, @Id and @DebRef annotations, we are not going to use them.

    +
  16. +
  17. +

    Create the io.pivotal.workshop.snippet.stream.SnippetLogger class.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/stream/SnippetLogger.java
    +
    +
    package io.pivotal.workshop.snippet.stream;
    +
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +import org.springframework.cloud.stream.annotation.EnableBinding;
    +import org.springframework.cloud.stream.annotation.StreamListener;
    +import org.springframework.cloud.stream.messaging.Sink;
    +
    +@EnableBinding(Sink.class)
    +public class SnippetLogger {
    +
    +    private Logger log = LoggerFactory.getLogger("[SNIPPET LOGGER]");
    +
    +    @StreamListener(Sink.INPUT) //(1)
    +    public void log(Snippet snippet){
    +        log.info(snippet.toString());
    +    }
    +}
    +
    +
    +
    +
      +
    1. +

      Here the importat part is the use of the @StreamListener(Sink.INPUT) annotation that will listen for any incoming Snippet message through the INPUT channel. Then it will be just logged.

      +
    2. +
    +
    +
  18. +
  19. +

    Open the src/main/resources/application.properties file and modify it to look like this:

    +
    +
    src/main/resources/application.properties
    +
    +
    server.port=8181
    +spring.cloud.stream.bindings.input.destination=snippet
    +
    +
    +
    +

    See that here we are going to use the port 8181 and the spring.cloud.stream.bindings.input.destination property that will be listen to (in this case the input channel: snippet).

    +
    +
  20. +
  21. +

    Now is time to test these microservices!

    +
  22. +
+
+
+
+
+

Testing

+
+
+

This part of the lab involves testing directly into these two microservices.

+
+
+
    +
  1. +

    Make sure you have RabbitMQ up and running.

    +
  2. +
  3. +

    Make sure you have the /tmp/snippets (or C:\tmp\snippets) directory with write permissions.

    +
  4. +
  5. +

    Start your code-snippet-manager-stream-log application.

    +
  6. +
  7. +

    Start your code-snippet-manager-stream application.

    +
  8. +
  9. +

    Create some code snippets files OUTSIDE the /tmp/snippets (or C:\tmp\snippets) directory first. These files will have a YAML format and they will contain a particular structure:

    +
    +
    hello-world-erl.code
    +
    +
    title: Hello World
    +description: A Erlang Hello World
    +lang: {
    +  name: Erlang,
    +  syntax: erl
    +  }
    +code: |
    +  -module(hello).
    +  -export([hello_world/0]).
    +
    +  hello_world() -> io:fwrite("hello, world\n").
    +
    +
    +
    +
    hello-world-javascript.code
    +
    +
    title: Hello World
    +description: A JavaScript Hello World
    +lang: {
    +  name: JavaScript,
    +  syntax: js
    +  }
    +code: |
    +    console.log("Hello World");
    +
    +
    +
  10. +
  11. +

    Copy these files into the /tmp/snippets (or C:\tmp\snippets) directory.

    +
  12. +
  13. +

    See how the code-snippet-manager-stream-log application print the receive Snippet objects.

    +
  14. +
+
+
+

Challenges

+
+
    +
  • +

    In the SnippetTransformer add a logic to save the Snippet into the Embedded MongoDB so it can be viewed over the http://localhost:8080/logs and the http://localhost:8080/snippets:

    +
    +
      +
    • +

      Create a io.pivotal.workshop.snippet.service.SnippetService class. This class will be a facade for all repository operations so it can be reused by the SnippetHandler and the SnippetTransformer classes.

      +
    • +
    +
    +
  • +
+
+
+
+
+
+

HOMEWORK

+
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/13-spring-boot-microservices.html b/src/main/UploadedContent/student-lab-instructions/13-spring-boot-microservices.html new file mode 100644 index 0000000..709a6b1 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/13-spring-boot-microservices.html @@ -0,0 +1,1101 @@ + + + + + + + +Microservices with Spring Boot + + + + + + + + + + + + + + + +
+
+
+
+

Create Microservices with Spring Boot and deploy them into Cloud Foundry

+
+
+

Time: 35 minutes.

+
+ +
+
+
+

Cloud Foundry

+
+
+

This lab is just to get familiar with some of the common Cloud Foundry command using the CF CLI.

+
+
+
    +
  1. +

    Make sure you have an account in PWS if not, create a trial account in Pivotal Web Services: https://run.pivotal.io

    +
  2. +
  3. +

    Install Cloud Foundry CLI. You can get it from: https://github.com/cloudfoundry/cli#installers-and-compressed-binaries

    +
  4. +
  5. +

    Login into Cloud Foundry

    +
    +
    cf login
    +
    +
    $ cf login -a api.run.pivotal.io
    +
    +
    +
  6. +
+
+
+

Useful CF CLI commands

+
+
cf help - Show help
+
+
$ cf help -a
+
+
+
+
marketplace - List available offerings in the marketplace
+
+
$ cf marketplace
+
+
+
+
services - List all service instances in the target space
+
+
$ cf services
+
+
+
+
create-service - Create a service instance
+
+
$ cf create-service cloudamqp lemur rabbitmq
+
+
+
+
bind-service - Bind a service instance to an app
+
+
$ cf bind-service myapp rabbitmq
+
+
+
+
push - Push a new app or sync changes to an existing app
+
+
$ cf push myapp
+
+
+
+
set-env - Set an env variable for an app
+
+
$ cf set-env myapp MY_VARIABLE ITS_VALUE
+
+
+
+
+
+
+

Deploy Spring Boot Microservices to Cloud Foundry

+
+
+

For this lab you will deploy two projects, the directory-web-security and the code-snippet-manager-security, both must be completed with OAuth2 implementation. +Remember that the directory-web-security is the Authorization Server and the code-snippet-manager-security is the Resource Server. +If you haven’t finish the challenges for both projects, the it’s time to do it.

+
+
+

Deploying directory-web-security project

+
+
    +
  1. +

    Open the direcoty-web-security app in your favorite IDE. Make sure your io.pivotal.workshop.directory.config.DirectorySecurityConfig class in the configure(HttpSecurity http) is based in the Http Basic Security (httpBasic()). +This will allow our code-snippet-manager-security app to connect passing credentials using a basic authentication,

    +
  2. +
  3. +

    Open a terminal window in the directory-web-security project and create the JAR file.

    +
    +
    maven
    +
    +
    ./mvnw clean package -DskipTests=true
    +
    +
    +
    +
    gradle
    +
    +
    ./gradlew build -x test
    +
    +
    +
  4. +
  5. +

    Deploy the directory-web-security JAR.

    +
    +
    If you used Maven, it creates the JAR in the target/ folder. If you used Gradle, the JAR is in the build/libs folder.
    +
    +
    cf push directory-web -p target/directory-web-security-0.0.1-SNAPSHOT.jar --random-route -b java_buildpack
    +
    +
    +
  6. +
  7. +

    Once deployed make sure is working by accessing the /api endpoint using the administrator credentials.

    +
  8. +
+
+
+
+

Deploying code-snippet-manager-security project

+
+

You will be using the code-snippet-manager-security project.

+
+
+

Remember that you had a Challenge for this project? Modify the code-snippet-manager-security project and use the directory-web-security project as authentication authority.

+
+
+

Well, this is the solution to that Challenge!!

+
+
+
    +
  1. +

    Open your code-snippet-manager-security project and open the io.pivotal.workshop.snippet.config.SnippetConfiguration class. Modify it to look like the following code:

    +
    +
    src/main/java/io/pivotal/workshop/snippet/config/SnippetConfiguration.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +import io.pivotal.workshop.snippet.domain.Code;
    +import io.pivotal.workshop.snippet.domain.Language;
    +import io.pivotal.workshop.snippet.domain.Snippet;
    +import io.pivotal.workshop.snippet.repository.SnippetRepository;
    +import org.springframework.boot.CommandLineRunner;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.Profile;
    +
    +import java.nio.file.Files;
    +import java.nio.file.Paths;
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +@Configuration
    +public class SnippetConfiguration {
    +
    +
    +        @Bean
    +        @Profile("local")
    +        public CommandLineRunner runner(SnippetRepository snippetRepo) {
    +                return args -> {
    +                        @SuppressWarnings("serial")
    +                        List<Snippet> snippets = new ArrayList<Snippet>() {
    +                                {
    +                                        add(new Snippet("Hello World", new Language("HTML", "xml"),new Code(new String(Files.readAllBytes(Paths.get("code/html-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("C#", "c#"),new Code(new String(Files.readAllBytes(Paths.get("code/cs-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("Pascal", "py"),new Code(new String(Files.readAllBytes(Paths.get("code/pas-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("Erlang", "erl"),new Code(new String(Files.readAllBytes(Paths.get("code/erl-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("JavaScript", "js"),new Code(new String(Files.readAllBytes(Paths.get("code/js-code.txt"))))));
    +                                        add(new Snippet("Hello World", new Language("Groovy", "groovy"),new Code("println 'Hello World'")));
    +                                }
    +                        };
    +
    +                        snippetRepo.saveAll(snippets);
    +
    +                };
    +        }
    +
    +        @Bean
    +        @Profile("cloud") //(1)
    +        public CommandLineRunner runnerCloud(SnippetRepository snippetRepo) {
    +                return args -> {
    +                        @SuppressWarnings("serial")
    +                        List<Snippet> snippets = new ArrayList<Snippet>() {{
    +                                add(new Snippet("Hello World", new Language("JavaScript", "js"),new Code("console.log(\"Hello World\");")));
    +                                add(new Snippet("Hello World", new Language("Groovy", "groovy"),new Code("println 'Hello World'")));
    +                        }};
    +                        snippetRepo.saveAll(snippets);
    +                };
    +        }
    +}
    +
    +
    +
    +
      +
    1. +

      As you can see we are using the @Profile annotation, and we adding it to the runner method, making it a local profile, but also we are adding a new method runnerCloud +and using the cloud profile. Why do we need to do this? well we are not deploying the code/ folder, so there are no initial snippets.

      +
    2. +
    +
    +
    + + + + + +
    +
    Tip
    +
    +By default Cloud Foundry activates the cloud profile. +
    +
    +
  2. +
  3. +

    This is part of the Challenge: Use the directory-web-security project as authentication-authority for the code-snippet-manager project. Here we are going to see the solution. Create the io.pivotal.workshop.snippet.config.SnippetSecurityConfig class, this class will reach out to the directory-web-security app.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/config/SnippetSecurityConfig.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +import io.pivotal.workshop.snippet.domain.Person;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.actuate.autoconfigure.security.EndpointRequest;
    +import org.springframework.boot.autoconfigure.security.StaticResourceRequest;
    +import org.springframework.boot.context.properties.EnableConfigurationProperties;
    +import org.springframework.boot.web.client.RestTemplateBuilder;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.core.ParameterizedTypeReference;
    +import org.springframework.hateoas.MediaTypes;
    +import org.springframework.hateoas.Resource;
    +import org.springframework.http.HttpMethod;
    +import org.springframework.http.HttpStatus;
    +import org.springframework.http.RequestEntity;
    +import org.springframework.http.ResponseEntity;
    +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    +import org.springframework.security.core.userdetails.User;
    +import org.springframework.security.core.userdetails.UserDetails;
    +import org.springframework.security.core.userdetails.UserDetailsService;
    +import org.springframework.security.core.userdetails.UsernameNotFoundException;
    +import org.springframework.web.client.RestTemplate;
    +import org.springframework.web.util.UriComponentsBuilder;
    +
    +import java.net.URI;
    +import java.util.Collections;
    +
    +@EnableConfigurationProperties(SnippetProperties.class)
    +@Configuration
    +public class SnippetSecurityConfig extends WebSecurityConfigurerAdapter {
    +
    +    private final Logger log = LoggerFactory.getLogger(SnippetSecurityConfig.class);
    +    private RestTemplate restTemplate;
    +    private UriComponentsBuilder builder;
    +    private SnippetProperties properties;
    +
    +    public SnippetSecurityConfig(RestTemplateBuilder restTemplateBuilder,SnippetProperties properties){
    +        this.restTemplate = restTemplateBuilder.basicAuthorization(properties.getAuthenticationUsername(),properties.getAuthenticationPassword()).build();
    +        this.properties = properties;
    +    }
    +
    +    @Override
    +    protected void configure(HttpSecurity http) throws Exception {
    +        http
    +                .authorizeRequests()
    +                .requestMatchers(EndpointRequest.to("status", "info"))
    +                .permitAll()
    +
    +                .requestMatchers(EndpointRequest.toAnyEndpoint())
    +                .hasRole("ACTUATOR")
    +
    +
    +                .requestMatchers(StaticResourceRequest.toCommonLocations())
    +                .permitAll()
    +
    +                .antMatchers("/api/**").hasRole("ADMIN")
    +                .antMatchers("/").hasRole("USER")
    +
    +                .and()
    +                .httpBasic();
    +    }
    +
    +    @Override
    +    public void configure(AuthenticationManagerBuilder auth) throws Exception { //(1)
    +        auth.userDetailsService(new UserDetailsService(){
    +
    +            @Override
    +            public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    +
    +                try {
    +                    builder = UriComponentsBuilder.fromUriString(properties.getAuthenticationUri())
    +                            .queryParam("email", email);
    +
    +                    log.info("Querying: " + builder.toUriString());
    +
    +                    ResponseEntity<Resource<Person>> responseEntity = restTemplate.exchange(  //(2)
    +                            RequestEntity.get(URI.create(builder.toUriString()))
    +                                .accept(MediaTypes.HAL_JSON)
    +                                .build()
    +                            , new ParameterizedTypeReference<Resource<Person>>() {
    +                            });
    +
    +                    if (responseEntity.getStatusCode() == HttpStatus.OK) {
    +
    +                        Resource<Person> resource = responseEntity.getBody();
    +                        Person person = resource.getContent();
    +                        return User.withDefaultPasswordEncoder().username(person.getEmail()).password(person.getPassword()).roles(person.getRole()).build();
    +                    }
    +
    +                }catch(Exception ex) {
    +                    ex.printStackTrace();
    +                }
    +
    +                throw new UsernameNotFoundException(email);
    +
    +
    +            }
    +        });
    +    }
    +
    +}
    +
    +
    +
    +
      +
    1. +

      We are configuring the AuthenticationManagerBuilder and the UserDetailsService that will use a restTemplate to contact the directory-web-security-app.

      +
    2. +
    3. +

      We are going to reach the /api/persons/search/findByEmailIgnoreCase endpoint. This endpoint returns a HAL+JSON, meaning that we need a Resource that will get email we are looking for.

      +
    4. +
    +
    +
  4. +
  5. +

    From the previous class, you’ve already saw that we need the SnippetProperties. Create/Modify the io.pivotal.workshop.snippet.config.SnippetProperties class.

    +
    +
    src/main/java/io/pivotal/workshop/snippet/config/SnippetProperties.java
    +
    +
    package io.pivotal.workshop.snippet.config;
    +
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +
    +@ConfigurationProperties(prefix = "snippet")
    +public class SnippetProperties {
    +
    +        private String path;
    +        private String authenticationUri;
    +        private String authenticationUsername;
    +        private String authenticationPassword;
    +
    +        public String getPath() {
    +                return path;
    +        }
    +
    +        public void setPath(String path) {
    +                this.path = path;
    +        }
    +
    +    public String getAuthenticationUri() {
    +        return authenticationUri;
    +    }
    +
    +    public void setAuthenticationUri(String authenticationUri) {
    +        this.authenticationUri = authenticationUri;
    +    }
    +
    +    public String getAuthenticationUsername() {
    +        return authenticationUsername;
    +    }
    +
    +    public void setAuthenticationUsername(String authenticationUsername) {
    +        this.authenticationUsername = authenticationUsername;
    +    }
    +
    +    public String getAuthenticationPassword() {
    +        return authenticationPassword;
    +    }
    +
    +    public void setAuthenticationPassword(String authenticationPassword) {
    +        this.authenticationPassword = authenticationPassword;
    +    }
    +}
    +
    +
    +
  6. +
  7. +

    Open the src/main/resources/application.properties file and add the following configuration.

    +
    +
    src/main/resources/application.properties
    +
    +
    ## Web App Port
    +server.port=${port:8081}
    +
    +## JPA
    +# spring.jpa.show-sql=true
    +spring.jpa.generate-ddl=true
    +spring.jpa.hibernate.ddl-auto=create-drop
    +
    +## REST
    +spring.data.rest.base-path=api
    +
    +## ACTUATOR
    +management.endpoints.web.base-path=/admin
    +endpoints.default.web.enabled=true
    +
    +## Snippet
    +snippet.path=/tmp/snippets
    +
    +## URL
    +snippet.authentication-uri=http://localhost:8585/api/persons/search/findByEmailIgnoreCase
    +snippet.authentication-username=admin
    +snippet.authentication-password=admin
    +
    +
    +
    +

    As you can see we are adding the snippet.authentication* properties, with the credentials and uri of the directory-web-security app.

    +
    +
  8. +
  9. +

    Next, open a terminal window in the code-snippet-manager-security project and create the JAR file.

    +
    +
    maven
    +
    +
    ./mvnw clean package -DskipTests=true
    +
    +
    +
    +
    gradle
    +
    +
    ./gradlew build -x test
    +
    +
    +
  10. +
  11. +

    Deploy the code-snippet-manager-security JAR but don’t start the application just yet.

    +
    +
    If you used Maven, it creates the JAR in the target/ folder. If you used Gradle, the JAR is in the build/libs folder.
    +
    +
    cf push code-snippet-manager -p target/code-snippet-manager-security-0.0.1-SNAPSHOT.jar --random-route -b java_buildpack --no-start
    +
    +
    +
  12. +
  13. +

    Remember that this application relies on the application.properties because it was added the credentials and the uri, so we need to alter at least the snippet.authentication-uri, so we can add the directory-web's uri. +to Modify this we are going to use environment variables:

    +
    +
    +
    cf set-env code-snippet-manager SNIPPET_AUTHENTICATION_URI http://directory-web-unapprehended-pluralism.cfapps.io/api/persons/search/findByEmailIgnoreCase
    +
    +
    +
    +

    The URI is the one from the directory-web application you have just deployed.

    +
    +
  14. +
  15. +

    Start the code-snippet-manager application.

    +
    +
    +
    cf start code-snippet-manager
    +
    +
    +
  16. +
  17. +

    Once started make sure is working by accessing the code-snippet-manager app /api using the credentials.

    +
  18. +
+
+
+
+
+
+

Challenges

+
+
+
    +
  • +

    Add the code from the code-snippet-manager-amqp to use a RabbitMQ service.

    +
  • +
  • +

    Instead of using an H2 as persistence engine, use a MySQL service, what changes do you need to do in your code?

    +
  • +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/14-spring-boot-extending.html b/src/main/UploadedContent/student-lab-instructions/14-spring-boot-extending.html new file mode 100644 index 0000000..de1061c --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/14-spring-boot-extending.html @@ -0,0 +1,1722 @@ + + + + + + + +Spring Boot - Creating a Custom Spring Boot Starter + + + + + + + + + + + + + + + +
+
+
+
+

You will create a custom Spring Boot Starter using the auto-configuration features.

+
+
+

Time: 30 minutes.

+
+ +
+
+
+

Spring Boot Starter

+
+
+

We are going create custom Spring Boot Starter. This starter will be a reusable client for the directory-web-security project. It will have the following features:

+
+
+
    +
  • +

    DirectoryWebClient bean that will have access to the persons in the directory. You will add a new person or list all the persons.

    +
  • +
  • +

    @EnableDirectoryWebClientUtils annotation that will bring a DirectoryWebClientUtils that will expose password encode using either BCRYPT (default) or PBKDF2 algorthims.

    +
  • +
+
+
+
+
+

Project Structure

+
+
+
    +
  1. +

    Create a directory structure where we are going to place 3 apps:

    +
    +
      +
    • +

      demo-starter

      +
    • +
    • +

      directory-web-client-spring-boot-autoconfigure

      +
    • +
    • +

      directory-web-client-spring-boot-starter

      +
      +
      +
      mkdir -p workspace/demo-starter workspace/directory-web-client-spring-boot-autoconfigure workspace/directory-web-client-spring-boot-starter
      +
      +
      +
    • +
    +
    +
  2. +
  3. +

    In the workspace directory add the following pom.xml. This will hold all the module information and the general dependencies.

    +
    +
    +
    <?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +                 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +        <modelVersion>4.0.0</modelVersion>
    +        <groupId>io.pivotal.workshop</groupId>
    +        <artifactId>directory-web-client</artifactId>
    +        <version>0.0.1-SNAPSHOT</version>
    +        <packaging>pom</packaging>
    +        <name>directory-web-client</name>
    +
    +        <modules>
    +                <module>directory-web-client-spring-boot-autoconfigure</module>
    +                <module>directory-web-client-spring-boot-starter</module>
    +                <module>demo-starter</module>
    +        </modules>
    +
    +
    +
    +        <dependencyManagement>
    +                <dependencies>
    +                        <dependency>
    +                                <groupId>org.springframework.boot</groupId>
    +                                <artifactId>spring-boot-dependencies</artifactId>
    +                                <version>2.0.0.M7</version>
    +                                <type>pom</type>
    +                                <scope>import</scope>
    +                        </dependency>
    +                </dependencies>
    +        </dependencyManagement>
    +
    +
    +    <repositories>
    +        <repository>
    +            <id>spring-snapshots</id>
    +            <name>Spring Snapshots</name>
    +            <url>https://repo.spring.io/snapshot</url>
    +            <snapshots>
    +                <enabled>true</enabled>
    +            </snapshots>
    +        </repository>
    +        <repository>
    +            <id>spring-milestones</id>
    +            <name>Spring Milestones</name>
    +            <url>https://repo.spring.io/milestone</url>
    +            <snapshots>
    +                <enabled>false</enabled>
    +            </snapshots>
    +        </repository>
    +    </repositories>
    +
    +    <pluginRepositories>
    +        <pluginRepository>
    +            <id>spring-snapshots</id>
    +            <name>Spring Snapshots</name>
    +            <url>https://repo.spring.io/snapshot</url>
    +            <snapshots>
    +                <enabled>true</enabled>
    +            </snapshots>
    +        </pluginRepository>
    +        <pluginRepository>
    +            <id>spring-milestones</id>
    +            <name>Spring Milestones</name>
    +            <url>https://repo.spring.io/milestone</url>
    +            <snapshots>
    +                <enabled>false</enabled>
    +            </snapshots>
    +        </pluginRepository>
    +    </pluginRepositories>
    +
    +
    +</project>
    +
    +
    +
    +

    As you can see it has the main dependency right now spring-boot-dependencies:2.0.0.M6 and is a pom import. It defines our 3 modules.

    +
    +
  4. +
+
+
+
+
+

Directory Web Client Spring Boot Starter

+
+
+

It’s a good idea to create custom starter by leaving at the end the keywords *-spring-boot-starter and at the beginning the technology that you are going to build.

+
+
+
    +
  1. +

    In the directory workspace/directory-web-client-spring-boot-starter add the following pom.xml file:

    +
    +
    +
    <?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +                 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +        <modelVersion>4.0.0</modelVersion>
    +        <groupId>io.pivotal.workshop</groupId>
    +        <artifactId>directory-web-client-spring-boot-starter</artifactId>
    +        <version>0.0.1-SNAPSHOT</version>
    +        <packaging>jar</packaging>
    +
    +        <name>directory-web-client-spring-boot-starter</name>
    +        <description>Demo project for Spring Boot</description>
    +
    +        <properties>
    +                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +                <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    +        </properties>
    +
    +    <parent>  <!-- <1> -->
    +        <groupId>io.pivotal.workshop</groupId>
    +        <artifactId>directory-web-client</artifactId>
    +        <version>0.0.1-SNAPSHOT</version>
    +        <relativePath>..</relativePath>
    +    </parent>
    +
    +        <dependencies>
    +                <dependency>
    +                        <groupId>io.pivotal.workshop</groupId>   <!-- <2> -->
    +                        <artifactId>directory-web-client-spring-boot-autoconfigure</artifactId>
    +                        <version>0.0.1-SNAPSHOT</version>
    +                </dependency>
    +        </dependencies>
    +
    +
    +
    +</project>
    +
    +
    +
    +
      +
    1. +

      See that we are using our parent pom.

      +
    2. +
    3. +

      We are adding our auto-configuration app.

      +
    4. +
    +
    +
    +

    Thats it, the spring-boot-starter is just a pom.xml file that declares the auto-configuration dependency.

    +
    +
  2. +
+
+
+
+
+

Directory Web Client Spring Boot Autoconfigure

+
+
+

This project will have all the logic to be a reusable component in any project.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Directory Web Client Spring Boot Autoconfigure Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 1. Directory Web Client Spring Boot Autoconfigure - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    directory-web-client-spring-boot-autoconfigure

    Name:

    directory-web-client-spring-boot-autoconfigure

    Package Name:

    io.pivotal.workshop.directory

    Dependencies:

    Web, HATEOAS, Security, Lombok, Configuration Processor

    Project Type:

    Maven

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
  6. +
  7. +

    Type Web, HATEOAS, Security, Lombok and Configuration Processor in the Dependencies field and press Enter.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    IMPORTANT Unzip the file in any directory you want and COPY the content in the workspace/directory-web-client-spring-boot-autoconfigure directory.

    +
  12. +
  13. +

    Import your project in any IDE you want.

    +
  14. +
  15. +

    Modify the pom.xml and make sure it looks like this:

    +
    +
    +
    <?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +        <modelVersion>4.0.0</modelVersion>
    +
    +        <groupId>io.pivotal.workshop</groupId>
    +        <artifactId>directory-web-client-spring-boot-autoconfigure</artifactId>
    +        <version>0.0.1-SNAPSHOT</version>
    +        <packaging>jar</packaging>
    +
    +        <name>directory-web-client-spring-boot-autoconfigure</name>
    +        <description>Demo project for Spring Boot</description>
    +
    +        <properties>
    +                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +                <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    +                <java.version>1.8</java.version>
    +        </properties>
    +
    +    <parent>
    +        <groupId>io.pivotal.workshop</groupId>
    +        <artifactId>directory-web-client</artifactId>
    +        <version>0.0.1-SNAPSHOT</version>
    +        <relativePath>..</relativePath>
    +    </parent>
    +
    +        <dependencies>
    +
    +                <dependency>
    +                        <groupId>org.projectlombok</groupId>
    +                        <artifactId>lombok</artifactId>
    +                </dependency>
    +                <dependency>
    +                        <groupId>org.springframework.boot</groupId>
    +                        <artifactId>spring-boot-starter-security</artifactId>
    +                </dependency>
    +
    +                <dependency>
    +                        <groupId>org.springframework.hateoas</groupId>
    +                        <artifactId>spring-hateoas</artifactId>
    +                </dependency>
    +
    +                <dependency>
    +                        <groupId>org.springframework.boot</groupId>
    +                        <artifactId>spring-boot-starter-web</artifactId>
    +                </dependency>
    +
    +
    +                <dependency>
    +                        <groupId>org.springframework.boot</groupId>
    +                        <artifactId>spring-boot-starter-test</artifactId>
    +                        <scope>test</scope>
    +                </dependency>
    +                <dependency>
    +                        <groupId>org.springframework.security</groupId>
    +                        <artifactId>spring-security-test</artifactId>
    +                        <scope>test</scope>
    +                </dependency>
    +
    +                <dependency>
    +                        <groupId>org.springframework.boot</groupId>
    +                        <artifactId>spring-boot-configuration-processor</artifactId>
    +                        <optional>true</optional>
    +                </dependency>
    +
    +
    +
    +        </dependencies>
    +
    +
    +</project>
    +
    +
    +
  16. +
  17. +

    Let’s start by telling Spring Boot where to go for the auto-configuration. Create in the src/main/resources directory, a new folder named: META-INF and inside this folder a file named: spring.factories with the following content:

    +
    +
    src/main/resources/META-INF/spring.factories
    +
    +
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.pivotal.workshop.directory.configuration.DirectoryWebClientAutoConfiguration
    +
    +
    +
    +

    Here we are declaring the class that will do the auto-configuration, in this case the io.pivotal.workshop.directory.configuration.DirectoryWebClientAutoConfiguration class.

    +
    +
  18. +
  19. +

    Next let’s create the io.pivotal.workshop.directory.configuration.DirectoryWebClientAutoConfiguration class:

    +
    +
    src/main/java/io/pivotal/workshop/directory/configuration/DirectoryWebClientAutoConfiguration.java
    +
    +
    package io.pivotal.workshop.directory.configuration;
    +
    +import io.pivotal.workshop.directory.client.DirectoryWebClient;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    +import org.springframework.boot.context.properties.EnableConfigurationProperties;
    +import org.springframework.boot.web.client.RestTemplateBuilder;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.hateoas.Resource;
    +import org.springframework.web.client.RestTemplate;
    +
    +//(1)
    +@Configuration
    +@ConditionalOnClass({Resource.class,RestTemplateBuilder.class})
    +@EnableConfigurationProperties(DirectoryWebClientProperties.class)
    +public class DirectoryWebClientAutoConfiguration {
    +
    +    private final Logger log = LoggerFactory.getLogger(DirectoryWebClientAutoConfiguration.class);
    +    private DirectoryWebClientProperties webClientProperties;
    +    private RestTemplateBuilder restTemplateBuilder;
    +
    +    //(2)
    +    public DirectoryWebClientAutoConfiguration(DirectoryWebClientProperties webClientProperties, RestTemplateBuilder restTemplateBuilder) {
    +        this.webClientProperties = webClientProperties;
    +        this.restTemplateBuilder = restTemplateBuilder;
    +    }
    +
    +    //(3)
    +    @Bean
    +    public DirectoryWebClient directoryClient(){
    +        log.info("Creating a Directory Web Client...");
    +        return new DirectoryWebClient(restTemplate(),this.webClientProperties);
    +    }
    +
    +
    +    //(4)
    +    @Bean
    +    public RestTemplate restTemplate(){
    +        // Make sure the directory-web-security has the httpBasic() security enabled and NOT the formLogin()
    +        log.info("Setting up admin credentials for Directory Web Client ...");
    +        return this.restTemplateBuilder.basicAuthorization(webClientProperties.getUsername(),webClientProperties.getPassword()).build();
    +    }
    +
    +
    +}
    +
    +
    +
    +
      +
    1. +

      This class is annotated with the @ConditionalOnClass that checks if in the classpath is the Resource and the RestTemplateBuilder classes, and if they do, then it will configure all the beans declared in this class.

      +
    2. +
    3. +

      The constructor injects the DirectoryWebClientProperties and the RestTemplateBuilder. The DirectoryWebClientProperties will bring the username and password (the admin credentials for the directory-web-security app.), and the RestTemplateBuilder will create the RestTemplate instance, needed for do all the rest operations.

      +
    4. +
    5. +

      The directoryClient is the one created to do all the rest operations over the directory-web-security app endpoints.

      +
    6. +
    7. +

      This is the RestTeamplate that holds the administrator credentials so it can do all the Rest operations.

      +
    8. +
    +
    +
    + + + + + +
    +
    Tip
    +
    +Is important to make sure the directory-web-security has the httpBasic() security enabled and NOT the formLogin() +
    +
    +
  20. +
  21. +

    Create the io.pivotal.workshop.directory.configuration.DirectoryWebClientProperties class. This class will have the admin credentials and the uri of the directory-web-security app.:

    +
    +
    src/main/java/io/pivotal/workshop/directory/configuration/DirectoryWebClientProperties.java
    +
    +
    package io.pivotal.workshop.directory.configuration;
    +
    +import lombok.Data;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +
    +@Data
    +@ConfigurationProperties(prefix = "directory.web.client")
    +public class DirectoryWebClientProperties {
    +
    +    String username = "admin";
    +    String password = "admin";
    +    String uri = "http://localhost:8585/api/persons";
    +
    +}
    +
    +
    +
    +

    See that we are using the Lombok library (with the @Data annotation)so it can generate the setters and getters from the fields.

    +
    +
  22. +
  23. +

    Next, let’s create the actual DirectoryWebClient class that will do all the Rest operations. Create the io.pivotal.workshop.directory.client.DirectoryWebClient class:

    +
    +
    src/main/java/io/pivotal/workshop/directory/client/DirectoryWebClient.java
    +
    +
    package io.pivotal.workshop.directory.client;
    +
    +
    +import io.pivotal.workshop.directory.configuration.DirectoryWebClientProperties;
    +import io.pivotal.workshop.directory.domain.Directory;
    +import io.pivotal.workshop.directory.domain.Person;
    +import org.springframework.core.ParameterizedTypeReference;
    +import org.springframework.hateoas.MediaTypes;
    +import org.springframework.hateoas.Resource;
    +import org.springframework.http.HttpStatus;
    +import org.springframework.http.RequestEntity;
    +import org.springframework.http.ResponseEntity;
    +import org.springframework.web.client.RestTemplate;
    +
    +import java.util.Collection;
    +import java.util.Collections;
    +
    +import static java.net.URI.create;
    +
    +public class DirectoryWebClient {
    +
    +
    +    private RestTemplate restTemplate;
    +    private DirectoryWebClientProperties props;
    +
    +    public DirectoryWebClient(RestTemplate restTemplate, DirectoryWebClientProperties props){
    +        this.restTemplate = restTemplate;
    +        this.props = props;
    +    }
    +
    +    public Person add(Person person){
    +        ResponseEntity<Resource<Person>> response =
    +                this.restTemplate.exchange(
    +                        RequestEntity.post(
    +                            create(this.props.getUri()))
    +                            .body(person)
    +                        ,new ParameterizedTypeReference<Resource<Person>>() {});
    +
    +        return response.getBody().getContent();
    +    }
    +
    +    public Collection<Person> getAll() {
    +
    +        ResponseEntity<Resource<Directory>> responseEntity =
    +                this.restTemplate.exchange(RequestEntity.get(create(this.props.getUri()))
    +                        .accept(MediaTypes.HAL_JSON)
    +                        .build(),new ParameterizedTypeReference<Resource<Directory>>() {});
    +
    +        if(responseEntity.getStatusCode() == HttpStatus.OK) {
    +            Directory company = responseEntity.getBody().getContent();
    +            return company.getEmbedded().getPersons();
    +        }else
    +            return Collections.emptyList();
    +    }
    +
    +}
    +
    +
    +
    +

    See that it has (for now) two methods. To add a new person object to the directory and get all the persons in the directory. As you can see, is using a Directoy and Person domain classes.

    +
    +
  24. +
  25. +

    Create the necessary domains, the io.pivotal.workshop.directory.domain.Directory and io.pivotal.workshop.directory.domain.Person classes, you can copy the Person class from the directory-web-security app, without the JPA annotations:

    +
    +
    src/main/java/io/pivotal/workshop/directory/domain/Directory.java
    +
    +
    package io.pivotal.workshop.directory.domain;
    +
    +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    +import com.fasterxml.jackson.annotation.JsonProperty;
    +import lombok.Data;
    +
    +import java.util.List;
    +
    +@Data
    +@JsonIgnoreProperties(ignoreUnknown = true)
    +public class Directory {
    +
    +    @JsonProperty("_embedded")
    +    public Embedded embedded;
    +
    +    @Data
    +    public class Embedded {
    +
    +        public List<Person> persons;
    +
    +    }
    +}
    +
    +
    +
    +
    src/main/java/io/pivotal/workshop/directory/domain/Person.java
    +
    +
    package io.pivotal.workshop.directory.domain;
    +
    +
    +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    +import com.fasterxml.jackson.annotation.JsonProperty;
    +import lombok.AllArgsConstructor;
    +import lombok.Data;
    +import lombok.NoArgsConstructor;
    +
    +import java.text.ParseException;
    +import java.text.SimpleDateFormat;
    +import java.util.Date;
    +
    +@JsonIgnoreProperties(ignoreUnknown = true)
    +public class Person {
    +
    +
    +    private SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd");
    +
    +    private String id;
    +    private String email;
    +    private String name;
    +    private String password;
    +    private String role = "USER";
    +    private boolean enabled = true;
    +    private Date birthday;
    +    private Date created;
    +    private Date modified;
    +
    +    public Person() {
    +        this.created = new Date();
    +        this.modified = new Date();
    +    }
    +
    +    public Person(String email, String name, String password, String birthday) {
    +        this();
    +        this.email = email;
    +        this.name = name;
    +        this.password = password;
    +
    +        try {
    +            this.birthday = date.parse(birthday);
    +        } catch (ParseException e) {
    +            this.birthday = null;
    +        }
    +    }
    +
    +    public Person(String email, String name, String password, Date birthday) {
    +        this();
    +        this.email = email;
    +        this.name = name;
    +        this.password = password;
    +        this.birthday = birthday;
    +    }
    +
    +    public Person(String email, String name, String password, String birthday, String role, boolean enabled) {
    +        this(email, name, password, birthday);
    +        this.role = role;
    +        this.enabled = enabled;
    +    }
    +
    +    public String getId() {
    +        return id;
    +    }
    +
    +    public void setId(String id) {
    +        this.id = id;
    +    }
    +
    +    public String getEmail() {
    +        return email;
    +    }
    +
    +    public void setEmail(String email) {
    +        this.email = email;
    +    }
    +
    +    public String getName() {
    +        return name;
    +    }
    +
    +    public void setName(String name) {
    +        this.name = name;
    +    }
    +
    +    public String getPassword() {
    +        return password;
    +    }
    +
    +    public void setPassword(String password) {
    +        this.password = password;
    +    }
    +
    +    public Date getBirthday() {
    +        return birthday;
    +    }
    +
    +    public void setBirthday(Date birthday) {
    +        this.birthday = birthday;
    +    }
    +
    +    public Date getCreated() {
    +        return created;
    +    }
    +
    +    public Date getModified() {
    +        return modified;
    +    }
    +
    +    public String getRole() {
    +        return role;
    +    }
    +
    +    public void setRole(String role) {
    +        this.role = role;
    +    }
    +
    +    public boolean isEnabled() {
    +        return enabled;
    +    }
    +
    +    public void setEnabled(boolean enabled) {
    +        this.enabled = enabled;
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "Person{" +
    +                "id='" + id + '\'' +
    +                ", email='" + email + '\'' +
    +                ", name='" + name + '\'' +
    +                ", password='" + password + '\'' +
    +                ", role='" + role + '\'' +
    +                ", enabled=" + enabled +
    +                ", birthday=" + birthday +
    +                ", date=" + date +
    +                ", created=" + created +
    +                ", modified=" + modified +
    +                '}';
    +    }
    +}
    +
    +
    +
  26. +
  27. +

    Lets add the next feature, the @EnableDirectoryWebClientUtils that will create a DirectoryWebClientUtils bean depending of what algorithm was declared. Add the custom annotation io.pivotal.workshop.directory.annotation.EnableDirectoryWebClientUtils class and its dependency, the io.pivotal.workshop.directory.annotation.Algorithm enum:

    +
    +
    src/main/java/io/pivotal/workshop/directory/annotation/EnableDirectoryWebClientUtils.java
    +
    +
    package io.pivotal.workshop.directory.annotation;
    +
    +
    +import io.pivotal.workshop.directory.utils.DirectoryWebClientUtilsConfiguration;
    +import org.springframework.context.annotation.Import;
    +
    +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.TYPE)
    +@Import(DirectoryWebClientUtilsConfiguration.class)
    +public @interface EnableDirectoryWebClientUtils {
    +    Algorithm algorithm() default Algorithm.BCRYPT;
    +}
    +
    +
    +
    +
    src/main/java/io/pivotal/workshop/directory/annotation/Algorithm.java
    +
    +
    package io.pivotal.workshop.directory.annotation;
    +
    +public enum Algorithm {
    +    BCRYPT, PBKDF2
    +}
    +
    +
    +
  28. +
  29. +

    Next let’s create the configuration needed to create the DirectoryWebClientUtils bean. Create the io.pivotal.workshop.directory.utils.DirectoryWebClientUtilsConfiguration class. This class will implement the ImportSelector interface that will be picked up by Spring Boot during the auto-configuration. It will identify if the @EnableDirectoryWebClientUtils was declared in a @Configuration class and it will create the necessary beans.

    +
    +
    src/main/java/io/pivotal/workshop/directory/utils/DirectoryWebClientUtilsConfiguration.java
    +
    +
    package io.pivotal.workshop.directory.utils;
    +
    +
    +import io.pivotal.workshop.directory.annotation.Algorithm;
    +import io.pivotal.workshop.directory.annotation.EnableDirectoryWebClientUtils;
    +import org.springframework.context.annotation.ImportSelector;
    +import org.springframework.core.annotation.AnnotationAttributes;
    +import org.springframework.core.type.AnnotationMetadata;
    +
    +public class DirectoryWebClientUtilsConfiguration implements ImportSelector {
    +
    +    public String[] selectImports(AnnotationMetadata annotationMetadata) {
    +        AnnotationAttributes attributes =
    +                AnnotationAttributes.fromMap(
    +                        annotationMetadata.getAnnotationAttributes(EnableDirectoryWebClientUtils.class.getName(), false));
    +        Algorithm algorithm = attributes.getEnum("algorithm");
    +        switch(algorithm){
    +            case PBKDF2:
    +                return new String[] {"io.pivotal.workshop.directory.utils.Pbkdf2Encoder"};
    +            case BCRYPT:
    +                default:
    +                return new String[] {"io.pivotal.workshop.directory.utils.BcryptEncoder"};
    +        }
    +    }
    +}
    +
    +
    +
    +

    The DirectoryWebClientUtilsConfiguration will decide which class to configure based on the Algorithm selected.

    +
    +
  30. +
  31. +

    Lets create the io.pivotal.workshop.directory.utils.BCryptEncoder and io.pivotal.workshop.directory.utils.Pbkdf2Encoder configuration classes. These classes will declare the DirectoryWebClientUtils bean and it will have the Algorithm selected.

    +
    +
    src/main/java/io/pivotal/workshop/directory/utils/BCryptEncoder.java
    +
    +
    package io.pivotal.workshop.directory.utils;
    +
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    +
    +@Configuration
    +public class BCryptEncoder {
    +
    +    @Bean
    +    public DirectoryWebClientUtils utils(){
    +        return new DirectoryWebClientUtils(new BCryptPasswordEncoder(16));
    +    }
    +}
    +
    +
    +
    +
    src/main/java/io/pivotal/workshop/directory/utils/Pbkdf2Encoder.java
    +
    +
    package io.pivotal.workshop.directory.utils;
    +
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
    +
    +@Configuration
    +public class Pbkdf2Encoder {
    +
    +    @Bean
    +    public DirectoryWebClientUtils utils(){
    +        return new DirectoryWebClientUtils(new Pbkdf2PasswordEncoder());
    +    }
    +}
    +
    +
    +
  32. +
  33. +

    Next, create the io.pivotal.workshop.directory.utils.DirectoryWebClientUtils class that will be the bean created based on the Algorithm selected:

    +
    +
    src/main/java/io/pivotal/workshop/directory/utils/DirectoryWebClientUtils.java
    +
    +
    package io.pivotal.workshop.directory.utils;
    +
    +import lombok.Data;
    +import org.springframework.security.crypto.password.PasswordEncoder;
    +
    +
    +@Data
    +public class DirectoryWebClientUtils {
    +
    +    private PasswordEncoder encoder;
    +
    +    public DirectoryWebClientUtils(PasswordEncoder encoder){
    +        this.encoder = encoder;
    +    }
    +
    +}
    +
    +
    +
  34. +
+
+
+
+
+

Demo Starter

+
+
+

This project will test the custom spring-boot-starter.

+
+
+
    +
  1. +

    Open a browser and hit the url: http://start.spring.io

    +
  2. +
  3. +

    Click the Switch to the full version link.

    +
  4. +
  5. +

    Fill out the Demo Starter Project metadata with (See Figure 1.0):

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Table 2. Demo Starter - metadata
    PropertyValue

    Group:

    io.pivotal.workshop

    Artifact:

    demo-starter

    Name:

    demo-starter

    Package Name:

    io.pivotal.workshop

    Project Type:

    Maven

    Spring Boot:

    2.0.0.M7

    +
    +
    Figure 1.0: Spring Initializr - http://start.spring.io
    +

    SpringInitializr

    +
    +
  6. +
  7. +

    This is just a simple Spring Boot app, without any additional Dependencies from the Spring Initializr.

    +
  8. +
  9. +

    Click the Generate Project button.

    +
  10. +
  11. +

    IMPORTANT Unzip the file in any directory you want and COPY the content in the workspace/demo-starter directory.

    +
  12. +
  13. +

    Import the project into your favorite IDE.

    +
  14. +
  15. +

    Add the directory-web-client-spring-boot-starter dependency to your pom.xml:

    +
    +
    +
    <dependency>
    +    <groupId>io.pivotal.workshop</groupId>
    +    <artifactId>directory-web-client-spring-boot-starter</artifactId>
    +    <version>0.0.1-SNAPSHOT</version>
    +</dependency>
    +
    +
    +
  16. +
  17. +

    In the src/main/resources/application.properties file add the following properties:

    +
    +
    +
    directory.web.client.username=admin
    +directory.web.client.password=admin
    +directory.web.client.uri=http://localhost:8585/api/persons
    +
    +
    +
    +

    These are the defaul values, but this give us more flexibility if the directory-web-security app has different credentials or is running in a different port.

    +
    +
  18. +
  19. +

    Next, we are ready to start testing. Make sure you have the directory-web-security running in the port 8585.

    +
  20. +
+
+
+

Testing the DirectoryWebClient bean

+
+
    +
  1. +

    In your demo-starter project open the DemoStarterApplication class and add the following code:

    +
    +
    +
    package io.pivotal.workshop;
    +
    +import io.pivotal.workshop.directory.client.DirectoryWebClient;
    +import org.springframework.boot.ApplicationRunner;
    +import org.springframework.boot.SpringApplication;
    +import org.springframework.boot.autoconfigure.SpringBootApplication;
    +import org.springframework.context.annotation.Bean;
    +
    +@SpringBootApplication
    +public class DemoStarterApplication {
    +
    +        public static void main(String[] args) {
    +                SpringApplication.run(DemoStarterApplication.class, args);
    +        }
    +
    +        @Bean
    +        ApplicationRunner getPersons(DirectoryWebClient client){
    +                return args -> {
    +                        client.getAll().forEach(System.out::println);
    +                };
    +        }
    +
    +}
    +
    +
    +
    +

    In the above code we are using the DirectoryWebClient client and calling the getAll() method.

    +
    +
  2. +
  3. +

    Run the application and you should see all the persons that are in the directory-web-security app.

    +
  4. +
+
+
+ + + + + +
+
Tip
+
+If you are using the command: ./mvnw spring-boot:run remember that you need to do a: ./mvnw install in the root folder (workspace), so the directory-web-client-spring-boot-starter and its dependencies get installed so they can be used. +
+
+
+
+

Testing the @EnableDirectoryWebClientUtils annotation.

+
+
    +
  1. +

    In your demo-starter project open the DemoStarterApplication class and add the following code:

    +
    +
    +
    package io.pivotal.workshop;
    +
    +import io.pivotal.workshop.directory.annotation.Algorithm;
    +import io.pivotal.workshop.directory.annotation.EnableDirectoryWebClientUtils;
    +import io.pivotal.workshop.directory.client.DirectoryWebClient;
    +import io.pivotal.workshop.directory.utils.DirectoryWebClientUtils;
    +import org.springframework.boot.ApplicationRunner;
    +import org.springframework.boot.SpringApplication;
    +import org.springframework.boot.autoconfigure.SpringBootApplication;
    +import org.springframework.context.annotation.Bean;
    +
    +@EnableDirectoryWebClientUtils(algorithm = Algorithm.PBKDF2)
    +@SpringBootApplication
    +public class DemoStarterApplication {
    +
    +        public static void main(String[] args) {
    +                SpringApplication.run(DemoStarterApplication.class, args);
    +        }
    +
    +        @Bean
    +        ApplicationRunner getPersons(DirectoryWebClient client){
    +                return args -> {
    +                        client.getAll().forEach(System.out::println);
    +                };
    +        }
    +
    +        @Bean
    +        ApplicationRunner encode(DirectoryWebClientUtils utils){
    +                return args -> {
    +                    String text = "This text will be encrypted";
    +                        String hash = utils.getEncoder().encode(text);
    +                        System.out.println(">>> ENCRYPT: " + hash);
    +                        System.out.println(">>> Verify: " + utils.getEncoder().matches(text,hash));
    +                };
    +        }
    +
    +}
    +
    +
    +
  2. +
  3. +

    Run the application and you will see the encrypted text.

    +
  4. +
+
+
+

CONGRATS! you have created your first custom spring-boot-starter

+
+
+
+
+
+

Challenges

+
+
+
    +
  1. +

    Comment out the @EnableDirectoryWebClientUtils, run the app again and see how the it fails because is looking for the DirectoryWebClientUtils bean. That’s the power of creating a @Enable* technology.

    +
  2. +
  3. +

    Add the code to add a new person using the DirectoryWebClient bean.

    +
  4. +
+
+
+
+
+

HOMEWORK

+
+
+
    +
  1. +

    Create a Web app that has the CRUD (Create, Read, Up, Delete) Forms for all these actions. It will use the directory-web-client-spring-boot-starter:

    +
    +
      +
    • +

      It will encrypt the user password (TIP: You can use the DirectoryWebClientUtils bean)

      +
    • +
    • +

      Add all the missing logic for Update, Delete, etc to the directory-web-client-spring-boot-autoconfigure so it can be more reusable for this new web app.

      +
    • +
    +
    +
  2. +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/home.html b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/home.html new file mode 100644 index 0000000..ee920a2 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/home.html @@ -0,0 +1,609 @@ + + + + + + + +Untitled + + + + + + + + + + + + + + + +
+
+
src/main/resources/templates/views/home.tpl
+
+
layout 'layouts/main.tpl',
+        pageTitle: 'Code Snippet Manager',
+        mainBody: contents {
+                        div(class: 'col-xs-12 col-sm-12'){
+
+                                p(class: 'pull-right visible-xs') {
+                                        button(type: 'button', class: 'btn btn-primary btn-xs', 'data-toggle':'offcanvas', 'Toggle nav')
+                                }
+
+                                div(class: 'jumbotron') {
+                                    h1('Web Directory')
+                                    p('A new way to save your contacts information!')
+                            }
+
+                            div(class: 'row') {
+
+                            }
+            }
+
+        }
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/main.html b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/main.html new file mode 100644 index 0000000..1f1bf14 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/main.html @@ -0,0 +1,629 @@ + + + + + + + +Untitled + + + + + + + + + + + + + + + +
+
+
src/main/resources/templates/layouts/main.tpl
+
+
yieldUnescaped '<!DOCTYPE html>'
+html {
+    head {
+        title(pageTitle)
+        meta(name: 'viewport', content:'width=device-width, initial-scale=1')
+
+        link(rel: 'stylesheet', href: 'webjars/bootstrap/3.3.6/css/bootstrap.min.css')
+        link(rel: 'stylesheet', href: 'css/offcanvas.css')
+        link(rel: 'stylesheet', href: 'css/theme.css')
+
+        script(src: 'webjars/jquery/2.2.4/jquery.min.js'){ }
+        script(src: 'webjars/bootstrap/3.3.6/js/bootstrap.min.js'){ }
+        script(src: 'webjars/angularjs/1.5.7/angular.min.js') { }
+
+    }
+    body {
+                    nav (class: 'navbar navbar-fixed-top navbar-inverse') {
+                            div(class: 'container'){
+                                    div(class: 'navbar-header'){
+                                            button(type: 'button', class: 'navbar-toggle collapsed', 'data-toggle':'collapse', 'data-target':'#navbar', 'aria-expanded':'false', 'aria-controls':'navbar'){
+
+                                            }
+                                            a(class:'navbar-brand','Web Directory')
+                                    }
+                            }
+                    }
+                    div(class: 'container'){
+                            div(class: 'row row-offcanvas row-offcanvas-right'){
+                                mainBody()
+                        }
+
+                        hr()
+
+            footer {
+                            p('&copy; 2017 Web Directory')
+            }
+        }
+
+    }
+}
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/offcanvas.html b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/offcanvas.html new file mode 100644 index 0000000..beb64bb --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/offcanvas.html @@ -0,0 +1,648 @@ + + + + + + + +Untitled + + + + + + + + + + + + + + + +
+
+
src/main/resources/static/css/offcanvas.css
+
+
/*
+ * Style tweaks
+ * --------------------------------------------------
+ */
+html,
+body {
+  overflow-x: hidden; /* Prevent scroll on narrow devices */
+}
+body {
+  padding-top: 70px;
+}
+footer {
+  padding: 30px 0;
+}
+
+/*
+ * Off Canvas
+ * --------------------------------------------------
+ */
+@media screen and (max-width: 767px) {
+  .row-offcanvas {
+    position: relative;
+    -webkit-transition: all .25s ease-out;
+         -o-transition: all .25s ease-out;
+            transition: all .25s ease-out;
+  }
+
+  .row-offcanvas-right {
+    right: 0;
+  }
+
+  .row-offcanvas-left {
+    left: 0;
+  }
+
+  .row-offcanvas-right
+  .sidebar-offcanvas {
+    right: -50%; /* 6 columns */
+  }
+
+  .row-offcanvas-left
+  .sidebar-offcanvas {
+    left: -50%; /* 6 columns */
+  }
+
+  .row-offcanvas-right.active {
+    right: 50%; /* 6 columns */
+  }
+
+  .row-offcanvas-left.active {
+    left: 50%; /* 6 columns */
+  }
+
+  .sidebar-offcanvas {
+    position: absolute;
+    top: 0;
+    width: 50%; /* 6 columns */
+  }
+}
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/theme.html b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/theme.html new file mode 100644 index 0000000..1bd8cea --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-security/theme.html @@ -0,0 +1,827 @@ + + + + + + + +Untitled + + + + + + + + + + + + + + + +
+
+
src/main/resources/static/css/theme.css
+
+
.syntaxhighlighter a,
+.syntaxhighlighter div,
+.syntaxhighlighter code,
+.syntaxhighlighter table,
+.syntaxhighlighter table td,
+.syntaxhighlighter table tr,
+.syntaxhighlighter table tbody,
+.syntaxhighlighter table thead,
+.syntaxhighlighter table caption,
+.syntaxhighlighter textarea {
+  -moz-border-radius: 0 0 0 0 !important;
+  -webkit-border-radius: 0 0 0 0 !important;
+  background: none !important;
+  border: 0 !important;
+  bottom: auto !important;
+  float: none !important;
+  height: auto !important;
+  left: auto !important;
+  line-height: 1.1em !important;
+  margin: 0 !important;
+  outline: 0 !important;
+  overflow: visible !important;
+  padding: 0 !important;
+  position: static !important;
+  right: auto !important;
+  text-align: left !important;
+  top: auto !important;
+  vertical-align: baseline !important;
+  width: auto !important;
+  box-sizing: content-box !important;
+  font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
+  font-weight: normal !important;
+  font-style: normal !important;
+  font-size: 1em !important;
+  min-height: inherit !important;
+  min-height: auto !important; }
+
+.syntaxhighlighter {
+  width: 100% !important;
+  margin: 1em 0 1em 0 !important;
+  position: relative !important;
+  overflow: auto !important;
+  font-size: 1em !important; }
+  .syntaxhighlighter .container:before, .syntaxhighlighter .container:after {
+    content: none !important; }
+  .syntaxhighlighter.source {
+    overflow: hidden !important; }
+  .syntaxhighlighter .bold {
+    font-weight: bold !important; }
+  .syntaxhighlighter .italic {
+    font-style: italic !important; }
+  .syntaxhighlighter .line {
+    white-space: pre !important; }
+  .syntaxhighlighter table {
+    width: 100% !important; }
+    .syntaxhighlighter table caption {
+      text-align: left !important;
+      padding: .5em 0 0.5em 1em !important; }
+    .syntaxhighlighter table td.code {
+      width: 100% !important; }
+      .syntaxhighlighter table td.code .container {
+        position: relative !important; }
+        .syntaxhighlighter table td.code .container textarea {
+          box-sizing: border-box !important;
+          position: absolute !important;
+          left: 0 !important;
+          top: 0 !important;
+          width: 100% !important;
+          height: 100% !important;
+          border: none !important;
+          background: white !important;
+          padding-left: 1em !important;
+          overflow: hidden !important;
+          white-space: pre !important; }
+    .syntaxhighlighter table td.gutter .line {
+      text-align: right !important;
+      padding: 0 0.5em 0 1em !important; }
+    .syntaxhighlighter table td.code .line {
+      padding: 0 1em !important; }
+  .syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
+    padding-left: 0em !important; }
+  .syntaxhighlighter.show {
+    display: block !important; }
+  .syntaxhighlighter.collapsed table {
+    display: none !important; }
+  .syntaxhighlighter.collapsed .toolbar {
+    padding: 0.1em 0.8em 0em 0.8em !important;
+    font-size: 1em !important;
+    position: static !important;
+    width: auto !important;
+    height: auto !important; }
+    .syntaxhighlighter.collapsed .toolbar span {
+      display: inline !important;
+      margin-right: 1em !important; }
+      .syntaxhighlighter.collapsed .toolbar span a {
+        padding: 0 !important;
+        display: none !important; }
+        .syntaxhighlighter.collapsed .toolbar span a.expandSource {
+          display: inline !important; }
+  .syntaxhighlighter .toolbar {
+    position: absolute !important;
+    right: 1px !important;
+    top: 1px !important;
+    width: 11px !important;
+    height: 11px !important;
+    font-size: 10px !important;
+    z-index: 10 !important; }
+    .syntaxhighlighter .toolbar span.title {
+      display: inline !important; }
+    .syntaxhighlighter .toolbar a {
+      display: block !important;
+      text-align: center !important;
+      text-decoration: none !important;
+      padding-top: 1px !important; }
+      .syntaxhighlighter .toolbar a.expandSource {
+        display: none !important; }
+  .syntaxhighlighter.ie {
+    font-size: .9em !important;
+    padding: 1px 0 1px 0 !important; }
+    .syntaxhighlighter.ie .toolbar {
+      line-height: 8px !important; }
+      .syntaxhighlighter.ie .toolbar a {
+        padding-top: 0px !important; }
+  .syntaxhighlighter.printing .line.alt1 .content,
+  .syntaxhighlighter.printing .line.alt2 .content,
+  .syntaxhighlighter.printing .line.highlighted .number,
+  .syntaxhighlighter.printing .line.highlighted.alt1 .content,
+  .syntaxhighlighter.printing .line.highlighted.alt2 .content {
+    background: none !important; }
+  .syntaxhighlighter.printing .line .number {
+    color: #bbbbbb !important; }
+  .syntaxhighlighter.printing .line .content {
+    color: black !important; }
+  .syntaxhighlighter.printing .toolbar {
+    display: none !important; }
+  .syntaxhighlighter.printing a {
+    text-decoration: none !important; }
+  .syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
+    color: black !important; }
+  .syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
+    color: #008200 !important; }
+  .syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
+    color: blue !important; }
+  .syntaxhighlighter.printing .keyword {
+    color: #006699 !important;
+    font-weight: bold !important; }
+  .syntaxhighlighter.printing .preprocessor {
+    color: gray !important; }
+  .syntaxhighlighter.printing .variable {
+    color: #aa7700 !important; }
+  .syntaxhighlighter.printing .value {
+    color: #009900 !important; }
+  .syntaxhighlighter.printing .functions {
+    color: #ff1493 !important; }
+  .syntaxhighlighter.printing .constants {
+    color: #0066cc !important; }
+  .syntaxhighlighter.printing .script {
+    font-weight: bold !important; }
+  .syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
+    color: gray !important; }
+  .syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
+    color: #ff1493 !important; }
+  .syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
+    color: red !important; }
+  .syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
+    color: black !important; }
+
+.syntaxhighlighter {
+  background-color: white !important; }
+  .syntaxhighlighter .line.alt1 {
+    background-color: white !important; }
+  .syntaxhighlighter .line.alt2 {
+    background-color: white !important; }
+  .syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
+    background-color: #e0e0e0 !important; }
+  .syntaxhighlighter .line.highlighted.number {
+    color: black !important; }
+  .syntaxhighlighter table caption {
+    color: black !important; }
+  .syntaxhighlighter table td.code .container textarea {
+    background: white;
+    color: black; }
+  .syntaxhighlighter .gutter {
+    color: #afafaf !important; }
+    .syntaxhighlighter .gutter .line {
+      border-right: 3px solid #6ce26c !important; }
+      .syntaxhighlighter .gutter .line.highlighted {
+        background-color: #6ce26c !important;
+        color: white !important; }
+  .syntaxhighlighter.printing .line .content {
+    border: none !important; }
+  .syntaxhighlighter.collapsed {
+    overflow: visible !important; }
+    .syntaxhighlighter.collapsed .toolbar {
+      color: #00f !important;
+      background: #fff !important;
+      border: 1px solid #6ce26c !important; }
+      .syntaxhighlighter.collapsed .toolbar a {
+        color: #00f !important; }
+        .syntaxhighlighter.collapsed .toolbar a:hover {
+          color: #f00 !important; }
+  .syntaxhighlighter .toolbar {
+    color: #fff !important;
+    background: #6ce26c !important;
+    border: none !important; }
+    .syntaxhighlighter .toolbar a {
+      color: #fff !important; }
+      .syntaxhighlighter .toolbar a:hover {
+        color: #000 !important; }
+  .syntaxhighlighter .plain, .syntaxhighlighter .plain a {
+    color: black !important; }
+  .syntaxhighlighter .comments, .syntaxhighlighter .comments a {
+    color: #008200 !important; }
+  .syntaxhighlighter .string, .syntaxhighlighter .string a {
+    color: blue !important; }
+  .syntaxhighlighter .keyword {
+    font-weight: bold !important;
+    color: #006699 !important; }
+  .syntaxhighlighter .preprocessor {
+    color: gray !important; }
+  .syntaxhighlighter .variable {
+    color: #aa7700 !important; }
+  .syntaxhighlighter .value {
+    color: #009900 !important; }
+  .syntaxhighlighter .functions {
+    color: #ff1493 !important; }
+  .syntaxhighlighter .constants {
+    color: #0066cc !important; }
+  .syntaxhighlighter .script {
+    font-weight: bold !important;
+    color: #006699 !important;
+    background-color: none !important; }
+  .syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
+    color: gray !important; }
+  .syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
+    color: #ff1493 !important; }
+  .syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
+    color: red !important; }
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/offcanvas.html b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/offcanvas.html new file mode 100644 index 0000000..ee6841d --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/offcanvas.html @@ -0,0 +1,648 @@ + + + + + + + +Untitled + + + + + + + + + + + + + + + +
+
+
src/main/resources/static/css/offcanvas.css
+
+
/*
+ * Style tweaks
+ * --------------------------------------------------
+ */
+html,
+body {
+  overflow-x: hidden; /* Prevent scroll on narrow devices */
+}
+body {
+  padding-top: 70px;
+}
+footer {
+  padding: 30px 0;
+}
+
+/*
+ * Off Canvas
+ * --------------------------------------------------
+ */
+@media screen and (max-width: 767px) {
+  .row-offcanvas {
+    position: relative;
+    -webkit-transition: all .25s ease-out;
+         -o-transition: all .25s ease-out;
+            transition: all .25s ease-out;
+  }
+
+  .row-offcanvas-right {
+    right: 0;
+  }
+
+  .row-offcanvas-left {
+    left: 0;
+  }
+
+  .row-offcanvas-right
+  .sidebar-offcanvas {
+    right: -50%; /* 6 columns */
+  }
+
+  .row-offcanvas-left
+  .sidebar-offcanvas {
+    left: -50%; /* 6 columns */
+  }
+
+  .row-offcanvas-right.active {
+    right: 50%; /* 6 columns */
+  }
+
+  .row-offcanvas-left.active {
+    left: 50%; /* 6 columns */
+  }
+
+  .sidebar-offcanvas {
+    position: absolute;
+    top: 0;
+    width: 50%; /* 6 columns */
+  }
+}
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/syntaxhighlighter.html b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/syntaxhighlighter.html new file mode 100644 index 0000000..c6c8734 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/syntaxhighlighter.html @@ -0,0 +1,5921 @@ + + + + + + + +Untitled + + + + + + + + + + + + + + + +
+
+
syntaxhighlighter.js
+
+
/*!
+ * SyntaxHighlighter
+ * https://github.com/syntaxhighlighter/syntaxhighlighter
+ *
+ * SyntaxHighlighter is donationware. If you are using it, please donate.
+ * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
+ *
+ * @version
+ * 4.0.1 (Tue, 05 Jul 2016 16:28:21 GMT)
+ *
+ * @copyright
+ * Copyright (C) 2004-2016 Alex Gorbatchev.
+ *
+ * @license
+ * Dual licensed under the MIT and GPL licenses.
+ */
+/******/ (function(modules) { // webpackBootstrap
+/******/         // The module cache
+/******/         var installedModules = {};
+/******/
+/******/         // The require function
+/******/         function __webpack_require__(moduleId) {
+/******/
+/******/                 // Check if module is in cache
+/******/                 if(installedModules[moduleId])
+/******/                         return installedModules[moduleId].exports;
+/******/
+/******/                 // Create a new module (and put it into the cache)
+/******/                 var module = installedModules[moduleId] = {
+/******/                         exports: {},
+/******/                         id: moduleId,
+/******/                         loaded: false
+/******/                 };
+/******/
+/******/                 // Execute the module function
+/******/                 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/                 // Flag the module as loaded
+/******/                 module.loaded = true;
+/******/
+/******/                 // Return the exports of the module
+/******/                 return module.exports;
+/******/         }
+/******/
+/******/
+/******/         // expose the modules object (__webpack_modules__)
+/******/         __webpack_require__.m = modules;
+/******/
+/******/         // expose the module cache
+/******/         __webpack_require__.c = installedModules;
+/******/
+/******/         // __webpack_public_path__
+/******/         __webpack_require__.p = "";
+/******/
+/******/         // Load entry module and return exports
+/******/         return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+
+        var _core = __webpack_require__(1);
+
+        Object.keys(_core).forEach(function (key) {
+          if (key === "default") return;
+          Object.defineProperty(exports, key, {
+            enumerable: true,
+            get: function get() {
+              return _core[key];
+            }
+          });
+        });
+
+        var _domready = __webpack_require__(52);
+
+        var _domready2 = _interopRequireDefault(_domready);
+
+        var _core2 = _interopRequireDefault(_core);
+
+        var _dasherize = __webpack_require__(53);
+
+        var dasherize = _interopRequireWildcard(_dasherize);
+
+        function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+        // configured through the `--compat` parameter.
+        if (false) {
+          require('./compatibility_layer_v3');
+        }
+
+        (0, _domready2.default)(function () {
+          return _core2.default.highlight(dasherize.object(window.syntaxhighlighterConfig || {}));
+        });
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+        var optsParser = __webpack_require__(2),
+            match = __webpack_require__(5),
+            Renderer = __webpack_require__(9).default,
+            utils = __webpack_require__(10),
+            transformers = __webpack_require__(11),
+            dom = __webpack_require__(17),
+            config = __webpack_require__(18),
+            defaults = __webpack_require__(19),
+            HtmlScript = __webpack_require__(20);
+
+        var sh = {
+          Match: match.Match,
+          Highlighter: __webpack_require__(22),
+
+          config: __webpack_require__(18),
+          regexLib: __webpack_require__(3).commonRegExp,
+
+          /** Internal 'global' variables. */
+          vars: {
+            discoveredBrushes: null,
+            highlighters: {}
+          },
+
+          /** This object is populated by user included external brush files. */
+          brushes: {},
+
+          /**
+           * Finds all elements on the page which should be processes by SyntaxHighlighter.
+           *
+           * @param {Object} globalParams   Optional parameters which override element's
+           *                  parameters. Only used if element is specified.
+           *
+           * @param {Object} element  Optional element to highlight. If none is
+           *              provided, all elements in the current document
+           *              are returned which qualify.
+           *
+           * @return {Array}  Returns list of <code>{ target: DOMElement, params: Object }</code> objects.
+           */
+          findElements: function findElements(globalParams, element) {
+            var elements = element ? [element] : utils.toArray(document.getElementsByTagName(sh.config.tagName)),
+                conf = sh.config,
+                result = [];
+
+            // support for <SCRIPT TYPE="syntaxhighlighter" /> feature
+            elements = elements.concat(dom.getSyntaxHighlighterScriptTags());
+
+            if (elements.length === 0) return result;
+
+            for (var i = 0, l = elements.length; i < l; i++) {
+              var item = {
+                target: elements[i],
+                // local params take precedence over globals
+                params: optsParser.defaults(optsParser.parse(elements[i].className), globalParams)
+              };
+
+              if (item.params['brush'] == null) continue;
+
+              result.push(item);
+            }
+
+            return result;
+          },
+
+          /**
+           * Shorthand to highlight all elements on the page that are marked as
+           * SyntaxHighlighter source code.
+           *
+           * @param {Object} globalParams   Optional parameters which override element's
+           *                  parameters. Only used if element is specified.
+           *
+           * @param {Object} element  Optional element to highlight. If none is
+           *              provided, all elements in the current document
+           *              are highlighted.
+           */
+          highlight: function highlight(globalParams, element) {
+            var elements = sh.findElements(globalParams, element),
+                propertyName = 'innerHTML',
+                brush = null,
+                renderer,
+                conf = sh.config;
+
+            if (elements.length === 0) return;
+
+            for (var i = 0, l = elements.length; i < l; i++) {
+              var element = elements[i],
+                  target = element.target,
+                  params = element.params,
+                  brushName = params.brush,
+                  brush,
+                  matches,
+                  code;
+
+              if (brushName == null) continue;
+
+              brush = findBrush(brushName);
+
+              if (!brush) continue;
+
+              // local params take precedence over defaults
+              params = optsParser.defaults(params || {}, defaults);
+              params = optsParser.defaults(params, config);
+
+              // Instantiate a brush
+              if (params['html-script'] == true || defaults['html-script'] == true) {
+                brush = new HtmlScript(findBrush('xml'), brush);
+                brushName = 'htmlscript';
+              } else {
+                brush = new brush();
+              }
+
+              code = target[propertyName];
+
+              // remove CDATA from <SCRIPT/> tags if it's present
+              if (conf.useScriptTags) code = stripCData(code);
+
+              // Inject title if the attribute is present
+              if ((target.title || '') != '') params.title = target.title;
+
+              params['brush'] = brushName;
+
+              code = transformers(code, params);
+              matches = match.applyRegexList(code, brush.regexList, params);
+              renderer = new Renderer(code, matches, params);
+
+              element = dom.create('div');
+              element.innerHTML = renderer.getHtml();
+
+              // id = utils.guid();
+              // element.id = highlighters.id(id);
+              // highlighters.set(id, element);
+
+              if (params.quickCode) dom.attachEvent(dom.findElement(element, '.code'), 'dblclick', dom.quickCodeHandler);
+
+              // carry over ID
+              if ((target.id || '') != '') element.id = target.id;
+
+              target.parentNode.replaceChild(element, target);
+            }
+          }
+        }; // end of sh
+
+        /**
+         * Displays an alert.
+         * @param {String} str String to display.
+         */
+        function alert(str) {
+          window.alert('SyntaxHighlighter\n\n' + str);
+        };
+
+        /**
+         * Finds a brush by its alias.
+         *
+         * @param {String} alias    Brush alias.
+         * @param {Boolean} showAlert Suppresses the alert if false.
+         * @return {Brush}        Returns bursh constructor if found, null otherwise.
+         */
+        function findBrush(alias, showAlert) {
+          var brushes = sh.vars.discoveredBrushes,
+              result = null;
+
+          if (brushes == null) {
+            brushes = {};
+
+            // Find all brushes
+            for (var brushName in sh.brushes) {
+              var brush = sh.brushes[brushName],
+                  aliases = brush.aliases;
+
+              if (aliases == null) {
+                continue;
+              }
+
+              brush.className = brush.className || brush.aliases[0];
+              brush.brushName = brush.className || brushName.toLowerCase();
+
+              for (var i = 0, l = aliases.length; i < l; i++) {
+                brushes[aliases[i]] = brushName;
+              }
+            }
+
+            sh.vars.discoveredBrushes = brushes;
+          }
+
+          result = sh.brushes[brushes[alias]];
+
+          if (result == null && showAlert) alert(sh.config.strings.noBrush + alias);
+
+          return result;
+        };
+
+        /**
+         * Strips <![CDATA[]]> from <SCRIPT /> content because it should be used
+         * there in most cases for XHTML compliance.
+         * @param {String} original Input code.
+         * @return {String} Returns code without leading <![CDATA[]]> tags.
+         */
+        function stripCData(original) {
+          var left = '<![CDATA[',
+              right = ']]>',
+
+          // for some reason IE inserts some leading blanks here
+          copy = utils.trim(original),
+              changed = false,
+              leftLength = left.length,
+              rightLength = right.length;
+
+          if (copy.indexOf(left) == 0) {
+            copy = copy.substring(leftLength);
+            changed = true;
+          }
+
+          var copyLength = copy.length;
+
+          if (copy.indexOf(right) == copyLength - rightLength) {
+            copy = copy.substring(0, copyLength - rightLength);
+            changed = true;
+          }
+
+          return changed ? copy : original;
+        };
+
+        var brushCounter = 0;
+
+        exports.default = sh;
+        var registerBrush = exports.registerBrush = function registerBrush(brush) {
+          return sh.brushes['brush' + brushCounter++] = brush.default || brush;
+        };
+        var clearRegisteredBrushes = exports.clearRegisteredBrushes = function clearRegisteredBrushes() {
+          sh.brushes = {};
+          brushCounter = 0;
+        };
+
+        /* an EJS hook for `gulp build --brushes` command
+         * */
+
+        registerBrush(__webpack_require__(23));
+
+        registerBrush(__webpack_require__(24));
+
+        registerBrush(__webpack_require__(22));
+
+        registerBrush(__webpack_require__(25));
+
+        registerBrush(__webpack_require__(26));
+
+        registerBrush(__webpack_require__(27));
+
+        registerBrush(__webpack_require__(28));
+
+        registerBrush(__webpack_require__(29));
+
+        registerBrush(__webpack_require__(30));
+
+        registerBrush(__webpack_require__(31));
+
+        registerBrush(__webpack_require__(32));
+
+        registerBrush(__webpack_require__(33));
+
+        registerBrush(__webpack_require__(34));
+
+        registerBrush(__webpack_require__(35));
+
+        registerBrush(__webpack_require__(36));
+
+        registerBrush(__webpack_require__(37));
+
+        registerBrush(__webpack_require__(38));
+
+        registerBrush(__webpack_require__(39));
+
+        registerBrush(__webpack_require__(40));
+
+        registerBrush(__webpack_require__(41));
+
+        registerBrush(__webpack_require__(42));
+
+        registerBrush(__webpack_require__(43));
+
+        registerBrush(__webpack_require__(44));
+
+        registerBrush(__webpack_require__(45));
+
+        registerBrush(__webpack_require__(46));
+
+        registerBrush(__webpack_require__(47));
+
+        registerBrush(__webpack_require__(48));
+
+        registerBrush(__webpack_require__(49));
+
+        registerBrush(__webpack_require__(50));
+
+        registerBrush(__webpack_require__(51));
+
+        /*
+
+         */
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var XRegExp = __webpack_require__(3).XRegExp;
+
+        var BOOLEANS = { 'true': true, 'false': false };
+
+        function camelize(key) {
+          return key.replace(/-(\w+)/g, function (match, word) {
+            return word.charAt(0).toUpperCase() + word.substr(1);
+          });
+        }
+
+        function process(value) {
+          var result = BOOLEANS[value];
+          return result == null ? value : result;
+        }
+
+        module.exports = {
+          defaults: function defaults(target, source) {
+            for (var key in source || {}) {
+              if (!target.hasOwnProperty(key)) target[key] = target[camelize(key)] = source[key];
+            }return target;
+          },
+
+          parse: function parse(str) {
+            var match,
+                key,
+                result = {},
+                arrayRegex = XRegExp("^\\[(?<values>(.*?))\\]$"),
+                pos = 0,
+                regex = XRegExp("(?<name>[\\w-]+)" + "\\s*:\\s*" + "(?<value>" + "[\\w%#-]+|" + // word
+            "\\[.*?\\]|" + // [] array
+            '".*?"|' + // "" string
+            "'.*?'" + // '' string
+            ")\\s*;?", "g");
+
+            while ((match = XRegExp.exec(str, regex, pos)) != null) {
+              var value = match.value.replace(/^['"]|['"]$/g, '') // strip quotes from end of strings
+              ;
+
+              // try to parse array value
+              if (value != null && arrayRegex.test(value)) {
+                var m = XRegExp.exec(value, arrayRegex);
+                value = m.values.length > 0 ? m.values.split(/\s*,\s*/) : [];
+              }
+
+              value = process(value);
+              result[match.name] = result[camelize(match.name)] = value;
+              pos = match.index + match[0].length;
+            }
+
+            return result;
+          }
+        };
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+        exports.commonRegExp = exports.XRegExp = undefined;
+
+        var _xregexp = __webpack_require__(4);
+
+        var _xregexp2 = _interopRequireDefault(_xregexp);
+
+        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+        exports.XRegExp = _xregexp2.default;
+        var commonRegExp = exports.commonRegExp = {
+          multiLineCComments: (0, _xregexp2.default)('/\\*.*?\\*/', 'gs'),
+          singleLineCComments: /\/\/.*$/gm,
+          singleLinePerlComments: /#.*$/gm,
+          doubleQuotedString: /"([^\\"\n]|\\.)*"/g,
+          singleQuotedString: /'([^\\'\n]|\\.)*'/g,
+          multiLineDoubleQuotedString: (0, _xregexp2.default)('"([^\\\\"]|\\\\.)*"', 'gs'),
+          multiLineSingleQuotedString: (0, _xregexp2.default)("'([^\\\\']|\\\\.)*'", 'gs'),
+          xmlComments: (0, _xregexp2.default)('(&lt;|<)!--.*?--(&gt;|>)', 'gs'),
+          url: /\w+:\/\/[\w-.\/?%&=:@;#]*/g,
+          phpScriptTags: { left: /(&lt;|<)\?(?:=|php)?/g, right: /\?(&gt;|>)/g, 'eof': true },
+          aspScriptTags: { left: /(&lt;|<)%=?/g, right: /%(&gt;|>)/g },
+          scriptScriptTags: { left: /(&lt;|<)\s*script.*?(&gt;|>)/gi, right: /(&lt;|<)\/\s*script\s*(&gt;|>)/gi }
+        };
+
+/***/ },
+/* 4 */
+/***/ function(module, exports) {
+
+        /*!
+         * XRegExp 3.1.0-dev
+         * <xregexp.com>
+         * Steven Levithan (c) 2007-2015 MIT License
+         */
+
+        /**
+         * XRegExp provides augmented, extensible regular expressions. You get additional regex syntax and
+         * flags, beyond what browsers support natively. XRegExp is also a regex utility belt with tools to
+         * make your client-side grepping simpler and more powerful, while freeing you from related
+         * cross-browser inconsistencies.
+         */
+
+        'use strict';
+
+        /* ==============================
+         * Private variables
+         * ============================== */
+
+        // Property name used for extended regex instance data
+
+        var REGEX_DATA = 'xregexp';
+        // Optional features that can be installed and uninstalled
+        var features = {
+            astral: false,
+            natives: false
+        };
+        // Native methods to use and restore ('native' is an ES3 reserved keyword)
+        var nativ = {
+            exec: RegExp.prototype.exec,
+            test: RegExp.prototype.test,
+            match: String.prototype.match,
+            replace: String.prototype.replace,
+            split: String.prototype.split
+        };
+        // Storage for fixed/extended native methods
+        var fixed = {};
+        // Storage for regexes cached by `XRegExp.cache`
+        var regexCache = {};
+        // Storage for pattern details cached by the `XRegExp` constructor
+        var patternCache = {};
+        // Storage for regex syntax tokens added internally or by `XRegExp.addToken`
+        var tokens = [];
+        // Token scopes
+        var defaultScope = 'default';
+        var classScope = 'class';
+        // Regexes that match native regex syntax, including octals
+        var nativeTokens = {
+            // Any native multicharacter token in default scope, or any single character
+            'default': /\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9]\d*|x[\dA-Fa-f]{2}|u(?:[\dA-Fa-f]{4}|{[\dA-Fa-f]+})|c[A-Za-z]|[\s\S])|\(\?[:=!]|[?*+]\?|{\d+(?:,\d*)?}\??|[\s\S]/,
+            // Any native multicharacter token in character class scope, or any single character
+            'class': /\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[\dA-Fa-f]{2}|u(?:[\dA-Fa-f]{4}|{[\dA-Fa-f]+})|c[A-Za-z]|[\s\S])|[\s\S]/
+        };
+        // Any backreference or dollar-prefixed character in replacement strings
+        var replacementToken = /\$(?:{([\w$]+)}|(\d\d?|[\s\S]))/g;
+        // Check for correct `exec` handling of nonparticipating capturing groups
+        var correctExecNpcg = nativ.exec.call(/()??/, '')[1] === undefined;
+        // Check for ES6 `u` flag support
+        var hasNativeU = function () {
+            var isSupported = true;
+            try {
+                new RegExp('', 'u');
+            } catch (exception) {
+                isSupported = false;
+            }
+            return isSupported;
+        }();
+        // Check for ES6 `y` flag support
+        var hasNativeY = function () {
+            var isSupported = true;
+            try {
+                new RegExp('', 'y');
+            } catch (exception) {
+                isSupported = false;
+            }
+            return isSupported;
+        }();
+        // Check for ES6 `flags` prop support
+        var hasFlagsProp = /a/.flags !== undefined;
+        // Tracker for known flags, including addon flags
+        var registeredFlags = {
+            g: true,
+            i: true,
+            m: true,
+            u: hasNativeU,
+            y: hasNativeY
+        };
+        // Shortcut to `Object.prototype.toString`
+        var toString = {}.toString;
+
+        /* ==============================
+         * Private functions
+         * ============================== */
+
+        /**
+         * Attaches extended data and `XRegExp.prototype` properties to a regex object.
+         *
+         * @private
+         * @param {RegExp} regex Regex to augment.
+         * @param {Array} captureNames Array with capture names, or `null`.
+         * @param {String} xSource XRegExp pattern used to generate `regex`, or `null` if N/A.
+         * @param {String} xFlags XRegExp flags used to generate `regex`, or `null` if N/A.
+         * @param {Boolean} [isInternalOnly=false] Whether the regex will be used only for internal
+         *   operations, and never exposed to users. For internal-only regexes, we can improve perf by
+         *   skipping some operations like attaching `XRegExp.prototype` properties.
+         * @returns {RegExp} Augmented regex.
+         */
+        function augment(regex, captureNames, xSource, xFlags, isInternalOnly) {
+            var p;
+
+            regex[REGEX_DATA] = {
+                captureNames: captureNames
+            };
+
+            if (isInternalOnly) {
+                return regex;
+            }
+
+            // Can't auto-inherit these since the XRegExp constructor returns a nonprimitive value
+            if (regex.__proto__) {
+                regex.__proto__ = XRegExp.prototype;
+            } else {
+                for (p in XRegExp.prototype) {
+                    // An `XRegExp.prototype.hasOwnProperty(p)` check wouldn't be worth it here, since
+                    // this is performance sensitive, and enumerable `Object.prototype` or
+                    // `RegExp.prototype` extensions exist on `regex.prototype` anyway
+                    regex[p] = XRegExp.prototype[p];
+                }
+            }
+
+            regex[REGEX_DATA].source = xSource;
+            // Emulate the ES6 `flags` prop by ensuring flags are in alphabetical order
+            regex[REGEX_DATA].flags = xFlags ? xFlags.split('').sort().join('') : xFlags;
+
+            return regex;
+        }
+
+        /**
+         * Removes any duplicate characters from the provided string.
+         *
+         * @private
+         * @param {String} str String to remove duplicate characters from.
+         * @returns {String} String with any duplicate characters removed.
+         */
+        function clipDuplicates(str) {
+            return nativ.replace.call(str, /([\s\S])(?=[\s\S]*\1)/g, '');
+        }
+
+        /**
+         * Copies a regex object while preserving extended data and augmenting with `XRegExp.prototype`
+         * properties. The copy has a fresh `lastIndex` property (set to zero). Allows adding and removing
+         * flags g and y while copying the regex.
+         *
+         * @private
+         * @param {RegExp} regex Regex to copy.
+         * @param {Object} [options] Options object with optional properties:
+         *   <li>`addG` {Boolean} Add flag g while copying the regex.
+         *   <li>`addY` {Boolean} Add flag y while copying the regex.
+         *   <li>`removeG` {Boolean} Remove flag g while copying the regex.
+         *   <li>`removeY` {Boolean} Remove flag y while copying the regex.
+         *   <li>`isInternalOnly` {Boolean} Whether the copied regex will be used only for internal
+         *     operations, and never exposed to users. For internal-only regexes, we can improve perf by
+         *     skipping some operations like attaching `XRegExp.prototype` properties.
+         * @returns {RegExp} Copy of the provided regex, possibly with modified flags.
+         */
+        function copyRegex(regex, options) {
+            if (!XRegExp.isRegExp(regex)) {
+                throw new TypeError('Type RegExp expected');
+            }
+
+            var xData = regex[REGEX_DATA] || {},
+                flags = getNativeFlags(regex),
+                flagsToAdd = '',
+                flagsToRemove = '',
+                xregexpSource = null,
+                xregexpFlags = null;
+
+            options = options || {};
+
+            if (options.removeG) {
+                flagsToRemove += 'g';
+            }
+            if (options.removeY) {
+                flagsToRemove += 'y';
+            }
+            if (flagsToRemove) {
+                flags = nativ.replace.call(flags, new RegExp('[' + flagsToRemove + ']+', 'g'), '');
+            }
+
+            if (options.addG) {
+                flagsToAdd += 'g';
+            }
+            if (options.addY) {
+                flagsToAdd += 'y';
+            }
+            if (flagsToAdd) {
+                flags = clipDuplicates(flags + flagsToAdd);
+            }
+
+            if (!options.isInternalOnly) {
+                if (xData.source !== undefined) {
+                    xregexpSource = xData.source;
+                }
+                // null or undefined; don't want to add to `flags` if the previous value was null, since
+                // that indicates we're not tracking original precompilation flags
+                if (xData.flags != null) {
+                    // Flags are only added for non-internal regexes by `XRegExp.globalize`. Flags are
+                    // never removed for non-internal regexes, so don't need to handle it
+                    xregexpFlags = flagsToAdd ? clipDuplicates(xData.flags + flagsToAdd) : xData.flags;
+                }
+            }
+
+            // Augment with `XRegExp.prototype` properties, but use the native `RegExp` constructor to
+            // avoid searching for special tokens. That would be wrong for regexes constructed by
+            // `RegExp`, and unnecessary for regexes constructed by `XRegExp` because the regex has
+            // already undergone the translation to native regex syntax
+            regex = augment(new RegExp(regex.source, flags), hasNamedCapture(regex) ? xData.captureNames.slice(0) : null, xregexpSource, xregexpFlags, options.isInternalOnly);
+
+            return regex;
+        }
+
+        /**
+         * Converts hexadecimal to decimal.
+         *
+         * @private
+         * @param {String} hex
+         * @returns {Number}
+         */
+        function dec(hex) {
+            return parseInt(hex, 16);
+        }
+
+        /**
+         * Returns native `RegExp` flags used by a regex object.
+         *
+         * @private
+         * @param {RegExp} regex Regex to check.
+         * @returns {String} Native flags in use.
+         */
+        function getNativeFlags(regex) {
+            return hasFlagsProp ? regex.flags :
+            // Explicitly using `RegExp.prototype.toString` (rather than e.g. `String` or
+            // concatenation with an empty string) allows this to continue working predictably when
+            // `XRegExp.proptotype.toString` is overriden
+            nativ.exec.call(/\/([a-z]*)$/i, RegExp.prototype.toString.call(regex))[1];
+        }
+
+        /**
+         * Determines whether a regex has extended instance data used to track capture names.
+         *
+         * @private
+         * @param {RegExp} regex Regex to check.
+         * @returns {Boolean} Whether the regex uses named capture.
+         */
+        function hasNamedCapture(regex) {
+            return !!(regex[REGEX_DATA] && regex[REGEX_DATA].captureNames);
+        }
+
+        /**
+         * Converts decimal to hexadecimal.
+         *
+         * @private
+         * @param {Number|String} dec
+         * @returns {String}
+         */
+        function hex(dec) {
+            return parseInt(dec, 10).toString(16);
+        }
+
+        /**
+         * Returns the first index at which a given value can be found in an array.
+         *
+         * @private
+         * @param {Array} array Array to search.
+         * @param {*} value Value to locate in the array.
+         * @returns {Number} Zero-based index at which the item is found, or -1.
+         */
+        function indexOf(array, value) {
+            var len = array.length,
+                i;
+
+            for (i = 0; i < len; ++i) {
+                if (array[i] === value) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        /**
+         * Determines whether a value is of the specified type, by resolving its internal [[Class]].
+         *
+         * @private
+         * @param {*} value Object to check.
+         * @param {String} type Type to check for, in TitleCase.
+         * @returns {Boolean} Whether the object matches the type.
+         */
+        function isType(value, type) {
+            return toString.call(value) === '[object ' + type + ']';
+        }
+
+        /**
+         * Checks whether the next nonignorable token after the specified position is a quantifier.
+         *
+         * @private
+         * @param {String} pattern Pattern to search within.
+         * @param {Number} pos Index in `pattern` to search at.
+         * @param {String} flags Flags used by the pattern.
+         * @returns {Boolean} Whether the next token is a quantifier.
+         */
+        function isQuantifierNext(pattern, pos, flags) {
+            return nativ.test.call(flags.indexOf('x') > -1 ?
+            // Ignore any leading whitespace, line comments, and inline comments
+            /^(?:\s+|#.*|\(\?#[^)]*\))*(?:[?*+]|{\d+(?:,\d*)?})/ :
+            // Ignore any leading inline comments
+            /^(?:\(\?#[^)]*\))*(?:[?*+]|{\d+(?:,\d*)?})/, pattern.slice(pos));
+        }
+
+        /**
+         * Pads the provided string with as many leading zeros as needed to get to length 4. Used to produce
+         * fixed-length hexadecimal values.
+         *
+         * @private
+         * @param {String} str
+         * @returns {String}
+         */
+        function pad4(str) {
+            while (str.length < 4) {
+                str = '0' + str;
+            }
+            return str;
+        }
+
+        /**
+         * Checks for flag-related errors, and strips/applies flags in a leading mode modifier. Offloads
+         * the flag preparation logic from the `XRegExp` constructor.
+         *
+         * @private
+         * @param {String} pattern Regex pattern, possibly with a leading mode modifier.
+         * @param {String} flags Any combination of flags.
+         * @returns {Object} Object with properties `pattern` and `flags`.
+         */
+        function prepareFlags(pattern, flags) {
+            var i;
+
+            // Recent browsers throw on duplicate flags, so copy this behavior for nonnative flags
+            if (clipDuplicates(flags) !== flags) {
+                throw new SyntaxError('Invalid duplicate regex flag ' + flags);
+            }
+
+            // Strip and apply a leading mode modifier with any combination of flags except g or y
+            pattern = nativ.replace.call(pattern, /^\(\?([\w$]+)\)/, function ($0, $1) {
+                if (nativ.test.call(/[gy]/, $1)) {
+                    throw new SyntaxError('Cannot use flag g or y in mode modifier ' + $0);
+                }
+                // Allow duplicate flags within the mode modifier
+                flags = clipDuplicates(flags + $1);
+                return '';
+            });
+
+            // Throw on unknown native or nonnative flags
+            for (i = 0; i < flags.length; ++i) {
+                if (!registeredFlags[flags.charAt(i)]) {
+                    throw new SyntaxError('Unknown regex flag ' + flags.charAt(i));
+                }
+            }
+
+            return {
+                pattern: pattern,
+                flags: flags
+            };
+        }
+
+        /**
+         * Prepares an options object from the given value.
+         *
+         * @private
+         * @param {String|Object} value Value to convert to an options object.
+         * @returns {Object} Options object.
+         */
+        function prepareOptions(value) {
+            var options = {};
+
+            if (isType(value, 'String')) {
+                XRegExp.forEach(value, /[^\s,]+/, function (match) {
+                    options[match] = true;
+                });
+
+                return options;
+            }
+
+            return value;
+        }
+
+        /**
+         * Registers a flag so it doesn't throw an 'unknown flag' error.
+         *
+         * @private
+         * @param {String} flag Single-character flag to register.
+         */
+        function registerFlag(flag) {
+            if (!/^[\w$]$/.test(flag)) {
+                throw new Error('Flag must be a single character A-Za-z0-9_$');
+            }
+
+            registeredFlags[flag] = true;
+        }
+
+        /**
+         * Runs built-in and custom regex syntax tokens in reverse insertion order at the specified
+         * position, until a match is found.
+         *
+         * @private
+         * @param {String} pattern Original pattern from which an XRegExp object is being built.
+         * @param {String} flags Flags being used to construct the regex.
+         * @param {Number} pos Position to search for tokens within `pattern`.
+         * @param {Number} scope Regex scope to apply: 'default' or 'class'.
+         * @param {Object} context Context object to use for token handler functions.
+         * @returns {Object} Object with properties `matchLength`, `output`, and `reparse`; or `null`.
+         */
+        function runTokens(pattern, flags, pos, scope, context) {
+            var i = tokens.length,
+                leadChar = pattern.charAt(pos),
+                result = null,
+                match,
+                t;
+
+            // Run in reverse insertion order
+            while (i--) {
+                t = tokens[i];
+                if (t.leadChar && t.leadChar !== leadChar || t.scope !== scope && t.scope !== 'all' || t.flag && flags.indexOf(t.flag) === -1) {
+                    continue;
+                }
+
+                match = XRegExp.exec(pattern, t.regex, pos, 'sticky');
+                if (match) {
+                    result = {
+                        matchLength: match[0].length,
+                        output: t.handler.call(context, match, scope, flags),
+                        reparse: t.reparse
+                    };
+                    // Finished with token tests
+                    break;
+                }
+            }
+
+            return result;
+        }
+
+        /**
+         * Enables or disables implicit astral mode opt-in. When enabled, flag A is automatically added to
+         * all new regexes created by XRegExp. This causes an error to be thrown when creating regexes if
+         * the Unicode Base addon is not available, since flag A is registered by that addon.
+         *
+         * @private
+         * @param {Boolean} on `true` to enable; `false` to disable.
+         */
+        function setAstral(on) {
+            features.astral = on;
+        }
+
+        /**
+         * Enables or disables native method overrides.
+         *
+         * @private
+         * @param {Boolean} on `true` to enable; `false` to disable.
+         */
+        function setNatives(on) {
+            RegExp.prototype.exec = (on ? fixed : nativ).exec;
+            RegExp.prototype.test = (on ? fixed : nativ).test;
+            String.prototype.match = (on ? fixed : nativ).match;
+            String.prototype.replace = (on ? fixed : nativ).replace;
+            String.prototype.split = (on ? fixed : nativ).split;
+
+            features.natives = on;
+        }
+
+        /**
+         * Returns the object, or throws an error if it is `null` or `undefined`. This is used to follow
+         * the ES5 abstract operation `ToObject`.
+         *
+         * @private
+         * @param {*} value Object to check and return.
+         * @returns {*} The provided object.
+         */
+        function toObject(value) {
+            // null or undefined
+            if (value == null) {
+                throw new TypeError('Cannot convert null or undefined to object');
+            }
+
+            return value;
+        }
+
+        /* ==============================
+         * Constructor
+         * ============================== */
+
+        /**
+         * Creates an extended regular expression object for matching text with a pattern. Differs from a
+         * native regular expression in that additional syntax and flags are supported. The returned object
+         * is in fact a native `RegExp` and works with all native methods.
+         *
+         * @class XRegExp
+         * @constructor
+         * @param {String|RegExp} pattern Regex pattern string, or an existing regex object to copy.
+         * @param {String} [flags] Any combination of flags.
+         *   Native flags:
+         *     <li>`g` - global
+         *     <li>`i` - ignore case
+         *     <li>`m` - multiline anchors
+         *     <li>`u` - unicode (ES6)
+         *     <li>`y` - sticky (Firefox 3+, ES6)
+         *   Additional XRegExp flags:
+         *     <li>`n` - explicit capture
+         *     <li>`s` - dot matches all (aka singleline)
+         *     <li>`x` - free-spacing and line comments (aka extended)
+         *     <li>`A` - astral (requires the Unicode Base addon)
+         *   Flags cannot be provided when constructing one `RegExp` from another.
+         * @returns {RegExp} Extended regular expression object.
+         * @example
+         *
+         * // With named capture and flag x
+         * XRegExp('(?<year>  [0-9]{4} ) -?  # year  \n\
+         *          (?<month> [0-9]{2} ) -?  # month \n\
+         *          (?<day>   [0-9]{2} )     # day   ', 'x');
+         *
+         * // Providing a regex object copies it. Native regexes are recompiled using native (not XRegExp)
+         * // syntax. Copies maintain extended data, are augmented with `XRegExp.prototype` properties, and
+         * // have fresh `lastIndex` properties (set to zero).
+         * XRegExp(/regex/);
+         */
+        function XRegExp(pattern, flags) {
+            var context = {
+                hasNamedCapture: false,
+                captureNames: []
+            },
+                scope = defaultScope,
+                output = '',
+                pos = 0,
+                result,
+                token,
+                generated,
+                appliedPattern,
+                appliedFlags;
+
+            if (XRegExp.isRegExp(pattern)) {
+                if (flags !== undefined) {
+                    throw new TypeError('Cannot supply flags when copying a RegExp');
+                }
+                return copyRegex(pattern);
+            }
+
+            // Copy the argument behavior of `RegExp`
+            pattern = pattern === undefined ? '' : String(pattern);
+            flags = flags === undefined ? '' : String(flags);
+
+            if (XRegExp.isInstalled('astral') && flags.indexOf('A') === -1) {
+                // This causes an error to be thrown if the Unicode Base addon is not available
+                flags += 'A';
+            }
+
+            if (!patternCache[pattern]) {
+                patternCache[pattern] = {};
+            }
+
+            if (!patternCache[pattern][flags]) {
+                // Check for flag-related errors, and strip/apply flags in a leading mode modifier
+                result = prepareFlags(pattern, flags);
+                appliedPattern = result.pattern;
+                appliedFlags = result.flags;
+
+                // Use XRegExp's tokens to translate the pattern to a native regex pattern.
+                // `appliedPattern.length` may change on each iteration if tokens use `reparse`
+                while (pos < appliedPattern.length) {
+                    do {
+                        // Check for custom tokens at the current position
+                        result = runTokens(appliedPattern, appliedFlags, pos, scope, context);
+                        // If the matched token used the `reparse` option, splice its output into the
+                        // pattern before running tokens again at the same position
+                        if (result && result.reparse) {
+                            appliedPattern = appliedPattern.slice(0, pos) + result.output + appliedPattern.slice(pos + result.matchLength);
+                        }
+                    } while (result && result.reparse);
+
+                    if (result) {
+                        output += result.output;
+                        pos += result.matchLength || 1;
+                    } else {
+                        // Get the native token at the current position
+                        token = XRegExp.exec(appliedPattern, nativeTokens[scope], pos, 'sticky')[0];
+                        output += token;
+                        pos += token.length;
+                        if (token === '[' && scope === defaultScope) {
+                            scope = classScope;
+                        } else if (token === ']' && scope === classScope) {
+                            scope = defaultScope;
+                        }
+                    }
+                }
+
+                patternCache[pattern][flags] = {
+                    // Cleanup token cruft: repeated `(?:)(?:)` and leading/trailing `(?:)`
+                    pattern: nativ.replace.call(output, /\(\?:\)(?:[*+?]|\{\d+(?:,\d*)?})?\??(?=\(\?:\))|^\(\?:\)(?:[*+?]|\{\d+(?:,\d*)?})?\??|\(\?:\)(?:[*+?]|\{\d+(?:,\d*)?})?\??$/g, ''),
+                    // Strip all but native flags
+                    flags: nativ.replace.call(appliedFlags, /[^gimuy]+/g, ''),
+                    // `context.captureNames` has an item for each capturing group, even if unnamed
+                    captures: context.hasNamedCapture ? context.captureNames : null
+                };
+            }
+
+            generated = patternCache[pattern][flags];
+            return augment(new RegExp(generated.pattern, generated.flags), generated.captures, pattern, flags);
+        };
+
+        // Add `RegExp.prototype` to the prototype chain
+        XRegExp.prototype = new RegExp();
+
+        /* ==============================
+         * Public properties
+         * ============================== */
+
+        /**
+         * The XRegExp version number as a string containing three dot-separated parts. For example,
+         * '2.0.0-beta-3'.
+         *
+         * @static
+         * @memberOf XRegExp
+         * @type String
+         */
+        XRegExp.version = '3.1.0-dev';
+
+        /* ==============================
+         * Public methods
+         * ============================== */
+
+        /**
+         * Extends XRegExp syntax and allows custom flags. This is used internally and can be used to
+         * create XRegExp addons. If more than one token can match the same string, the last added wins.
+         *
+         * @memberOf XRegExp
+         * @param {RegExp} regex Regex object that matches the new token.
+         * @param {Function} handler Function that returns a new pattern string (using native regex syntax)
+         *   to replace the matched token within all future XRegExp regexes. Has access to persistent
+         *   properties of the regex being built, through `this`. Invoked with three arguments:
+         *   <li>The match array, with named backreference properties.
+         *   <li>The regex scope where the match was found: 'default' or 'class'.
+         *   <li>The flags used by the regex, including any flags in a leading mode modifier.
+         *   The handler function becomes part of the XRegExp construction process, so be careful not to
+         *   construct XRegExps within the function or you will trigger infinite recursion.
+         * @param {Object} [options] Options object with optional properties:
+         *   <li>`scope` {String} Scope where the token applies: 'default', 'class', or 'all'.
+         *   <li>`flag` {String} Single-character flag that triggers the token. This also registers the
+         *     flag, which prevents XRegExp from throwing an 'unknown flag' error when the flag is used.
+         *   <li>`optionalFlags` {String} Any custom flags checked for within the token `handler` that are
+         *     not required to trigger the token. This registers the flags, to prevent XRegExp from
+         *     throwing an 'unknown flag' error when any of the flags are used.
+         *   <li>`reparse` {Boolean} Whether the `handler` function's output should not be treated as
+         *     final, and instead be reparseable by other tokens (including the current token). Allows
+         *     token chaining or deferring.
+         *   <li>`leadChar` {String} Single character that occurs at the beginning of any successful match
+         *     of the token (not always applicable). This doesn't change the behavior of the token unless
+         *     you provide an erroneous value. However, providing it can increase the token's performance
+         *     since the token can be skipped at any positions where this character doesn't appear.
+         * @example
+         *
+         * // Basic usage: Add \a for the ALERT control code
+         * XRegExp.addToken(
+         *   /\\a/,
+         *   function() {return '\\x07';},
+         *   {scope: 'all'}
+         * );
+         * XRegExp('\\a[\\a-\\n]+').test('\x07\n\x07'); // -> true
+         *
+         * // Add the U (ungreedy) flag from PCRE and RE2, which reverses greedy and lazy quantifiers.
+         * // Since `scope` is not specified, it uses 'default' (i.e., transformations apply outside of
+         * // character classes only)
+         * XRegExp.addToken(
+         *   /([?*+]|{\d+(?:,\d*)?})(\??)/,
+         *   function(match) {return match[1] + (match[2] ? '' : '?');},
+         *   {flag: 'U'}
+         * );
+         * XRegExp('a+', 'U').exec('aaa')[0]; // -> 'a'
+         * XRegExp('a+?', 'U').exec('aaa')[0]; // -> 'aaa'
+         */
+        XRegExp.addToken = function (regex, handler, options) {
+            options = options || {};
+            var optionalFlags = options.optionalFlags,
+                i;
+
+            if (options.flag) {
+                registerFlag(options.flag);
+            }
+
+            if (optionalFlags) {
+                optionalFlags = nativ.split.call(optionalFlags, '');
+                for (i = 0; i < optionalFlags.length; ++i) {
+                    registerFlag(optionalFlags[i]);
+                }
+            }
+
+            // Add to the private list of syntax tokens
+            tokens.push({
+                regex: copyRegex(regex, {
+                    addG: true,
+                    addY: hasNativeY,
+                    isInternalOnly: true
+                }),
+                handler: handler,
+                scope: options.scope || defaultScope,
+                flag: options.flag,
+                reparse: options.reparse,
+                leadChar: options.leadChar
+            });
+
+            // Reset the pattern cache used by the `XRegExp` constructor, since the same pattern and
+            // flags might now produce different results
+            XRegExp.cache.flush('patterns');
+        };
+
+        /**
+         * Caches and returns the result of calling `XRegExp(pattern, flags)`. On any subsequent call with
+         * the same pattern and flag combination, the cached copy of the regex is returned.
+         *
+         * @memberOf XRegExp
+         * @param {String} pattern Regex pattern string.
+         * @param {String} [flags] Any combination of XRegExp flags.
+         * @returns {RegExp} Cached XRegExp object.
+         * @example
+         *
+         * while (match = XRegExp.cache('.', 'gs').exec(str)) {
+         *   // The regex is compiled once only
+         * }
+         */
+        XRegExp.cache = function (pattern, flags) {
+            if (!regexCache[pattern]) {
+                regexCache[pattern] = {};
+            }
+            return regexCache[pattern][flags] || (regexCache[pattern][flags] = XRegExp(pattern, flags));
+        };
+
+        // Intentionally undocumented
+        XRegExp.cache.flush = function (cacheName) {
+            if (cacheName === 'patterns') {
+                // Flush the pattern cache used by the `XRegExp` constructor
+                patternCache = {};
+            } else {
+                // Flush the regex cache populated by `XRegExp.cache`
+                regexCache = {};
+            }
+        };
+
+        /**
+         * Escapes any regular expression metacharacters, for use when matching literal strings. The result
+         * can safely be used at any point within a regex that uses any flags.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to escape.
+         * @returns {String} String with regex metacharacters escaped.
+         * @example
+         *
+         * XRegExp.escape('Escaped? <.>');
+         * // -> 'Escaped\?\ <\.>'
+         */
+        XRegExp.escape = function (str) {
+            return nativ.replace.call(toObject(str), /[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+        };
+
+        /**
+         * Executes a regex search in a specified string. Returns a match array or `null`. If the provided
+         * regex uses named capture, named backreference properties are included on the match array.
+         * Optional `pos` and `sticky` arguments specify the search start position, and whether the match
+         * must start at the specified position only. The `lastIndex` property of the provided regex is not
+         * used, but is updated for compatibility. Also fixes browser bugs compared to the native
+         * `RegExp.prototype.exec` and can be used reliably cross-browser.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to search.
+         * @param {RegExp} regex Regex to search with.
+         * @param {Number} [pos=0] Zero-based index at which to start the search.
+         * @param {Boolean|String} [sticky=false] Whether the match must start at the specified position
+         *   only. The string `'sticky'` is accepted as an alternative to `true`.
+         * @returns {Array} Match array with named backreference properties, or `null`.
+         * @example
+         *
+         * // Basic use, with named backreference
+         * var match = XRegExp.exec('U+2620', XRegExp('U\\+(?<hex>[0-9A-F]{4})'));
+         * match.hex; // -> '2620'
+         *
+         * // With pos and sticky, in a loop
+         * var pos = 2, result = [], match;
+         * while (match = XRegExp.exec('<1><2><3><4>5<6>', /<(\d)>/, pos, 'sticky')) {
+         *   result.push(match[1]);
+         *   pos = match.index + match[0].length;
+         * }
+         * // result -> ['2', '3', '4']
+         */
+        XRegExp.exec = function (str, regex, pos, sticky) {
+            var cacheKey = 'g',
+                addY = false,
+                match,
+                r2;
+
+            addY = hasNativeY && !!(sticky || regex.sticky && sticky !== false);
+            if (addY) {
+                cacheKey += 'y';
+            }
+
+            regex[REGEX_DATA] = regex[REGEX_DATA] || {};
+
+            // Shares cached copies with `XRegExp.match`/`replace`
+            r2 = regex[REGEX_DATA][cacheKey] || (regex[REGEX_DATA][cacheKey] = copyRegex(regex, {
+                addG: true,
+                addY: addY,
+                removeY: sticky === false,
+                isInternalOnly: true
+            }));
+
+            r2.lastIndex = pos = pos || 0;
+
+            // Fixed `exec` required for `lastIndex` fix, named backreferences, etc.
+            match = fixed.exec.call(r2, str);
+
+            if (sticky && match && match.index !== pos) {
+                match = null;
+            }
+
+            if (regex.global) {
+                regex.lastIndex = match ? r2.lastIndex : 0;
+            }
+
+            return match;
+        };
+
+        /**
+         * Executes a provided function once per regex match. Searches always start at the beginning of the
+         * string and continue until the end, regardless of the state of the regex's `global` property and
+         * initial `lastIndex`.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to search.
+         * @param {RegExp} regex Regex to search with.
+         * @param {Function} callback Function to execute for each match. Invoked with four arguments:
+         *   <li>The match array, with named backreference properties.
+         *   <li>The zero-based match index.
+         *   <li>The string being traversed.
+         *   <li>The regex object being used to traverse the string.
+         * @example
+         *
+         * // Extracts every other digit from a string
+         * var evens = [];
+         * XRegExp.forEach('1a2345', /\d/, function(match, i) {
+         *   if (i % 2) evens.push(+match[0]);
+         * });
+         * // evens -> [2, 4]
+         */
+        XRegExp.forEach = function (str, regex, callback) {
+            var pos = 0,
+                i = -1,
+                match;
+
+            while (match = XRegExp.exec(str, regex, pos)) {
+                // Because `regex` is provided to `callback`, the function could use the deprecated/
+                // nonstandard `RegExp.prototype.compile` to mutate the regex. However, since
+                // `XRegExp.exec` doesn't use `lastIndex` to set the search position, this can't lead
+                // to an infinite loop, at least. Actually, because of the way `XRegExp.exec` caches
+                // globalized versions of regexes, mutating the regex will not have any effect on the
+                // iteration or matched strings, which is a nice side effect that brings extra safety
+                callback(match, ++i, str, regex);
+
+                pos = match.index + (match[0].length || 1);
+            }
+        };
+
+        /**
+         * Copies a regex object and adds flag `g`. The copy maintains extended data, is augmented with
+         * `XRegExp.prototype` properties, and has a fresh `lastIndex` property (set to zero). Native
+         * regexes are not recompiled using XRegExp syntax.
+         *
+         * @memberOf XRegExp
+         * @param {RegExp} regex Regex to globalize.
+         * @returns {RegExp} Copy of the provided regex with flag `g` added.
+         * @example
+         *
+         * var globalCopy = XRegExp.globalize(/regex/);
+         * globalCopy.global; // -> true
+         */
+        XRegExp.globalize = function (regex) {
+            return copyRegex(regex, { addG: true });
+        };
+
+        /**
+         * Installs optional features according to the specified options. Can be undone using
+         * {@link #XRegExp.uninstall}.
+         *
+         * @memberOf XRegExp
+         * @param {Object|String} options Options object or string.
+         * @example
+         *
+         * // With an options object
+         * XRegExp.install({
+         *   // Enables support for astral code points in Unicode addons (implicitly sets flag A)
+         *   astral: true,
+         *
+         *   // Overrides native regex methods with fixed/extended versions that support named
+         *   // backreferences and fix numerous cross-browser bugs
+         *   natives: true
+         * });
+         *
+         * // With an options string
+         * XRegExp.install('astral natives');
+         */
+        XRegExp.install = function (options) {
+            options = prepareOptions(options);
+
+            if (!features.astral && options.astral) {
+                setAstral(true);
+            }
+
+            if (!features.natives && options.natives) {
+                setNatives(true);
+            }
+        };
+
+        /**
+         * Checks whether an individual optional feature is installed.
+         *
+         * @memberOf XRegExp
+         * @param {String} feature Name of the feature to check. One of:
+         *   <li>`natives`
+         *   <li>`astral`
+         * @returns {Boolean} Whether the feature is installed.
+         * @example
+         *
+         * XRegExp.isInstalled('natives');
+         */
+        XRegExp.isInstalled = function (feature) {
+            return !!features[feature];
+        };
+
+        /**
+         * Returns `true` if an object is a regex; `false` if it isn't. This works correctly for regexes
+         * created in another frame, when `instanceof` and `constructor` checks would fail.
+         *
+         * @memberOf XRegExp
+         * @param {*} value Object to check.
+         * @returns {Boolean} Whether the object is a `RegExp` object.
+         * @example
+         *
+         * XRegExp.isRegExp('string'); // -> false
+         * XRegExp.isRegExp(/regex/i); // -> true
+         * XRegExp.isRegExp(RegExp('^', 'm')); // -> true
+         * XRegExp.isRegExp(XRegExp('(?s).')); // -> true
+         */
+        XRegExp.isRegExp = function (value) {
+            return toString.call(value) === '[object RegExp]';
+            //return isType(value, 'RegExp');
+        };
+
+        /**
+         * Returns the first matched string, or in global mode, an array containing all matched strings.
+         * This is essentially a more convenient re-implementation of `String.prototype.match` that gives
+         * the result types you actually want (string instead of `exec`-style array in match-first mode,
+         * and an empty array instead of `null` when no matches are found in match-all mode). It also lets
+         * you override flag g and ignore `lastIndex`, and fixes browser bugs.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to search.
+         * @param {RegExp} regex Regex to search with.
+         * @param {String} [scope='one'] Use 'one' to return the first match as a string. Use 'all' to
+         *   return an array of all matched strings. If not explicitly specified and `regex` uses flag g,
+         *   `scope` is 'all'.
+         * @returns {String|Array} In match-first mode: First match as a string, or `null`. In match-all
+         *   mode: Array of all matched strings, or an empty array.
+         * @example
+         *
+         * // Match first
+         * XRegExp.match('abc', /\w/); // -> 'a'
+         * XRegExp.match('abc', /\w/g, 'one'); // -> 'a'
+         * XRegExp.match('abc', /x/g, 'one'); // -> null
+         *
+         * // Match all
+         * XRegExp.match('abc', /\w/g); // -> ['a', 'b', 'c']
+         * XRegExp.match('abc', /\w/, 'all'); // -> ['a', 'b', 'c']
+         * XRegExp.match('abc', /x/, 'all'); // -> []
+         */
+        XRegExp.match = function (str, regex, scope) {
+            var global = regex.global && scope !== 'one' || scope === 'all',
+                cacheKey = (global ? 'g' : '') + (regex.sticky ? 'y' : '') || 'noGY',
+                result,
+                r2;
+
+            regex[REGEX_DATA] = regex[REGEX_DATA] || {};
+
+            // Shares cached copies with `XRegExp.exec`/`replace`
+            r2 = regex[REGEX_DATA][cacheKey] || (regex[REGEX_DATA][cacheKey] = copyRegex(regex, {
+                addG: !!global,
+                addY: !!regex.sticky,
+                removeG: scope === 'one',
+                isInternalOnly: true
+            }));
+
+            result = nativ.match.call(toObject(str), r2);
+
+            if (regex.global) {
+                regex.lastIndex = scope === 'one' && result ?
+                // Can't use `r2.lastIndex` since `r2` is nonglobal in this case
+                result.index + result[0].length : 0;
+            }
+
+            return global ? result || [] : result && result[0];
+        };
+
+        /**
+         * Retrieves the matches from searching a string using a chain of regexes that successively search
+         * within previous matches. The provided `chain` array can contain regexes and or objects with
+         * `regex` and `backref` properties. When a backreference is specified, the named or numbered
+         * backreference is passed forward to the next regex or returned.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to search.
+         * @param {Array} chain Regexes that each search for matches within preceding results.
+         * @returns {Array} Matches by the last regex in the chain, or an empty array.
+         * @example
+         *
+         * // Basic usage; matches numbers within <b> tags
+         * XRegExp.matchChain('1 <b>2</b> 3 <b>4 a 56</b>', [
+         *   XRegExp('(?is)<b>.*?</b>'),
+         *   /\d+/
+         * ]);
+         * // -> ['2', '4', '56']
+         *
+         * // Passing forward and returning specific backreferences
+         * html = '<a href="http://xregexp.com/api/">XRegExp</a>\
+         *         <a href="http://www.google.com/">Google</a>';
+         * XRegExp.matchChain(html, [
+         *   {regex: /<a href="([^"]+)">/i, backref: 1},
+         *   {regex: XRegExp('(?i)^https?://(?<domain>[^/?#]+)'), backref: 'domain'}
+         * ]);
+         * // -> ['xregexp.com', 'www.google.com']
+         */
+        XRegExp.matchChain = function (str, chain) {
+            return function recurseChain(values, level) {
+                var item = chain[level].regex ? chain[level] : { regex: chain[level] },
+                    matches = [],
+                    addMatch = function addMatch(match) {
+                    if (item.backref) {
+                        /* Safari 4.0.5 (but not 5.0.5+) inappropriately uses sparse arrays to hold
+                         * the `undefined`s for backreferences to nonparticipating capturing
+                         * groups. In such cases, a `hasOwnProperty` or `in` check on its own would
+                         * inappropriately throw the exception, so also check if the backreference
+                         * is a number that is within the bounds of the array.
+                         */
+                        if (!(match.hasOwnProperty(item.backref) || +item.backref < match.length)) {
+                            throw new ReferenceError('Backreference to undefined group: ' + item.backref);
+                        }
+
+                        matches.push(match[item.backref] || '');
+                    } else {
+                        matches.push(match[0]);
+                    }
+                },
+                    i;
+
+                for (i = 0; i < values.length; ++i) {
+                    XRegExp.forEach(values[i], item.regex, addMatch);
+                }
+
+                return level === chain.length - 1 || !matches.length ? matches : recurseChain(matches, level + 1);
+            }([str], 0);
+        };
+
+        /**
+         * Returns a new string with one or all matches of a pattern replaced. The pattern can be a string
+         * or regex, and the replacement can be a string or a function to be called for each match. To
+         * perform a global search and replace, use the optional `scope` argument or include flag g if using
+         * a regex. Replacement strings can use `${n}` for named and numbered backreferences. Replacement
+         * functions can use named backreferences via `arguments[0].name`. Also fixes browser bugs compared
+         * to the native `String.prototype.replace` and can be used reliably cross-browser.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to search.
+         * @param {RegExp|String} search Search pattern to be replaced.
+         * @param {String|Function} replacement Replacement string or a function invoked to create it.
+         *   Replacement strings can include special replacement syntax:
+         *     <li>$$ - Inserts a literal $ character.
+         *     <li>$&, $0 - Inserts the matched substring.
+         *     <li>$` - Inserts the string that precedes the matched substring (left context).
+         *     <li>$' - Inserts the string that follows the matched substring (right context).
+         *     <li>$n, $nn - Where n/nn are digits referencing an existent capturing group, inserts
+         *       backreference n/nn.
+         *     <li>${n} - Where n is a name or any number of digits that reference an existent capturing
+         *       group, inserts backreference n.
+         *   Replacement functions are invoked with three or more arguments:
+         *     <li>The matched substring (corresponds to $& above). Named backreferences are accessible as
+         *       properties of this first argument.
+         *     <li>0..n arguments, one for each backreference (corresponding to $1, $2, etc. above).
+         *     <li>The zero-based index of the match within the total search string.
+         *     <li>The total string being searched.
+         * @param {String} [scope='one'] Use 'one' to replace the first match only, or 'all'. If not
+         *   explicitly specified and using a regex with flag g, `scope` is 'all'.
+         * @returns {String} New string with one or all matches replaced.
+         * @example
+         *
+         * // Regex search, using named backreferences in replacement string
+         * var name = XRegExp('(?<first>\\w+) (?<last>\\w+)');
+         * XRegExp.replace('John Smith', name, '${last}, ${first}');
+         * // -> 'Smith, John'
+         *
+         * // Regex search, using named backreferences in replacement function
+         * XRegExp.replace('John Smith', name, function(match) {
+         *   return match.last + ', ' + match.first;
+         * });
+         * // -> 'Smith, John'
+         *
+         * // String search, with replace-all
+         * XRegExp.replace('RegExp builds RegExps', 'RegExp', 'XRegExp', 'all');
+         * // -> 'XRegExp builds XRegExps'
+         */
+        XRegExp.replace = function (str, search, replacement, scope) {
+            var isRegex = XRegExp.isRegExp(search),
+                global = search.global && scope !== 'one' || scope === 'all',
+                cacheKey = (global ? 'g' : '') + (search.sticky ? 'y' : '') || 'noGY',
+                s2 = search,
+                result;
+
+            if (isRegex) {
+                search[REGEX_DATA] = search[REGEX_DATA] || {};
+
+                // Shares cached copies with `XRegExp.exec`/`match`. Since a copy is used, `search`'s
+                // `lastIndex` isn't updated *during* replacement iterations
+                s2 = search[REGEX_DATA][cacheKey] || (search[REGEX_DATA][cacheKey] = copyRegex(search, {
+                    addG: !!global,
+                    addY: !!search.sticky,
+                    removeG: scope === 'one',
+                    isInternalOnly: true
+                }));
+            } else if (global) {
+                s2 = new RegExp(XRegExp.escape(String(search)), 'g');
+            }
+
+            // Fixed `replace` required for named backreferences, etc.
+            result = fixed.replace.call(toObject(str), s2, replacement);
+
+            if (isRegex && search.global) {
+                // Fixes IE, Safari bug (last tested IE 9, Safari 5.1)
+                search.lastIndex = 0;
+            }
+
+            return result;
+        };
+
+        /**
+         * Performs batch processing of string replacements. Used like {@link #XRegExp.replace}, but
+         * accepts an array of replacement details. Later replacements operate on the output of earlier
+         * replacements. Replacement details are accepted as an array with a regex or string to search for,
+         * the replacement string or function, and an optional scope of 'one' or 'all'. Uses the XRegExp
+         * replacement text syntax, which supports named backreference properties via `${name}`.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to search.
+         * @param {Array} replacements Array of replacement detail arrays.
+         * @returns {String} New string with all replacements.
+         * @example
+         *
+         * str = XRegExp.replaceEach(str, [
+         *   [XRegExp('(?<name>a)'), 'z${name}'],
+         *   [/b/gi, 'y'],
+         *   [/c/g, 'x', 'one'], // scope 'one' overrides /g
+         *   [/d/, 'w', 'all'],  // scope 'all' overrides lack of /g
+         *   ['e', 'v', 'all'],  // scope 'all' allows replace-all for strings
+         *   [/f/g, function($0) {
+         *     return $0.toUpperCase();
+         *   }]
+         * ]);
+         */
+        XRegExp.replaceEach = function (str, replacements) {
+            var i, r;
+
+            for (i = 0; i < replacements.length; ++i) {
+                r = replacements[i];
+                str = XRegExp.replace(str, r[0], r[1], r[2]);
+            }
+
+            return str;
+        };
+
+        /**
+         * Splits a string into an array of strings using a regex or string separator. Matches of the
+         * separator are not included in the result array. However, if `separator` is a regex that contains
+         * capturing groups, backreferences are spliced into the result each time `separator` is matched.
+         * Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably
+         * cross-browser.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to split.
+         * @param {RegExp|String} separator Regex or string to use for separating the string.
+         * @param {Number} [limit] Maximum number of items to include in the result array.
+         * @returns {Array} Array of substrings.
+         * @example
+         *
+         * // Basic use
+         * XRegExp.split('a b c', ' ');
+         * // -> ['a', 'b', 'c']
+         *
+         * // With limit
+         * XRegExp.split('a b c', ' ', 2);
+         * // -> ['a', 'b']
+         *
+         * // Backreferences in result array
+         * XRegExp.split('..word1..', /([a-z]+)(\d+)/i);
+         * // -> ['..', 'word', '1', '..']
+         */
+        XRegExp.split = function (str, separator, limit) {
+            return fixed.split.call(toObject(str), separator, limit);
+        };
+
+        /**
+         * Executes a regex search in a specified string. Returns `true` or `false`. Optional `pos` and
+         * `sticky` arguments specify the search start position, and whether the match must start at the
+         * specified position only. The `lastIndex` property of the provided regex is not used, but is
+         * updated for compatibility. Also fixes browser bugs compared to the native
+         * `RegExp.prototype.test` and can be used reliably cross-browser.
+         *
+         * @memberOf XRegExp
+         * @param {String} str String to search.
+         * @param {RegExp} regex Regex to search with.
+         * @param {Number} [pos=0] Zero-based index at which to start the search.
+         * @param {Boolean|String} [sticky=false] Whether the match must start at the specified position
+         *   only. The string `'sticky'` is accepted as an alternative to `true`.
+         * @returns {Boolean} Whether the regex matched the provided value.
+         * @example
+         *
+         * // Basic use
+         * XRegExp.test('abc', /c/); // -> true
+         *
+         * // With pos and sticky
+         * XRegExp.test('abc', /c/, 0, 'sticky'); // -> false
+         * XRegExp.test('abc', /c/, 2, 'sticky'); // -> true
+         */
+        XRegExp.test = function (str, regex, pos, sticky) {
+            // Do this the easy way :-)
+            return !!XRegExp.exec(str, regex, pos, sticky);
+        };
+
+        /**
+         * Uninstalls optional features according to the specified options. All optional features start out
+         * uninstalled, so this is used to undo the actions of {@link #XRegExp.install}.
+         *
+         * @memberOf XRegExp
+         * @param {Object|String} options Options object or string.
+         * @example
+         *
+         * // With an options object
+         * XRegExp.uninstall({
+         *   // Disables support for astral code points in Unicode addons
+         *   astral: true,
+         *
+         *   // Restores native regex methods
+         *   natives: true
+         * });
+         *
+         * // With an options string
+         * XRegExp.uninstall('astral natives');
+         */
+        XRegExp.uninstall = function (options) {
+            options = prepareOptions(options);
+
+            if (features.astral && options.astral) {
+                setAstral(false);
+            }
+
+            if (features.natives && options.natives) {
+                setNatives(false);
+            }
+        };
+
+        /**
+         * Returns an XRegExp object that is the union of the given patterns. Patterns can be provided as
+         * regex objects or strings. Metacharacters are escaped in patterns provided as strings.
+         * Backreferences in provided regex objects are automatically renumbered to work correctly within
+         * the larger combined pattern. Native flags used by provided regexes are ignored in favor of the
+         * `flags` argument.
+         *
+         * @memberOf XRegExp
+         * @param {Array} patterns Regexes and strings to combine.
+         * @param {String} [flags] Any combination of XRegExp flags.
+         * @returns {RegExp} Union of the provided regexes and strings.
+         * @example
+         *
+         * XRegExp.union(['a+b*c', /(dogs)\1/, /(cats)\1/], 'i');
+         * // -> /a\+b\*c|(dogs)\1|(cats)\2/i
+         */
+        XRegExp.union = function (patterns, flags) {
+            var parts = /(\()(?!\?)|\\([1-9]\d*)|\\[\s\S]|\[(?:[^\\\]]|\\[\s\S])*]/g,
+                output = [],
+                numCaptures = 0,
+                numPriorCaptures,
+                captureNames,
+                pattern,
+                rewrite = function rewrite(match, paren, backref) {
+                var name = captureNames[numCaptures - numPriorCaptures];
+
+                // Capturing group
+                if (paren) {
+                    ++numCaptures;
+                    // If the current capture has a name, preserve the name
+                    if (name) {
+                        return '(?<' + name + '>';
+                    }
+                    // Backreference
+                } else if (backref) {
+                    // Rewrite the backreference
+                    return '\\' + (+backref + numPriorCaptures);
+                }
+
+                return match;
+            },
+                i;
+
+            if (!(isType(patterns, 'Array') && patterns.length)) {
+                throw new TypeError('Must provide a nonempty array of patterns to merge');
+            }
+
+            for (i = 0; i < patterns.length; ++i) {
+                pattern = patterns[i];
+
+                if (XRegExp.isRegExp(pattern)) {
+                    numPriorCaptures = numCaptures;
+                    captureNames = pattern[REGEX_DATA] && pattern[REGEX_DATA].captureNames || [];
+
+                    // Rewrite backreferences. Passing to XRegExp dies on octals and ensures patterns
+                    // are independently valid; helps keep this simple. Named captures are put back
+                    output.push(nativ.replace.call(XRegExp(pattern.source).source, parts, rewrite));
+                } else {
+                    output.push(XRegExp.escape(pattern));
+                }
+            }
+
+            return XRegExp(output.join('|'), flags);
+        };
+
+        /* ==============================
+         * Fixed/extended native methods
+         * ============================== */
+
+        /**
+         * Adds named capture support (with backreferences returned as `result.name`), and fixes browser
+         * bugs in the native `RegExp.prototype.exec`. Calling `XRegExp.install('natives')` uses this to
+         * override the native method. Use via `XRegExp.exec` without overriding natives.
+         *
+         * @private
+         * @param {String} str String to search.
+         * @returns {Array} Match array with named backreference properties, or `null`.
+         */
+        fixed.exec = function (str) {
+            var origLastIndex = this.lastIndex,
+                match = nativ.exec.apply(this, arguments),
+                name,
+                r2,
+                i;
+
+            if (match) {
+                // Fix browsers whose `exec` methods don't return `undefined` for nonparticipating
+                // capturing groups. This fixes IE 5.5-8, but not IE 9's quirks mode or emulation of
+                // older IEs. IE 9 in standards mode follows the spec
+                if (!correctExecNpcg && match.length > 1 && indexOf(match, '') > -1) {
+                    r2 = copyRegex(this, {
+                        removeG: true,
+                        isInternalOnly: true
+                    });
+                    // Using `str.slice(match.index)` rather than `match[0]` in case lookahead allowed
+                    // matching due to characters outside the match
+                    nativ.replace.call(String(str).slice(match.index), r2, function () {
+                        var len = arguments.length,
+                            i;
+                        // Skip index 0 and the last 2
+                        for (i = 1; i < len - 2; ++i) {
+                            if (arguments[i] === undefined) {
+                                match[i] = undefined;
+                            }
+                        }
+                    });
+                }
+
+                // Attach named capture properties
+                if (this[REGEX_DATA] && this[REGEX_DATA].captureNames) {
+                    // Skip index 0
+                    for (i = 1; i < match.length; ++i) {
+                        name = this[REGEX_DATA].captureNames[i - 1];
+                        if (name) {
+                            match[name] = match[i];
+                        }
+                    }
+                }
+
+                // Fix browsers that increment `lastIndex` after zero-length matches
+                if (this.global && !match[0].length && this.lastIndex > match.index) {
+                    this.lastIndex = match.index;
+                }
+            }
+
+            if (!this.global) {
+                // Fixes IE, Opera bug (last tested IE 9, Opera 11.6)
+                this.lastIndex = origLastIndex;
+            }
+
+            return match;
+        };
+
+        /**
+         * Fixes browser bugs in the native `RegExp.prototype.test`. Calling `XRegExp.install('natives')`
+         * uses this to override the native method.
+         *
+         * @private
+         * @param {String} str String to search.
+         * @returns {Boolean} Whether the regex matched the provided value.
+         */
+        fixed.test = function (str) {
+            // Do this the easy way :-)
+            return !!fixed.exec.call(this, str);
+        };
+
+        /**
+         * Adds named capture support (with backreferences returned as `result.name`), and fixes browser
+         * bugs in the native `String.prototype.match`. Calling `XRegExp.install('natives')` uses this to
+         * override the native method.
+         *
+         * @private
+         * @param {RegExp|*} regex Regex to search with. If not a regex object, it is passed to `RegExp`.
+         * @returns {Array} If `regex` uses flag g, an array of match strings or `null`. Without flag g,
+         *   the result of calling `regex.exec(this)`.
+         */
+        fixed.match = function (regex) {
+            var result;
+
+            if (!XRegExp.isRegExp(regex)) {
+                // Use the native `RegExp` rather than `XRegExp`
+                regex = new RegExp(regex);
+            } else if (regex.global) {
+                result = nativ.match.apply(this, arguments);
+                // Fixes IE bug
+                regex.lastIndex = 0;
+
+                return result;
+            }
+
+            return fixed.exec.call(regex, toObject(this));
+        };
+
+        /**
+         * Adds support for `${n}` tokens for named and numbered backreferences in replacement text, and
+         * provides named backreferences to replacement functions as `arguments[0].name`. Also fixes browser
+         * bugs in replacement text syntax when performing a replacement using a nonregex search value, and
+         * the value of a replacement regex's `lastIndex` property during replacement iterations and upon
+         * completion. Calling `XRegExp.install('natives')` uses this to override the native method. Note
+         * that this doesn't support SpiderMonkey's proprietary third (`flags`) argument. Use via
+         * `XRegExp.replace` without overriding natives.
+         *
+         * @private
+         * @param {RegExp|String} search Search pattern to be replaced.
+         * @param {String|Function} replacement Replacement string or a function invoked to create it.
+         * @returns {String} New string with one or all matches replaced.
+         */
+        fixed.replace = function (search, replacement) {
+            var isRegex = XRegExp.isRegExp(search),
+                origLastIndex,
+                captureNames,
+                result;
+
+            if (isRegex) {
+                if (search[REGEX_DATA]) {
+                    captureNames = search[REGEX_DATA].captureNames;
+                }
+                // Only needed if `search` is nonglobal
+                origLastIndex = search.lastIndex;
+            } else {
+                search += ''; // Type-convert
+            }
+
+            // Don't use `typeof`; some older browsers return 'function' for regex objects
+            if (isType(replacement, 'Function')) {
+                // Stringifying `this` fixes a bug in IE < 9 where the last argument in replacement
+                // functions isn't type-converted to a string
+                result = nativ.replace.call(String(this), search, function () {
+                    var args = arguments,
+                        i;
+                    if (captureNames) {
+                        // Change the `arguments[0]` string primitive to a `String` object that can
+                        // store properties. This really does need to use `String` as a constructor
+                        args[0] = new String(args[0]);
+                        // Store named backreferences on the first argument
+                        for (i = 0; i < captureNames.length; ++i) {
+                            if (captureNames[i]) {
+                                args[0][captureNames[i]] = args[i + 1];
+                            }
+                        }
+                    }
+                    // Update `lastIndex` before calling `replacement`. Fixes IE, Chrome, Firefox,
+                    // Safari bug (last tested IE 9, Chrome 17, Firefox 11, Safari 5.1)
+                    if (isRegex && search.global) {
+                        search.lastIndex = args[args.length - 2] + args[0].length;
+                    }
+                    // ES6 specs the context for replacement functions as `undefined`
+                    return replacement.apply(undefined, args);
+                });
+            } else {
+                // Ensure that the last value of `args` will be a string when given nonstring `this`,
+                // while still throwing on null or undefined context
+                result = nativ.replace.call(this == null ? this : String(this), search, function () {
+                    // Keep this function's `arguments` available through closure
+                    var args = arguments;
+                    return nativ.replace.call(String(replacement), replacementToken, function ($0, $1, $2) {
+                        var n;
+                        // Named or numbered backreference with curly braces
+                        if ($1) {
+                            // XRegExp behavior for `${n}`:
+                            // 1. Backreference to numbered capture, if `n` is an integer. Use `0` for
+                            //    for the entire match. Any number of leading zeros may be used.
+                            // 2. Backreference to named capture `n`, if it exists and is not an
+                            //    integer overridden by numbered capture. In practice, this does not
+                            //    overlap with numbered capture since XRegExp does not allow named
+                            //    capture to use a bare integer as the name.
+                            // 3. If the name or number does not refer to an existing capturing group,
+                            //    it's an error.
+                            n = +$1; // Type-convert; drop leading zeros
+                            if (n <= args.length - 3) {
+                                return args[n] || '';
+                            }
+                            // Groups with the same name is an error, else would need `lastIndexOf`
+                            n = captureNames ? indexOf(captureNames, $1) : -1;
+                            if (n < 0) {
+                                throw new SyntaxError('Backreference to undefined group ' + $0);
+                            }
+                            return args[n + 1] || '';
+                        }
+                        // Else, special variable or numbered backreference without curly braces
+                        if ($2 === '$') {
+                            // $$
+                            return '$';
+                        }
+                        if ($2 === '&' || +$2 === 0) {
+                            // $&, $0 (not followed by 1-9), $00
+                            return args[0];
+                        }
+                        if ($2 === '`') {
+                            // $` (left context)
+                            return args[args.length - 1].slice(0, args[args.length - 2]);
+                        }
+                        if ($2 === "'") {
+                            // $' (right context)
+                            return args[args.length - 1].slice(args[args.length - 2] + args[0].length);
+                        }
+                        // Else, numbered backreference without curly braces
+                        $2 = +$2; // Type-convert; drop leading zero
+                        // XRegExp behavior for `$n` and `$nn`:
+                        // - Backrefs end after 1 or 2 digits. Use `${..}` for more digits.
+                        // - `$1` is an error if no capturing groups.
+                        // - `$10` is an error if less than 10 capturing groups. Use `${1}0` instead.
+                        // - `$01` is `$1` if at least one capturing group, else it's an error.
+                        // - `$0` (not followed by 1-9) and `$00` are the entire match.
+                        // Native behavior, for comparison:
+                        // - Backrefs end after 1 or 2 digits. Cannot reference capturing group 100+.
+                        // - `$1` is a literal `$1` if no capturing groups.
+                        // - `$10` is `$1` followed by a literal `0` if less than 10 capturing groups.
+                        // - `$01` is `$1` if at least one capturing group, else it's a literal `$01`.
+                        // - `$0` is a literal `$0`.
+                        if (!isNaN($2)) {
+                            if ($2 > args.length - 3) {
+                                throw new SyntaxError('Backreference to undefined group ' + $0);
+                            }
+                            return args[$2] || '';
+                        }
+                        // `$` followed by an unsupported char is an error, unlike native JS
+                        throw new SyntaxError('Invalid token ' + $0);
+                    });
+                });
+            }
+
+            if (isRegex) {
+                if (search.global) {
+                    // Fixes IE, Safari bug (last tested IE 9, Safari 5.1)
+                    search.lastIndex = 0;
+                } else {
+                    // Fixes IE, Opera bug (last tested IE 9, Opera 11.6)
+                    search.lastIndex = origLastIndex;
+                }
+            }
+
+            return result;
+        };
+
+        /**
+         * Fixes browser bugs in the native `String.prototype.split`. Calling `XRegExp.install('natives')`
+         * uses this to override the native method. Use via `XRegExp.split` without overriding natives.
+         *
+         * @private
+         * @param {RegExp|String} separator Regex or string to use for separating the string.
+         * @param {Number} [limit] Maximum number of items to include in the result array.
+         * @returns {Array} Array of substrings.
+         */
+        fixed.split = function (separator, limit) {
+            if (!XRegExp.isRegExp(separator)) {
+                // Browsers handle nonregex split correctly, so use the faster native method
+                return nativ.split.apply(this, arguments);
+            }
+
+            var str = String(this),
+                output = [],
+                origLastIndex = separator.lastIndex,
+                lastLastIndex = 0,
+                lastLength;
+
+            // Values for `limit`, per the spec:
+            // If undefined: pow(2,32) - 1
+            // If 0, Infinity, or NaN: 0
+            // If positive number: limit = floor(limit); if (limit >= pow(2,32)) limit -= pow(2,32);
+            // If negative number: pow(2,32) - floor(abs(limit))
+            // If other: Type-convert, then use the above rules
+            // This line fails in very strange ways for some values of `limit` in Opera 10.5-10.63,
+            // unless Opera Dragonfly is open (go figure). It works in at least Opera 9.5-10.1 and 11+
+            limit = (limit === undefined ? -1 : limit) >>> 0;
+
+            XRegExp.forEach(str, separator, function (match) {
+                // This condition is not the same as `if (match[0].length)`
+                if (match.index + match[0].length > lastLastIndex) {
+                    output.push(str.slice(lastLastIndex, match.index));
+                    if (match.length > 1 && match.index < str.length) {
+                        Array.prototype.push.apply(output, match.slice(1));
+                    }
+                    lastLength = match[0].length;
+                    lastLastIndex = match.index + lastLength;
+                }
+            });
+
+            if (lastLastIndex === str.length) {
+                if (!nativ.test.call(separator, '') || lastLength) {
+                    output.push('');
+                }
+            } else {
+                output.push(str.slice(lastLastIndex));
+            }
+
+            separator.lastIndex = origLastIndex;
+            return output.length > limit ? output.slice(0, limit) : output;
+        };
+
+        /* ==============================
+         * Built-in syntax/flag tokens
+         * ============================== */
+
+        /*
+         * Letter escapes that natively match literal characters: `\a`, `\A`, etc. These should be
+         * SyntaxErrors but are allowed in web reality. XRegExp makes them errors for cross-browser
+         * consistency and to reserve their syntax, but lets them be superseded by addons.
+         */
+        XRegExp.addToken(/\\([ABCE-RTUVXYZaeg-mopqyz]|c(?![A-Za-z])|u(?![\dA-Fa-f]{4}|{[\dA-Fa-f]+})|x(?![\dA-Fa-f]{2}))/, function (match, scope) {
+            // \B is allowed in default scope only
+            if (match[1] === 'B' && scope === defaultScope) {
+                return match[0];
+            }
+            throw new SyntaxError('Invalid escape ' + match[0]);
+        }, {
+            scope: 'all',
+            leadChar: '\\'
+        });
+
+        /*
+         * Unicode code point escape with curly braces: `\u{N..}`. `N..` is any one or more digit
+         * hexadecimal number from 0-10FFFF, and can include leading zeros. Requires the native ES6 `u` flag
+         * to support code points greater than U+FFFF. Avoids converting code points above U+FFFF to
+         * surrogate pairs (which could be done without flag `u`), since that could lead to broken behavior
+         * if you follow a `\u{N..}` token that references a code point above U+FFFF with a quantifier, or
+         * if you use the same in a character class.
+         */
+        XRegExp.addToken(/\\u{([\dA-Fa-f]+)}/, function (match, scope, flags) {
+            var code = dec(match[1]);
+            if (code > 0x10FFFF) {
+                throw new SyntaxError('Invalid Unicode code point ' + match[0]);
+            }
+            if (code <= 0xFFFF) {
+                // Converting to \uNNNN avoids needing to escape the literal character and keep it
+                // separate from preceding tokens
+                return '\\u' + pad4(hex(code));
+            }
+            // If `code` is between 0xFFFF and 0x10FFFF, require and defer to native handling
+            if (hasNativeU && flags.indexOf('u') > -1) {
+                return match[0];
+            }
+            throw new SyntaxError('Cannot use Unicode code point above \\u{FFFF} without flag u');
+        }, {
+            scope: 'all',
+            leadChar: '\\'
+        });
+
+        /*
+         * Empty character class: `[]` or `[^]`. This fixes a critical cross-browser syntax inconsistency.
+         * Unless this is standardized (per the ES spec), regex syntax can't be accurately parsed because
+         * character class endings can't be determined.
+         */
+        XRegExp.addToken(/\[(\^?)]/, function (match) {
+            // For cross-browser compatibility with ES3, convert [] to \b\B and [^] to [\s\S].
+            // (?!) should work like \b\B, but is unreliable in some versions of Firefox
+            return match[1] ? '[\\s\\S]' : '\\b\\B';
+        }, { leadChar: '[' });
+
+        /*
+         * Comment pattern: `(?# )`. Inline comments are an alternative to the line comments allowed in
+         * free-spacing mode (flag x).
+         */
+        XRegExp.addToken(/\(\?#[^)]*\)/, function (match, scope, flags) {
+            // Keep tokens separated unless the following token is a quantifier
+            return isQuantifierNext(match.input, match.index + match[0].length, flags) ? '' : '(?:)';
+        }, { leadChar: '(' });
+
+        /*
+         * Whitespace and line comments, in free-spacing mode (aka extended mode, flag x) only.
+         */
+        XRegExp.addToken(/\s+|#.*/, function (match, scope, flags) {
+            // Keep tokens separated unless the following token is a quantifier
+            return isQuantifierNext(match.input, match.index + match[0].length, flags) ? '' : '(?:)';
+        }, { flag: 'x' });
+
+        /*
+         * Dot, in dotall mode (aka singleline mode, flag s) only.
+         */
+        XRegExp.addToken(/\./, function () {
+            return '[\\s\\S]';
+        }, {
+            flag: 's',
+            leadChar: '.'
+        });
+
+        /*
+         * Named backreference: `\k<name>`. Backreference names can use the characters A-Z, a-z, 0-9, _,
+         * and $ only. Also allows numbered backreferences as `\k<n>`.
+         */
+        XRegExp.addToken(/\\k<([\w$]+)>/, function (match) {
+            // Groups with the same name is an error, else would need `lastIndexOf`
+            var index = isNaN(match[1]) ? indexOf(this.captureNames, match[1]) + 1 : +match[1],
+                endIndex = match.index + match[0].length;
+            if (!index || index > this.captureNames.length) {
+                throw new SyntaxError('Backreference to undefined group ' + match[0]);
+            }
+            // Keep backreferences separate from subsequent literal numbers
+            return '\\' + index + (endIndex === match.input.length || isNaN(match.input.charAt(endIndex)) ? '' : '(?:)');
+        }, { leadChar: '\\' });
+
+        /*
+         * Numbered backreference or octal, plus any following digits: `\0`, `\11`, etc. Octals except `\0`
+         * not followed by 0-9 and backreferences to unopened capture groups throw an error. Other matches
+         * are returned unaltered. IE < 9 doesn't support backreferences above `\99` in regex syntax.
+         */
+        XRegExp.addToken(/\\(\d+)/, function (match, scope) {
+            if (!(scope === defaultScope && /^[1-9]/.test(match[1]) && +match[1] <= this.captureNames.length) && match[1] !== '0') {
+                throw new SyntaxError('Cannot use octal escape or backreference to undefined group ' + match[0]);
+            }
+            return match[0];
+        }, {
+            scope: 'all',
+            leadChar: '\\'
+        });
+
+        /*
+         * Named capturing group; match the opening delimiter only: `(?<name>`. Capture names can use the
+         * characters A-Z, a-z, 0-9, _, and $ only. Names can't be integers. Supports Python-style
+         * `(?P<name>` as an alternate syntax to avoid issues in some older versions of Opera which natively
+         * supported the Python-style syntax. Otherwise, XRegExp might treat numbered backreferences to
+         * Python-style named capture as octals.
+         */
+        XRegExp.addToken(/\(\?P?<([\w$]+)>/, function (match) {
+            // Disallow bare integers as names because named backreferences are added to match
+            // arrays and therefore numeric properties may lead to incorrect lookups
+            if (!isNaN(match[1])) {
+                throw new SyntaxError('Cannot use integer as capture name ' + match[0]);
+            }
+            if (match[1] === 'length' || match[1] === '__proto__') {
+                throw new SyntaxError('Cannot use reserved word as capture name ' + match[0]);
+            }
+            if (indexOf(this.captureNames, match[1]) > -1) {
+                throw new SyntaxError('Cannot use same name for multiple groups ' + match[0]);
+            }
+            this.captureNames.push(match[1]);
+            this.hasNamedCapture = true;
+            return '(';
+        }, { leadChar: '(' });
+
+        /*
+         * Capturing group; match the opening parenthesis only. Required for support of named capturing
+         * groups. Also adds explicit capture mode (flag n).
+         */
+        XRegExp.addToken(/\((?!\?)/, function (match, scope, flags) {
+            if (flags.indexOf('n') > -1) {
+                return '(?:';
+            }
+            this.captureNames.push(null);
+            return '(';
+        }, {
+            optionalFlags: 'n',
+            leadChar: '('
+        });
+
+        /* ==============================
+         * Expose XRegExp
+         * ============================== */
+
+        module.exports = XRegExp;
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+
+        var _match = __webpack_require__(6);
+
+        Object.keys(_match).forEach(function (key) {
+          if (key === "default") return;
+          Object.defineProperty(exports, key, {
+            enumerable: true,
+            get: function get() {
+              return _match[key];
+            }
+          });
+        });
+
+        var _applyRegexList = __webpack_require__(7);
+
+        Object.keys(_applyRegexList).forEach(function (key) {
+          if (key === "default") return;
+          Object.defineProperty(exports, key, {
+            enumerable: true,
+            get: function get() {
+              return _applyRegexList[key];
+            }
+          });
+        });
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+        "use strict";
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+
+        var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+        function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+        var Match = exports.Match = function () {
+          function Match(value, index, css) {
+            _classCallCheck(this, Match);
+
+            this.value = value;
+            this.index = index;
+            this.length = value.length;
+            this.css = css;
+            this.brushName = null;
+          }
+
+          _createClass(Match, [{
+            key: "toString",
+            value: function toString() {
+              return this.value;
+            }
+          }]);
+
+          return Match;
+        }();
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+
+        var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
+
+        exports.applyRegexList = applyRegexList;
+
+        var _matches = __webpack_require__(8);
+
+        /**
+         * Applies all regular expression to the code and stores all found
+         * matches in the `this.matches` array.
+         */
+        function applyRegexList(code, regexList) {
+          var result = [];
+
+          regexList = regexList || [];
+
+          for (var i = 0, l = regexList.length; i < l; i++) {
+            // BUG: length returns len+1 for array if methods added to prototype chain (oising@gmail.com)
+            if (_typeof(regexList[i]) === 'object') result = result.concat((0, _matches.find)(code, regexList[i]));
+          }
+
+          result = (0, _matches.sort)(result);
+          result = (0, _matches.removeNested)(result);
+          result = (0, _matches.compact)(result);
+
+          return result;
+        }
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+        exports.find = find;
+        exports.sort = sort;
+        exports.compact = compact;
+        exports.removeNested = removeNested;
+
+        var _match = __webpack_require__(6);
+
+        var _syntaxhighlighterRegex = __webpack_require__(3);
+
+        /**
+         * Executes given regular expression on provided code and returns all matches that are found.
+         *
+         * @param {String} code    Code to execute regular expression on.
+         * @param {Object} regex   Regular expression item info from `regexList` collection.
+         * @return {Array}         Returns a list of Match objects.
+         */
+        function find(code, regexInfo) {
+          function defaultAdd(match, regexInfo) {
+            return match[0];
+          };
+
+          var index = 0,
+              match = null,
+              matches = [],
+              process = regexInfo.func ? regexInfo.func : defaultAdd,
+              pos = 0;
+
+          while (match = _syntaxhighlighterRegex.XRegExp.exec(code, regexInfo.regex, pos)) {
+            var resultMatch = process(match, regexInfo);
+
+            if (typeof resultMatch === 'string') resultMatch = [new _match.Match(resultMatch, match.index, regexInfo.css)];
+
+            matches = matches.concat(resultMatch);
+            pos = match.index + match[0].length;
+          }
+
+          return matches;
+        };
+
+        /**
+         * Sorts matches by index position and then by length.
+         */
+        function sort(matches) {
+          function sortMatchesCallback(m1, m2) {
+            // sort matches by index first
+            if (m1.index < m2.index) return -1;else if (m1.index > m2.index) return 1;else {
+              // if index is the same, sort by length
+              if (m1.length < m2.length) return -1;else if (m1.length > m2.length) return 1;
+            }
+
+            return 0;
+          }
+
+          return matches.sort(sortMatchesCallback);
+        }
+
+        function compact(matches) {
+          var result = [],
+              i,
+              l;
+
+          for (i = 0, l = matches.length; i < l; i++) {
+            if (matches[i]) result.push(matches[i]);
+          }return result;
+        }
+
+        /**
+         * Checks to see if any of the matches are inside of other matches.
+         * This process would get rid of highligted strings inside comments,
+         * keywords inside strings and so on.
+         */
+        function removeNested(matches) {
+          // Optimized by Jose Prado (http://joseprado.com)
+          for (var i = 0, l = matches.length; i < l; i++) {
+            if (matches[i] === null) continue;
+
+            var itemI = matches[i],
+                itemIEndPos = itemI.index + itemI.length;
+
+            for (var j = i + 1, l = matches.length; j < l && matches[i] !== null; j++) {
+              var itemJ = matches[j];
+
+              if (itemJ === null) continue;else if (itemJ.index > itemIEndPos) break;else if (itemJ.index == itemI.index && itemJ.length > itemI.length) matches[i] = null;else if (itemJ.index >= itemI.index && itemJ.index < itemIEndPos) matches[j] = null;
+            }
+          }
+
+          return matches;
+        }
+
+/***/ },
+/* 9 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+        exports.default = Renderer;
+        /**
+         * Pads number with zeros until it's length is the same as given length.
+         *
+         * @param {Number} number Number to pad.
+         * @param {Number} length Max string length with.
+         * @return {String}     Returns a string padded with proper amount of '0'.
+         */
+        function padNumber(number, length) {
+          var result = number.toString();
+
+          while (result.length < length) {
+            result = '0' + result;
+          }return result;
+        };
+
+        function getLines(str) {
+          return str.split(/\r?\n/);
+        }
+
+        function getLinesToHighlight(opts) {
+          var results = {},
+              linesToHighlight,
+              l,
+              i;
+
+          linesToHighlight = opts.highlight || [];
+
+          if (typeof linesToHighlight.push !== 'function') linesToHighlight = [linesToHighlight];
+
+          for (i = 0, l = linesToHighlight.length; i < l; i++) {
+            results[linesToHighlight[i]] = true;
+          }return results;
+        }
+
+        function Renderer(code, matches, opts) {
+          var _this = this;
+
+          _this.opts = opts;
+          _this.code = code;
+          _this.matches = matches;
+          _this.lines = getLines(code);
+          _this.linesToHighlight = getLinesToHighlight(opts);
+        }
+
+        Renderer.prototype = {
+          /**
+           * Wraps each line of the string into <code/> tag with given style applied to it.
+           *
+           * @param {String} str   Input string.
+           * @param {String} css   Style name to apply to the string.
+           * @return {String}      Returns input string with each line surrounded by <span/> tag.
+           */
+          wrapLinesWithCode: function wrapLinesWithCode(str, css) {
+            if (str == null || str.length == 0 || str == '\n' || css == null) return str;
+
+            var _this = this,
+                results = [],
+                lines,
+                line,
+                spaces,
+                i,
+                l;
+
+            str = str.replace(/</g, '&lt;');
+
+            // Replace two or more sequential spaces with &nbsp; leaving last space untouched.
+            str = str.replace(/ {2,}/g, function (m) {
+              spaces = '';
+
+              for (i = 0, l = m.length; i < l - 1; i++) {
+                spaces += _this.opts.space;
+              }return spaces + ' ';
+            });
+
+            lines = getLines(str);
+
+            // Split each line and apply <span class="...">...</span> to them so that leading spaces aren't included.
+            for (i = 0, l = lines.length; i < l; i++) {
+              line = lines[i];
+              spaces = '';
+
+              if (line.length > 0) {
+                line = line.replace(/^(&nbsp;| )+/, function (s) {
+                  spaces = s;
+                  return '';
+                });
+
+                line = line.length === 0 ? spaces : spaces + '<code class="' + css + '">' + line + '</code>';
+              }
+
+              results.push(line);
+            }
+
+            return results.join('\n');
+          },
+
+          /**
+           * Turns all URLs in the code into <a/> tags.
+           * @param {String} code Input code.
+           * @return {String} Returns code with </a> tags.
+           */
+          processUrls: function processUrls(code) {
+            var gt = /(.*)((&gt;|&lt;).*)/,
+                url = /\w+:\/\/[\w-.\/?%&=:@;#]*/g;
+
+            return code.replace(url, function (m) {
+              var suffix = '',
+                  match = null;
+
+              // We include &lt; and &gt; in the URL for the common cases like <http://google.com>
+              // The problem is that they get transformed into &lt;http://google.com&gt;
+              // Where as &gt; easily looks like part of the URL string.
+
+              if (match = gt.exec(m)) {
+                m = match[1];
+                suffix = match[2];
+              }
+
+              return '<a href="' + m + '">' + m + '</a>' + suffix;
+            });
+          },
+
+          /**
+           * Creates an array containing integer line numbers starting from the 'first-line' param.
+           * @return {Array} Returns array of integers.
+           */
+          figureOutLineNumbers: function figureOutLineNumbers(code) {
+            var lineNumbers = [],
+                lines = this.lines,
+                firstLine = parseInt(this.opts.firstLine || 1),
+                i,
+                l;
+
+            for (i = 0, l = lines.length; i < l; i++) {
+              lineNumbers.push(i + firstLine);
+            }return lineNumbers;
+          },
+
+          /**
+           * Generates HTML markup for a single line of code while determining alternating line style.
+           * @param {Integer} lineNumber  Line number.
+           * @param {String} code Line  HTML markup.
+           * @return {String}       Returns HTML markup.
+           */
+          wrapLine: function wrapLine(lineIndex, lineNumber, lineHtml) {
+            var classes = ['line', 'number' + lineNumber, 'index' + lineIndex, 'alt' + (lineNumber % 2 == 0 ? 1 : 2).toString()];
+
+            if (this.linesToHighlight[lineNumber]) classes.push('highlighted');
+
+            if (lineNumber == 0) classes.push('break');
+
+            return '<div class="' + classes.join(' ') + '">' + lineHtml + '</div>';
+          },
+
+          /**
+           * Generates HTML markup for line number column.
+           * @param {String} code     Complete code HTML markup.
+           * @param {Array} lineNumbers Calculated line numbers.
+           * @return {String}       Returns HTML markup.
+           */
+          renderLineNumbers: function renderLineNumbers(code, lineNumbers) {
+            var _this = this,
+                opts = _this.opts,
+                html = '',
+                count = _this.lines.length,
+                firstLine = parseInt(opts.firstLine || 1),
+                pad = opts.padLineNumbers,
+                lineNumber,
+                i;
+
+            if (pad == true) pad = (firstLine + count - 1).toString().length;else if (isNaN(pad) == true) pad = 0;
+
+            for (i = 0; i < count; i++) {
+              lineNumber = lineNumbers ? lineNumbers[i] : firstLine + i;
+              code = lineNumber == 0 ? opts.space : padNumber(lineNumber, pad);
+              html += _this.wrapLine(i, lineNumber, code);
+            }
+
+            return html;
+          },
+
+          /**
+           * Splits block of text into individual DIV lines.
+           * @param {String} code     Code to highlight.
+           * @param {Array} lineNumbers Calculated line numbers.
+           * @return {String}       Returns highlighted code in HTML form.
+           */
+          getCodeLinesHtml: function getCodeLinesHtml(html, lineNumbers) {
+            // html = utils.trim(html);
+
+            var _this = this,
+                opts = _this.opts,
+                lines = getLines(html),
+                padLength = opts.padLineNumbers,
+                firstLine = parseInt(opts.firstLine || 1),
+                brushName = opts.brush,
+                html = '';
+
+            for (var i = 0, l = lines.length; i < l; i++) {
+              var line = lines[i],
+                  indent = /^(&nbsp;|\s)+/.exec(line),
+                  spaces = null,
+                  lineNumber = lineNumbers ? lineNumbers[i] : firstLine + i;
+              ;
+
+              if (indent != null) {
+                spaces = indent[0].toString();
+                line = line.substr(spaces.length);
+                spaces = spaces.replace(' ', opts.space);
+              }
+
+              // line = utils.trim(line);
+
+              if (line.length == 0) line = opts.space;
+
+              html += _this.wrapLine(i, lineNumber, (spaces != null ? '<code class="' + brushName + ' spaces">' + spaces + '</code>' : '') + line);
+            }
+
+            return html;
+          },
+
+          /**
+           * Returns HTML for the table title or empty string if title is null.
+           */
+          getTitleHtml: function getTitleHtml(title) {
+            return title ? '<caption>' + title + '</caption>' : '';
+          },
+
+          /**
+           * Finds all matches in the source code.
+           * @param {String} code   Source code to process matches in.
+           * @param {Array} matches Discovered regex matches.
+           * @return {String} Returns formatted HTML with processed mathes.
+           */
+          getMatchesHtml: function getMatchesHtml(code, matches) {
+            function getBrushNameCss(match) {
+              var result = match ? match.brushName || brushName : brushName;
+              return result ? result + ' ' : '';
+            };
+
+            var _this = this,
+                pos = 0,
+                result = '',
+                brushName = _this.opts.brush || '',
+                match,
+                matchBrushName,
+                i,
+                l;
+
+            // Finally, go through the final list of matches and pull the all
+            // together adding everything in between that isn't a match.
+            for (i = 0, l = matches.length; i < l; i++) {
+              match = matches[i];
+
+              if (match === null || match.length === 0) continue;
+
+              matchBrushName = getBrushNameCss(match);
+
+              result += _this.wrapLinesWithCode(code.substr(pos, match.index - pos), matchBrushName + 'plain') + _this.wrapLinesWithCode(match.value, matchBrushName + match.css);
+
+              pos = match.index + match.length + (match.offset || 0);
+            }
+
+            // don't forget to add whatever's remaining in the string
+            result += _this.wrapLinesWithCode(code.substr(pos), getBrushNameCss() + 'plain');
+
+            return result;
+          },
+
+          /**
+           * Generates HTML markup for the whole syntax highlighter.
+           * @param {String} code Source code.
+           * @return {String} Returns HTML markup.
+           */
+          getHtml: function getHtml() {
+            var _this = this,
+                opts = _this.opts,
+                code = _this.code,
+                matches = _this.matches,
+                classes = ['syntaxhighlighter'],
+                lineNumbers,
+                gutter,
+                html;
+
+            if (opts.collapse === true) classes.push('collapsed');
+
+            gutter = opts.gutter !== false;
+
+            if (!gutter) classes.push('nogutter');
+
+            // add custom user style name
+            classes.push(opts.className);
+
+            // add brush alias to the class name for custom CSS
+            classes.push(opts.brush);
+
+            if (gutter) lineNumbers = _this.figureOutLineNumbers(code);
+
+            // processes found matches into the html
+            html = _this.getMatchesHtml(code, matches);
+
+            // finally, split all lines so that they wrap well
+            html = _this.getCodeLinesHtml(html, lineNumbers);
+
+            // finally, process the links
+            if (opts.autoLinks) html = _this.processUrls(html);
+
+            html = '\n      <div class="' + classes.join(' ') + '">\n        <table border="0" cellpadding="0" cellspacing="0">\n          ' + _this.getTitleHtml(opts.title) + '\n          <tbody>\n            <tr>\n              ' + (gutter ? '<td class="gutter">' + _this.renderLineNumbers(code) + '</td>' : '') + '\n              <td class="code">\n                <div class="container">' + html + '</div>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    ';
+
+            return html;
+          }
+        };
+
+/***/ },
+/* 10 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        /**
+         * Splits block of text into lines.
+         * @param {String} block Block of text.
+         * @return {Array} Returns array of lines.
+         */
+        function splitLines(block) {
+          return block.split(/\r?\n/);
+        }
+
+        /**
+         * Executes a callback on each line and replaces each line with result from the callback.
+         * @param {Object} str      Input string.
+         * @param {Object} callback   Callback function taking one string argument and returning a string.
+         */
+        function eachLine(str, callback) {
+          var lines = splitLines(str);
+
+          for (var i = 0, l = lines.length; i < l; i++) {
+            lines[i] = callback(lines[i], i);
+          }return lines.join('\n');
+        }
+
+        /**
+         * Generates a unique element ID.
+         */
+        function guid(prefix) {
+          return (prefix || '') + Math.round(Math.random() * 1000000).toString();
+        }
+
+        /**
+         * Merges two objects. Values from obj2 override values in obj1.
+         * Function is NOT recursive and works only for one dimensional objects.
+         * @param {Object} obj1 First object.
+         * @param {Object} obj2 Second object.
+         * @return {Object} Returns combination of both objects.
+         */
+        function merge(obj1, obj2) {
+          var result = {},
+              name;
+
+          for (name in obj1) {
+            result[name] = obj1[name];
+          }for (name in obj2) {
+            result[name] = obj2[name];
+          }return result;
+        }
+
+        /**
+         * Removes all white space at the begining and end of a string.
+         *
+         * @param {String} str   String to trim.
+         * @return {String}      Returns string without leading and following white space characters.
+         */
+        function trim(str) {
+          return str.replace(/^\s+|\s+$/g, '');
+        }
+
+        /**
+         * Converts the source to array object. Mostly used for function arguments and
+         * lists returned by getElementsByTagName() which aren't Array objects.
+         * @param {List} source Source list.
+         * @return {Array} Returns array.
+         */
+        function toArray(source) {
+          return Array.prototype.slice.apply(source);
+        }
+
+        /**
+         * Attempts to convert string to boolean.
+         * @param {String} value Input string.
+         * @return {Boolean} Returns true if input was "true", false if input was "false" and value otherwise.
+         */
+        function toBoolean(value) {
+          var result = { "true": true, "false": false }[value];
+          return result == null ? value : result;
+        }
+
+        module.exports = {
+          splitLines: splitLines,
+          eachLine: eachLine,
+          guid: guid,
+          merge: merge,
+          trim: trim,
+          toArray: toArray,
+          toBoolean: toBoolean
+        };
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var trim = __webpack_require__(12),
+            bloggerMode = __webpack_require__(13),
+            stripBrs = __webpack_require__(14),
+            unindenter = __webpack_require__(15),
+            retabber = __webpack_require__(16);
+
+        module.exports = function (code, opts) {
+          code = trim(code, opts);
+          code = bloggerMode(code, opts);
+          code = stripBrs(code, opts);
+          code = unindenter.unindent(code, opts);
+
+          var tabSize = opts['tab-size'];
+          code = opts['smart-tabs'] === true ? retabber.smart(code, tabSize) : retabber.regular(code, tabSize);
+
+          return code;
+        };
+
+/***/ },
+/* 12 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        module.exports = function (code, opts) {
+           return code
+           // This is a special trim which only removes first and last empty lines
+           // and doesn't affect valid leading space on the first line.
+           .replace(/^[ ]*[\n]+|[\n]*[ ]*$/g, '')
+
+           // IE lets these buggers through
+           .replace(/\r/g, ' ');
+        };
+
+/***/ },
+/* 13 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        module.exports = function (code, opts) {
+          var br = /<br\s*\/?>|&lt;br\s*\/?&gt;/gi;
+
+          if (opts['bloggerMode'] === true) code = code.replace(br, '\n');
+
+          return code;
+        };
+
+/***/ },
+/* 14 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        module.exports = function (code, opts) {
+          var br = /<br\s*\/?>|&lt;br\s*\/?&gt;/gi;
+
+          if (opts['stripBrs'] === true) code = code.replace(br, '');
+
+          return code;
+        };
+
+/***/ },
+/* 15 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        function isEmpty(str) {
+          return (/^\s*$/.test(str)
+          );
+        }
+
+        module.exports = {
+          unindent: function unindent(code) {
+            var lines = code.split(/\r?\n/),
+                regex = /^\s*/,
+                min = 1000,
+                line,
+                matches,
+                i,
+                l;
+
+            // go through every line and check for common number of indents
+            for (i = 0, l = lines.length; i < l && min > 0; i++) {
+              line = lines[i];
+
+              if (isEmpty(line)) continue;
+
+              matches = regex.exec(line);
+
+              // In the event that just one line doesn't have leading white space
+              // we can't unindent anything, so bail completely.
+              if (matches == null) return code;
+
+              min = Math.min(matches[0].length, min);
+            }
+
+            // trim minimum common number of white space from the begining of every line
+            if (min > 0) for (i = 0, l = lines.length; i < l; i++) {
+              if (!isEmpty(lines[i])) lines[i] = lines[i].substr(min);
+            }return lines.join('\n');
+          }
+        };
+
+/***/ },
+/* 16 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        var spaces = '';
+
+        // Create a string with 1000 spaces to copy spaces from...
+        // It's assumed that there would be no indentation longer than that.
+        for (var i = 0; i < 50; i++) {
+          spaces += '                    ';
+        } // 20 spaces * 50
+
+        // This function inserts specified amount of spaces in the string
+        // where a tab is while removing that given tab.
+        function insertSpaces(line, pos, count) {
+          return line.substr(0, pos) + spaces.substr(0, count) + line.substr(pos + 1, line.length) // pos + 1 will get rid of the tab
+          ;
+        }
+
+        module.exports = {
+          smart: function smart(code, tabSize) {
+            var lines = code.split(/\r?\n/),
+                tab = '\t',
+                line,
+                pos,
+                i,
+                l;
+
+            // Go through all the lines and do the 'smart tabs' magic.
+            for (i = 0, l = lines.length; i < l; i++) {
+              line = lines[i];
+
+              if (line.indexOf(tab) === -1) continue;
+
+              pos = 0;
+
+              while ((pos = line.indexOf(tab)) !== -1) {
+                // This is pretty much all there is to the 'smart tabs' logic.
+                // Based on the position within the line and size of a tab,
+                // calculate the amount of spaces we need to insert.
+                line = insertSpaces(line, pos, tabSize - pos % tabSize);
+              }
+
+              lines[i] = line;
+            }
+
+            return lines.join('\n');
+          },
+
+          regular: function regular(code, tabSize) {
+            return code.replace(/\t/g, spaces.substr(0, tabSize));
+          }
+        };
+
+/***/ },
+/* 17 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        /**
+         * Finds all &lt;SCRIPT TYPE="text/syntaxhighlighter" /> elementss.
+         * Finds both "text/syntaxhighlighter" and "syntaxhighlighter"
+         * ...in order to make W3C validator happy with subtype and backwardscompatible without subtype
+         * @return {Array} Returns array of all found SyntaxHighlighter tags.
+         */
+        function getSyntaxHighlighterScriptTags() {
+          var tags = document.getElementsByTagName('script'),
+              result = [];
+
+          for (var i = 0; i < tags.length; i++) {
+            if (tags[i].type == 'text/syntaxhighlighter' || tags[i].type == 'syntaxhighlighter') result.push(tags[i]);
+          }return result;
+        };
+
+        /**
+         * Checks if target DOM elements has specified CSS class.
+         * @param {DOMElement} target Target DOM element to check.
+         * @param {String} className Name of the CSS class to check for.
+         * @return {Boolean} Returns true if class name is present, false otherwise.
+         */
+        function hasClass(target, className) {
+          return target.className.indexOf(className) != -1;
+        }
+
+        /**
+         * Adds CSS class name to the target DOM element.
+         * @param {DOMElement} target Target DOM element.
+         * @param {String} className New CSS class to add.
+         */
+        function addClass(target, className) {
+          if (!hasClass(target, className)) target.className += ' ' + className;
+        }
+
+        /**
+         * Removes CSS class name from the target DOM element.
+         * @param {DOMElement} target Target DOM element.
+         * @param {String} className CSS class to remove.
+         */
+        function removeClass(target, className) {
+          target.className = target.className.replace(className, '');
+        }
+
+        /**
+         * Adds event handler to the target object.
+         * @param {Object} obj    Target object.
+         * @param {String} type   Name of the event.
+         * @param {Function} func Handling function.
+         */
+        function attachEvent(obj, type, func, scope) {
+          function handler(e) {
+            e = e || window.event;
+
+            if (!e.target) {
+              e.target = e.srcElement;
+              e.preventDefault = function () {
+                this.returnValue = false;
+              };
+            }
+
+            func.call(scope || window, e);
+          };
+
+          if (obj.attachEvent) {
+            obj.attachEvent('on' + type, handler);
+          } else {
+            obj.addEventListener(type, handler, false);
+          }
+        }
+
+        /**
+         * Looks for a child or parent node which has specified classname.
+         * Equivalent to jQuery's $(container).find(".className")
+         * @param {Element} target Target element.
+         * @param {String} search Class name or node name to look for.
+         * @param {Boolean} reverse If set to true, will go up the node tree instead of down.
+         * @return {Element} Returns found child or parent element on null.
+         */
+        function findElement(target, search, reverse /* optional */) {
+          if (target == null) return null;
+
+          var nodes = reverse != true ? target.childNodes : [target.parentNode],
+              propertyToFind = { '#': 'id', '.': 'className' }[search.substr(0, 1)] || 'nodeName',
+              expectedValue,
+              found;
+
+          expectedValue = propertyToFind != 'nodeName' ? search.substr(1) : search.toUpperCase();
+
+          // main return of the found node
+          if ((target[propertyToFind] || '').indexOf(expectedValue) != -1) return target;
+
+          for (var i = 0, l = nodes.length; nodes && i < l && found == null; i++) {
+            found = findElement(nodes[i], search, reverse);
+          }return found;
+        }
+
+        /**
+         * Looks for a parent node which has specified classname.
+         * This is an alias to <code>findElement(container, className, true)</code>.
+         * @param {Element} target Target element.
+         * @param {String} className Class name to look for.
+         * @return {Element} Returns found parent element on null.
+         */
+        function findParentElement(target, className) {
+          return findElement(target, className, true);
+        }
+
+        /**
+         * Opens up a centered popup window.
+         * @param {String} url    URL to open in the window.
+         * @param {String} name   Popup name.
+         * @param {int} width   Popup width.
+         * @param {int} height    Popup height.
+         * @param {String} options  window.open() options.
+         * @return {Window}     Returns window instance.
+         */
+        function popup(url, name, width, height, options) {
+          var x = (screen.width - width) / 2,
+              y = (screen.height - height) / 2;
+
+          options += ', left=' + x + ', top=' + y + ', width=' + width + ', height=' + height;
+          options = options.replace(/^,/, '');
+
+          var win = window.open(url, name, options);
+          win.focus();
+          return win;
+        }
+
+        function getElementsByTagName(name) {
+          return document.getElementsByTagName(name);
+        }
+
+        /**
+         * Finds all elements on the page which could be processes by SyntaxHighlighter.
+         */
+        function findElementsToHighlight(opts) {
+          var elements = getElementsByTagName(opts['tagName']),
+              scripts,
+              i;
+
+          // support for <SCRIPT TYPE="syntaxhighlighter" /> feature
+          if (opts['useScriptTags']) {
+            scripts = getElementsByTagName('script');
+
+            for (i = 0; i < scripts.length; i++) {
+              if (scripts[i].type.match(/^(text\/)?syntaxhighlighter$/)) elements.push(scripts[i]);
+            }
+          }
+
+          return elements;
+        }
+
+        function create(name) {
+          return document.createElement(name);
+        }
+
+        /**
+         * Quick code mouse double click handler.
+         */
+        function quickCodeHandler(e) {
+          var target = e.target,
+              highlighterDiv = findParentElement(target, '.syntaxhighlighter'),
+              container = findParentElement(target, '.container'),
+              textarea = document.createElement('textarea'),
+              highlighter;
+
+          if (!container || !highlighterDiv || findElement(container, 'textarea')) return;
+
+          //highlighter = highlighters.get(highlighterDiv.id);
+
+          // add source class name
+          addClass(highlighterDiv, 'source');
+
+          // Have to go over each line and grab it's text, can't just do it on the
+          // container because Firefox loses all \n where as Webkit doesn't.
+          var lines = container.childNodes,
+              code = [];
+
+          for (var i = 0, l = lines.length; i < l; i++) {
+            code.push(lines[i].innerText || lines[i].textContent);
+          } // using \r instead of \r or \r\n makes this work equally well on IE, FF and Webkit
+          code = code.join('\r');
+
+          // For Webkit browsers, replace nbsp with a breaking space
+          code = code.replace(/\u00a0/g, " ");
+
+          // inject <textarea/> tag
+          textarea.readOnly = true; // https://github.com/syntaxhighlighter/syntaxhighlighter/pull/329
+          textarea.appendChild(document.createTextNode(code));
+          container.appendChild(textarea);
+
+          // preselect all text
+          textarea.focus();
+          textarea.select();
+
+          // set up handler for lost focus
+          attachEvent(textarea, 'blur', function (e) {
+            textarea.parentNode.removeChild(textarea);
+            removeClass(highlighterDiv, 'source');
+          });
+        };
+
+        module.exports = {
+          quickCodeHandler: quickCodeHandler,
+          create: create,
+          popup: popup,
+          hasClass: hasClass,
+          addClass: addClass,
+          removeClass: removeClass,
+          attachEvent: attachEvent,
+          findElement: findElement,
+          findParentElement: findParentElement,
+          getSyntaxHighlighterScriptTags: getSyntaxHighlighterScriptTags,
+          findElementsToHighlight: findElementsToHighlight
+        };
+
+/***/ },
+/* 18 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        module.exports = {
+          space: '&nbsp;',
+
+          /** Enables use of <SCRIPT type="syntaxhighlighter" /> tags. */
+          useScriptTags: true,
+
+          /** Blogger mode flag. */
+          bloggerMode: false,
+
+          stripBrs: false,
+
+          /** Name of the tag that SyntaxHighlighter will automatically look for. */
+          tagName: 'pre'
+        };
+
+/***/ },
+/* 19 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        module.exports = {
+          /** Additional CSS class names to be added to highlighter elements. */
+          'class-name': '',
+
+          /** First line number. */
+          'first-line': 1,
+
+          /**
+           * Pads line numbers. Possible values are:
+           *
+           *   false - don't pad line numbers.
+           *   true  - automaticaly pad numbers with minimum required number of leading zeroes.
+           *   [int] - length up to which pad line numbers.
+           */
+          'pad-line-numbers': false,
+
+          /** Lines to highlight. */
+          'highlight': null,
+
+          /** Title to be displayed above the code block. */
+          'title': null,
+
+          /** Enables or disables smart tabs. */
+          'smart-tabs': true,
+
+          /** Gets or sets tab size. */
+          'tab-size': 4,
+
+          /** Enables or disables gutter. */
+          'gutter': true,
+
+          /** Enables quick code copy and paste from double click. */
+          'quick-code': true,
+
+          /** Forces code view to be collapsed. */
+          'collapse': false,
+
+          /** Enables or disables automatic links. */
+          'auto-links': true,
+
+          'unindent': true,
+
+          'html-script': false
+        };
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+        /* WEBPACK VAR INJECTION */(function(process) {'use strict';
+
+        var applyRegexList = __webpack_require__(5).applyRegexList;
+
+        function HtmlScript(BrushXML, brushClass) {
+          var scriptBrush,
+              xmlBrush = new BrushXML();
+
+          if (brushClass == null) return;
+
+          scriptBrush = new brushClass();
+
+          if (scriptBrush.htmlScript == null) throw new Error('Brush wasn\'t configured for html-script option: ' + brushClass.brushName);
+
+          xmlBrush.regexList.push({ regex: scriptBrush.htmlScript.code, func: process });
+
+          this.regexList = xmlBrush.regexList;
+
+          function offsetMatches(matches, offset) {
+            for (var j = 0, l = matches.length; j < l; j++) {
+              matches[j].index += offset;
+            }
+          }
+
+          function process(match, info) {
+            var code = match.code,
+                results = [],
+                regexList = scriptBrush.regexList,
+                offset = match.index + match.left.length,
+                htmlScript = scriptBrush.htmlScript,
+                matches;
+
+            function add(matches) {
+              results = results.concat(matches);
+            }
+
+            matches = applyRegexList(code, regexList);
+            offsetMatches(matches, offset);
+            add(matches);
+
+            // add left script bracket
+            if (htmlScript.left != null && match.left != null) {
+              matches = applyRegexList(match.left, [htmlScript.left]);
+              offsetMatches(matches, match.index);
+              add(matches);
+            }
+
+            // add right script bracket
+            if (htmlScript.right != null && match.right != null) {
+              matches = applyRegexList(match.right, [htmlScript.right]);
+              offsetMatches(matches, match.index + match[0].lastIndexOf(match.right));
+              add(matches);
+            }
+
+            for (var j = 0, l = results.length; j < l; j++) {
+              results[j].brushName = brushClass.brushName;
+            }return results;
+          }
+        };
+
+        module.exports = HtmlScript;
+        /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(21)))
+
+/***/ },
+/* 21 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        // shim for using process in browser
+
+        var process = module.exports = {};
+
+        // cached from whatever global is present so that test runners that stub it
+        // don't break things.  But we need to wrap it in a try catch in case it is
+        // wrapped in strict mode code which doesn't define any globals.  It's inside a
+        // function because try/catches deoptimize in certain engines.
+
+        var cachedSetTimeout;
+        var cachedClearTimeout;
+
+        (function () {
+            try {
+                cachedSetTimeout = setTimeout;
+            } catch (e) {
+                cachedSetTimeout = function cachedSetTimeout() {
+                    throw new Error('setTimeout is not defined');
+                };
+            }
+            try {
+                cachedClearTimeout = clearTimeout;
+            } catch (e) {
+                cachedClearTimeout = function cachedClearTimeout() {
+                    throw new Error('clearTimeout is not defined');
+                };
+            }
+        })();
+        var queue = [];
+        var draining = false;
+        var currentQueue;
+        var queueIndex = -1;
+
+        function cleanUpNextTick() {
+            if (!draining || !currentQueue) {
+                return;
+            }
+            draining = false;
+            if (currentQueue.length) {
+                queue = currentQueue.concat(queue);
+            } else {
+                queueIndex = -1;
+            }
+            if (queue.length) {
+                drainQueue();
+            }
+        }
+
+        function drainQueue() {
+            if (draining) {
+                return;
+            }
+            var timeout = cachedSetTimeout(cleanUpNextTick);
+            draining = true;
+
+            var len = queue.length;
+            while (len) {
+                currentQueue = queue;
+                queue = [];
+                while (++queueIndex < len) {
+                    if (currentQueue) {
+                        currentQueue[queueIndex].run();
+                    }
+                }
+                queueIndex = -1;
+                len = queue.length;
+            }
+            currentQueue = null;
+            draining = false;
+            cachedClearTimeout(timeout);
+        }
+
+        process.nextTick = function (fun) {
+            var args = new Array(arguments.length - 1);
+            if (arguments.length > 1) {
+                for (var i = 1; i < arguments.length; i++) {
+                    args[i - 1] = arguments[i];
+                }
+            }
+            queue.push(new Item(fun, args));
+            if (queue.length === 1 && !draining) {
+                cachedSetTimeout(drainQueue, 0);
+            }
+        };
+
+        // v8 likes predictible objects
+        function Item(fun, array) {
+            this.fun = fun;
+            this.array = array;
+        }
+        Item.prototype.run = function () {
+            this.fun.apply(null, this.array);
+        };
+        process.title = 'browser';
+        process.browser = true;
+        process.env = {};
+        process.argv = [];
+        process.version = ''; // empty string to avoid regexp issues
+        process.versions = {};
+
+        function noop() {}
+
+        process.on = noop;
+        process.addListener = noop;
+        process.once = noop;
+        process.off = noop;
+        process.removeListener = noop;
+        process.removeAllListeners = noop;
+        process.emit = noop;
+
+        process.binding = function (name) {
+            throw new Error('process.binding is not supported');
+        };
+
+        process.cwd = function () {
+            return '/';
+        };
+        process.chdir = function (dir) {
+            throw new Error('process.chdir is not supported');
+        };
+        process.umask = function () {
+            return 0;
+        };
+
+/***/ },
+/* 22 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+        var _syntaxhighlighterHtmlRenderer = __webpack_require__(9);
+
+        var _syntaxhighlighterHtmlRenderer2 = _interopRequireDefault(_syntaxhighlighterHtmlRenderer);
+
+        var _syntaxhighlighterRegex = __webpack_require__(3);
+
+        var _syntaxhighlighterMatch = __webpack_require__(5);
+
+        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+        function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+        module.exports = function () {
+          function BrushBase() {
+            _classCallCheck(this, BrushBase);
+          }
+
+          _createClass(BrushBase, [{
+            key: 'getKeywords',
+
+            /**
+             * Converts space separated list of keywords into a regular expression string.
+             * @param {String} str Space separated keywords.
+             * @return {String} Returns regular expression string.
+             */
+            value: function getKeywords(str) {
+              var results = str.replace(/^\s+|\s+$/g, '').replace(/\s+/g, '|');
+
+              return '\\b(?:' + results + ')\\b';
+            }
+
+            /**
+             * Makes a brush compatible with the `html-script` functionality.
+             * @param {Object} regexGroup Object containing `left` and `right` regular expressions.
+             */
+
+          }, {
+            key: 'forHtmlScript',
+            value: function forHtmlScript(regexGroup) {
+              var regex = { 'end': regexGroup.right.source };
+
+              if (regexGroup.eof) {
+                regex.end = '(?:(?:' + regex.end + ')|$)';
+              }
+
+              this.htmlScript = {
+                left: { regex: regexGroup.left, css: 'script' },
+                right: { regex: regexGroup.right, css: 'script' },
+                code: (0, _syntaxhighlighterRegex.XRegExp)("(?<left>" + regexGroup.left.source + ")" + "(?<code>.*?)" + "(?<right>" + regex.end + ")", "sgi")
+              };
+            }
+          }, {
+            key: 'getHtml',
+            value: function getHtml(code) {
+              var params = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+              var matches = (0, _syntaxhighlighterMatch.applyRegexList)(code, this.regexList);
+              var renderer = new _syntaxhighlighterHtmlRenderer2.default(code, matches, params);
+              return renderer.getHtml();
+            }
+          }]);
+
+          return BrushBase;
+        }();
+
+/***/ },
+/* 23 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // AppleScript brush by David Chambers
+          // http://davidchambersdesign.com/
+          var keywords = 'after before beginning continue copy each end every from return get global in local named of set some that the then times to where whose with without';
+          var ordinals = 'first second third fourth fifth sixth seventh eighth ninth tenth last front back middle';
+          var specials = 'activate add alias ask attachment boolean class constant delete duplicate empty exists id integer list make message modal modified new no pi properties quit real record remove rest result reveal reverse run running save string word yes';
+
+          this.regexList = [{
+            regex: /(--|#).*$/gm,
+            css: 'comments'
+          }, {
+            regex: /\(\*(?:[\s\S]*?\(\*[\s\S]*?\*\))*[\s\S]*?\*\)/gm, // support nested comments
+            css: 'comments'
+          }, {
+            regex: /"[\s\S]*?"/gm,
+            css: 'string'
+          }, {
+            regex: /(?:,|:|¬|'s\b|\(|\)|\{|\}|«|\b\w*»)/g, // operators
+            css: 'color1'
+          }, {
+            regex: /(-)?(\d)+(\.(\d)?)?(E\+(\d)+)?/g, // numbers
+            css: 'color1'
+          }, {
+            regex: /(?:&(amp;|gt;|lt;)?|=|>|<|≥|>=|≤|<=|\*|\+|-|\/|÷|\^)/g,
+            css: 'color2'
+          }, {
+            regex: /\b(?:and|as|div|mod|not|or|return(?!\s&)(ing)?|equals|(is(n't| not)? )?equal( to)?|does(n't| not) equal|(is(n't| not)? )?(greater|less) than( or equal( to)?)?|(comes|does(n't| not) come) (after|before)|is(n't| not)?( in)? (back|front) of|is(n't| not)? behind|is(n't| not)?( (in|contained by))?|does(n't| not) contain|contain(s)?|(start|begin|end)(s)? with|((but|end) )?(consider|ignor)ing|prop(erty)?|(a )?ref(erence)?( to)?|repeat (until|while|with)|((end|exit) )?repeat|((else|end) )?if|else|(end )?(script|tell|try)|(on )?error|(put )?into|(of )?(it|me)|its|my|with (timeout( of)?|transaction)|end (timeout|transaction))\b/g,
+            css: 'keyword'
+          }, {
+            regex: /\b\d+(st|nd|rd|th)\b/g, // ordinals
+            css: 'keyword'
+          }, {
+            regex: /\b(?:about|above|against|around|at|below|beneath|beside|between|by|(apart|aside) from|(instead|out) of|into|on(to)?|over|since|thr(ough|u)|under)\b/g,
+            css: 'color3'
+          }, {
+            regex: /\b(?:adding folder items to|after receiving|clipboard info|set the clipboard to|(the )?clipboard|entire contents|document( (edited|file|nib name))?|file( (name|type))?|(info )?for|giving up after|(name )?extension|return(ed)?|second(?! item)(s)?|list (disks|folder)|(Unicode )?text|(disk )?item(s)?|((current|list) )?view|((container|key) )?window|case|diacriticals|hyphens|numeric strings|punctuation|white space|folder creation|application(s( folder)?| (processes|scripts position|support))?|((desktop )?(pictures )?|(documents|downloads|favorites|home|keychain|library|movies|music|public|scripts|sites|system|users|utilities|workflows) )folder|desktop|Folder Action scripts|font(s| panel)?|help|internet plugins|modem scripts|(system )?preferences|printer descriptions|scripting (additions|components)|shared (documents|libraries)|startup (disk|items)|temporary items|trash|on server|in AppleTalk zone|((as|long|short) )?user name|user (ID|locale)|(with )?password|in (bundle( with identifier)?|directory)|(close|open for) access|read|write( permission)?|(g|s)et eof|starting at|hidden( answer)?|open(ed| (location|untitled))?|error (handling|reporting)|administrator privileges|altering line endings|get volume settings|(alert|boot|input|mount|output|set) volume|output muted|(fax|random )?number|round(ing)?|up|down|toward zero|to nearest|as taught in school|system (attribute|info)|((AppleScript( Studio)?|system) )?version|(home )?directory|(IPv4|primary Ethernet) addressId|CPU (type|speed)|physical memory|time (stamp|to GMT)|replacing|ASCII (character|number)|localized string|from table|offset|summarize|beep|delay|say|(empty|multiple) selections allowed|(of|preferred) type|invisibles|showing( package contents)?|editable URL|(File|FTP|News|Media|Web) [Ss]ervers|Telnet hosts|Directory services|Remote applications|waiting until completion|saving( (in|to))?|path (for|to( (((current|frontmost) )?application|resource))?)|(background|RGB) color|(OK|cancel) button name|cancel button|button(s)?|cubic ((centi)?met(re|er)s|yards|feet|inches)|square ((kilo)?met(re|er)s|miles|yards|feet)|(centi|kilo)?met(re|er)s|miles|yards|feet|inches|lit(re|er)s|gallons|quarts|(kilo)?grams|ounces|pounds|degrees (Celsius|Fahrenheit|Kelvin)|print( (dialog|settings))?|clos(e(able)?|ing)|(de)?miniaturized|miniaturizable|zoom(ed|able)|attribute run|action (method|property|title)|phone|email|((start|end)ing|home) page|((birth|creation|current|custom|modification) )?date|((((phonetic )?(first|last|middle))|computer|host|maiden|related) |nick)?name|aim|icq|jabber|msn|yahoo|addressId(es)?|save addressbook|should enable action|city|country( code)?|formatte(r|d addressId)|(palette )?label|state|street|zip|AIM [Hh]andle(s)?|my card|select(ion| all)?|unsaved|(alpha )?value|entr(y|ies)|(ICQ|Jabber|MSN) handle|person|people|company|department|icon image|job title|note|organization|suffix|vcard|url|copies|collating|pages (across|down)|request print time|target( printer)?|((GUI Scripting|Script menu) )?enabled|show Computer scripts|(de)?activated|awake from nib|became (key|main)|call method|of (class|object)|center|clicked toolbar item|closed|for document|exposed|(can )?hide|idle|keyboard (down|up)|event( (number|type))?|launch(ed)?|load (image|movie|nib|sound)|owner|log|mouse (down|dragged|entered|exited|moved|up)|move|column|localization|resource|script|register|drag (info|types)|resigned (active|key|main)|resiz(e(d)?|able)|right mouse (down|dragged|up)|scroll wheel|(at )?index|should (close|open( untitled)?|quit( after last window closed)?|zoom)|((proposed|screen) )?bounds|show(n)?|behind|in front of|size (mode|to fit)|update(d| toolbar item)?|was (hidden|miniaturized)|will (become active|close|finish launching|hide|miniaturize|move|open|quit|(resign )?active|((maximum|minimum|proposed) )?size|show|zoom)|bundle|data source|movie|pasteboard|sound|tool(bar| tip)|(color|open|save) panel|coordinate system|frontmost|main( (bundle|menu|window))?|((services|(excluded from )?windows) )?menu|((executable|frameworks|resource|scripts|shared (frameworks|support)) )?path|(selected item )?identifier|data|content(s| view)?|character(s)?|click count|(command|control|option|shift) key down|context|delta (x|y|z)|key( code)?|location|pressure|unmodified characters|types|(first )?responder|playing|(allowed|selectable) identifiers|allows customization|(auto saves )?configuration|visible|image( name)?|menu form representation|tag|user(-| )defaults|associated file name|(auto|needs) display|current field editor|floating|has (resize indicator|shadow)|hides when deactivated|level|minimized (image|title)|opaque|position|release when closed|sheet|title(d)?)\b/g,
+            css: 'color4'
+          }, {
+            regex: /\b(?:tracks|paragraph|text item(s)?)\b/g,
+            css: 'classes'
+          }, {
+            regex: /\b(?:AppleScript|album|video kind|grouping|length|text item delimiters|quoted form|POSIX path(?= of))\b/g,
+            css: 'properties'
+          }, {
+            regex: /\b(?:run|exists|count)\b/g,
+            css: 'commandNames'
+          }, {
+            regex: /\b(?:POSIX (file|path))\b/g,
+            css: 'additionClasses'
+          }, {
+            regex: /\b(?:message|with (data|icon( (caution|note|stop))?|parameter(s)?|prompt|properties|seed|title)|regexp|string result|using( delimiters)?|default (answer|button|color|country code|entr(y|ies)|identifiers|items|name|location|script editor))\b/g,
+            css: 'additionParameterNames'
+          }, {
+            regex: /\b(?:display(ing| (alert|dialog|mode))?|choose( ((remote )?application|color|folder|from list|URL))?|(do( shell)?|load|run|store) script|re_compile|find text)\b/g,
+            css: 'additionCommandNames'
+          }, {
+            regex: /\b(?:xxx)\b/g,
+            css: 'parameterNames'
+          }, {
+            regex: /\b(?:true|false|none)\b/g,
+            css: 'enumeratedValues'
+          }, {
+            regex: new RegExp(this.getKeywords(specials), 'gm'),
+            css: 'color3'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(this.getKeywords(ordinals), 'gm'),
+            css: 'keyword'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['applescript'];
+        module.exports = Brush;
+
+/***/ },
+/* 24 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Created by Peter Atoria @ http://iAtoria.com
+
+          var inits = 'class interface function package';
+
+          var keywords = '-Infinity ...rest Array as AS3 Boolean break case catch const continue Date decodeURI ' + 'decodeURIComponent default delete do dynamic each else encodeURI encodeURIComponent escape ' + 'extends false final finally flash_proxy for get if implements import in include Infinity ' + 'instanceof int internal is isFinite isNaN isXMLName label namespace NaN native new null ' + 'Null Number Object object_proxy override parseFloat parseInt private protected public ' + 'return set static String super switch this throw true try typeof uint undefined unescape ' + 'use void while with';
+
+          this.regexList = [{
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi,
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(inits), 'gm'),
+            css: 'color3'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp('var', 'gm'),
+            css: 'variable'
+          }, {
+            regex: new RegExp('trace', 'gm'),
+            css: 'color1'
+          }];
+
+          this.forHtmlScript(regexLib.scriptScriptTags);
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['actionscript3', 'as3'];
+        module.exports = Brush;
+
+/***/ },
+/* 25 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+        var XRegExp = __webpack_require__(3).XRegExp;
+        var Match = __webpack_require__(5).Match;
+
+        function Brush() {
+          function hereDocProcess(match, regexInfo) {
+            var result = [];
+
+            if (match.here_doc != null) result.push(new Match(match.here_doc, match.index + match[0].indexOf(match.here_doc), 'string'));
+
+            if (match.full_tag != null) result.push(new Match(match.full_tag, match.index, 'preprocessor'));
+
+            if (match.end_tag != null) result.push(new Match(match.end_tag, match.index + match[0].lastIndexOf(match.end_tag), 'preprocessor'));
+
+            return result;
+          }
+
+          var keywords = 'if fi then elif else for do done until while break continue case esac function return in eq ne ge le';
+          var commands = 'alias apropos awk basename base64 bash bc bg builtin bunzip2 bzcat bzip2 bzip2recover cal cat cd cfdisk chgrp chmod chown chroot' + 'cksum clear cmp comm command cp cron crontab crypt csplit cut date dc dd ddrescue declare df ' + 'diff diff3 dig dir dircolors dirname dirs du echo egrep eject enable env ethtool eval ' + 'exec exit expand export expr false fdformat fdisk fg fgrep file find fmt fold format ' + 'free fsck ftp gawk gcc gdb getconf getopts grep groups gunzip gzcat gzip hash head history hostname id ifconfig ' + 'import install join kill less let ln local locate logname logout look lpc lpr lprint ' + 'lprintd lprintq lprm ls lsof make man mkdir mkfifo mkisofs mknod more mount mtools ' + 'mv nasm nc ndisasm netstat nice nl nohup nslookup objdump od open op passwd paste pathchk ping popd pr printcap ' + 'printenv printf ps pushd pwd quota quotacheck quotactl ram rcp read readonly renice ' + 'remsync rm rmdir rsync screen scp sdiff sed select seq set sftp shift shopt shutdown ' + 'sleep sort source split ssh strace strings su sudo sum symlink sync tail tar tee test time ' + 'times touch top traceroute trap tr true tsort tty type ulimit umask umount unalias ' + 'uname unexpand uniq units unset unshar useradd usermod users uuencode uudecode v vdir ' + 'vi watch wc whereis which who whoami Wget xargs xxd yes chsh zcat';
+
+          this.regexList = [{
+            regex: /^#!.*$/gm,
+            css: 'preprocessor bold'
+          }, {
+            regex: /\/[\w-\/]+/gm,
+            css: 'plain'
+          }, {
+            regex: regexLib.singleLinePerlComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(this.getKeywords(commands), 'gm'),
+            css: 'functions'
+          }, {
+            regex: new XRegExp("(?<full_tag>(&lt;|<){2}(?<tag>\\w+)) .*$(?<here_doc>[\\s\\S]*)(?<end_tag>^\\k<tag>$)", 'gm'),
+            func: hereDocProcess
+          }];
+        }
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['bash', 'shell', 'sh'];
+        module.exports = Brush;
+
+/***/ },
+/* 26 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Jen
+          // http://www.jensbits.com/2009/05/14/coldfusion-brush-for-syntaxhighlighter-plus
+
+          var funcs = 'Abs ACos AddSOAPRequestHeader AddSOAPResponseHeader AjaxLink AjaxOnLoad ArrayAppend ArrayAvg ArrayClear ArrayDeleteAt ' + 'ArrayInsertAt ArrayIsDefined ArrayIsEmpty ArrayLen ArrayMax ArrayMin ArraySet ArraySort ArraySum ArraySwap ArrayToList ' + 'Asc ASin Atn BinaryDecode BinaryEncode BitAnd BitMaskClear BitMaskRead BitMaskSet BitNot BitOr BitSHLN BitSHRN BitXor ' + 'Ceiling CharsetDecode CharsetEncode Chr CJustify Compare CompareNoCase Cos CreateDate CreateDateTime CreateObject ' + 'CreateODBCDate CreateODBCDateTime CreateODBCTime CreateTime CreateTimeSpan CreateUUID DateAdd DateCompare DateConvert ' + 'DateDiff DateFormat DatePart Day DayOfWeek DayOfWeekAsString DayOfYear DaysInMonth DaysInYear DE DecimalFormat DecrementValue ' + 'Decrypt DecryptBinary DeleteClientVariable DeserializeJSON DirectoryExists DollarFormat DotNetToCFType Duplicate Encrypt ' + 'EncryptBinary Evaluate Exp ExpandPath FileClose FileCopy FileDelete FileExists FileIsEOF FileMove FileOpen FileRead ' + 'FileReadBinary FileReadLine FileSetAccessMode FileSetAttribute FileSetLastModified FileWrite Find FindNoCase FindOneOf ' + 'FirstDayOfMonth Fix FormatBaseN GenerateSecretKey GetAuthUser GetBaseTagData GetBaseTagList GetBaseTemplatePath ' + 'GetClientVariablesList GetComponentMetaData GetContextRoot GetCurrentTemplatePath GetDirectoryFromPath GetEncoding ' + 'GetException GetFileFromPath GetFileInfo GetFunctionList GetGatewayHelper GetHttpRequestData GetHttpTimeString ' + 'GetK2ServerDocCount GetK2ServerDocCountLimit GetLocale GetLocaleDisplayName GetLocalHostIP GetMetaData GetMetricData ' + 'GetPageContext GetPrinterInfo GetProfileSections GetProfileString GetReadableImageFormats GetSOAPRequest GetSOAPRequestHeader ' + 'GetSOAPResponse GetSOAPResponseHeader GetTempDirectory GetTempFile GetTemplatePath GetTickCount GetTimeZoneInfo GetToken ' + 'GetUserRoles GetWriteableImageFormats Hash Hour HTMLCodeFormat HTMLEditFormat IIf ImageAddBorder ImageBlur ImageClearRect ' + 'ImageCopy ImageCrop ImageDrawArc ImageDrawBeveledRect ImageDrawCubicCurve ImageDrawLine ImageDrawLines ImageDrawOval ' + 'ImageDrawPoint ImageDrawQuadraticCurve ImageDrawRect ImageDrawRoundRect ImageDrawText ImageFlip ImageGetBlob ImageGetBufferedImage ' + 'ImageGetEXIFTag ImageGetHeight ImageGetIPTCTag ImageGetWidth ImageGrayscale ImageInfo ImageNegative ImageNew ImageOverlay ImagePaste ' + 'ImageRead ImageReadBase64 ImageResize ImageRotate ImageRotateDrawingAxis ImageScaleToFit ImageSetAntialiasing ImageSetBackgroundColor ' + 'ImageSetDrawingColor ImageSetDrawingStroke ImageSetDrawingTransparency ImageSharpen ImageShear ImageShearDrawingAxis ImageTranslate ' + 'ImageTranslateDrawingAxis ImageWrite ImageWriteBase64 ImageXORDrawingMode IncrementValue InputBaseN Insert Int IsArray IsBinary ' + 'IsBoolean IsCustomFunction IsDate IsDDX IsDebugMode IsDefined IsImage IsImageFile IsInstanceOf IsJSON IsLeapYear IsLocalHost ' + 'IsNumeric IsNumericDate IsObject IsPDFFile IsPDFObject IsQuery IsSimpleValue IsSOAPRequest IsStruct IsUserInAnyRole IsUserInRole ' + 'IsUserLoggedIn IsValid IsWDDX IsXML IsXmlAttribute IsXmlDoc IsXmlElem IsXmlNode IsXmlRoot JavaCast JSStringFormat LCase Left Len ' + 'ListAppend ListChangeDelims ListContains ListContainsNoCase ListDeleteAt ListFind ListFindNoCase ListFirst ListGetAt ListInsertAt ' + 'ListLast ListLen ListPrepend ListQualify ListRest ListSetAt ListSort ListToArray ListValueCount ListValueCountNoCase LJustify Log ' + 'Log10 LSCurrencyFormat LSDateFormat LSEuroCurrencyFormat LSIsCurrency LSIsDate LSIsNumeric LSNumberFormat LSParseCurrency LSParseDateTime ' + 'LSParseEuroCurrency LSParseNumber LSTimeFormat LTrim Max Mid Min Minute Month MonthAsString Now NumberFormat ParagraphFormat ParseDateTime ' + 'Pi PrecisionEvaluate PreserveSingleQuotes Quarter QueryAddColumn QueryAddRow QueryConvertForGrid QueryNew QuerySetCell QuotedValueList Rand ' + 'Randomize RandRange REFind REFindNoCase ReleaseComObject REMatch REMatchNoCase RemoveChars RepeatString Replace ReplaceList ReplaceNoCase ' + 'REReplace REReplaceNoCase Reverse Right RJustify Round RTrim Second SendGatewayMessage SerializeJSON SetEncoding SetLocale SetProfileString ' + 'SetVariable Sgn Sin Sleep SpanExcluding SpanIncluding Sqr StripCR StructAppend StructClear StructCopy StructCount StructDelete StructFind ' + 'StructFindKey StructFindValue StructGet StructInsert StructIsEmpty StructKeyArray StructKeyExists StructKeyList StructKeyList StructNew ' + 'StructSort StructUpdate Tan TimeFormat ToBase64 ToBinary ToScript ToString Trim UCase URLDecode URLEncodedFormat URLSessionFormat Val ' + 'ValueList VerifyClient Week Wrap Wrap WriteOutput XmlChildPos XmlElemNew XmlFormat XmlGetNodeType XmlNew XmlParse XmlSearch XmlTransform ' + 'XmlValidate Year YesNoFormat';
+
+          var keywords = 'cfabort cfajaximport cfajaxproxy cfapplet cfapplication cfargument cfassociate cfbreak cfcache cfcalendar ' + 'cfcase cfcatch cfchart cfchartdata cfchartseries cfcol cfcollection cfcomponent cfcontent cfcookie cfdbinfo ' + 'cfdefaultcase cfdirectory cfdiv cfdocument cfdocumentitem cfdocumentsection cfdump cfelse cfelseif cferror ' + 'cfexchangecalendar cfexchangeconnection cfexchangecontact cfexchangefilter cfexchangemail cfexchangetask ' + 'cfexecute cfexit cffeed cffile cfflush cfform cfformgroup cfformitem cfftp cffunction cfgrid cfgridcolumn ' + 'cfgridrow cfgridupdate cfheader cfhtmlhead cfhttp cfhttpparam cfif cfimage cfimport cfinclude cfindex ' + 'cfinput cfinsert cfinterface cfinvoke cfinvokeargument cflayout cflayoutarea cfldap cflocation cflock cflog ' + 'cflogin cfloginuser cflogout cfloop cfmail cfmailparam cfmailpart cfmenu cfmenuitem cfmodule cfNTauthenticate ' + 'cfobject cfobjectcache cfoutput cfparam cfpdf cfpdfform cfpdfformparam cfpdfparam cfpdfsubform cfpod cfpop ' + 'cfpresentation cfpresentationslide cfpresenter cfprint cfprocessingdirective cfprocparam cfprocresult ' + 'cfproperty cfquery cfqueryparam cfregistry cfreport cfreportparam cfrethrow cfreturn cfsavecontent cfschedule ' + 'cfscript cfsearch cfselect cfset cfsetting cfsilent cfslider cfsprydataset cfstoredproc cfswitch cftable ' + 'cftextarea cfthread cfthrow cftimer cftooltip cftrace cftransaction cftree cftreeitem cftry cfupdate cfwddx ' + 'cfwindow cfxml cfzip cfzipparam';
+
+          var operators = 'all and any between cross in join like not null or outer some';
+
+          this.regexList = [{
+            regex: new RegExp('--(.*)$', 'gm'),
+            css: 'comments'
+          }, {
+            regex: regexLib.xmlComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: new RegExp(this.getKeywords(funcs), 'gmi'),
+            css: 'functions'
+          }, {
+            regex: new RegExp(this.getKeywords(operators), 'gmi'),
+            css: 'color1'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gmi'),
+            css: 'keyword'
+          }];
+        }
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['coldfusion', 'cf'];
+        module.exports = Brush;
+
+/***/ },
+/* 27 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Copyright 2006 Shin, YoungJin
+
+          var datatypes = 'ATOM BOOL BOOLEAN BYTE CHAR COLORREF DWORD DWORDLONG DWORD_PTR ' + 'DWORD32 DWORD64 FLOAT HACCEL HALF_PTR HANDLE HBITMAP HBRUSH ' + 'HCOLORSPACE HCONV HCONVLIST HCURSOR HDC HDDEDATA HDESK HDROP HDWP ' + 'HENHMETAFILE HFILE HFONT HGDIOBJ HGLOBAL HHOOK HICON HINSTANCE HKEY ' + 'HKL HLOCAL HMENU HMETAFILE HMODULE HMONITOR HPALETTE HPEN HRESULT ' + 'HRGN HRSRC HSZ HWINSTA HWND INT INT_PTR INT32 INT64 LANGID LCID LCTYPE ' + 'LGRPID LONG LONGLONG LONG_PTR LONG32 LONG64 LPARAM LPBOOL LPBYTE LPCOLORREF ' + 'LPCSTR LPCTSTR LPCVOID LPCWSTR LPDWORD LPHANDLE LPINT LPLONG LPSTR LPTSTR ' + 'LPVOID LPWORD LPWSTR LRESULT PBOOL PBOOLEAN PBYTE PCHAR PCSTR PCTSTR PCWSTR ' + 'PDWORDLONG PDWORD_PTR PDWORD32 PDWORD64 PFLOAT PHALF_PTR PHANDLE PHKEY PINT ' + 'PINT_PTR PINT32 PINT64 PLCID PLONG PLONGLONG PLONG_PTR PLONG32 PLONG64 POINTER_32 ' + 'POINTER_64 PSHORT PSIZE_T PSSIZE_T PSTR PTBYTE PTCHAR PTSTR PUCHAR PUHALF_PTR ' + 'PUINT PUINT_PTR PUINT32 PUINT64 PULONG PULONGLONG PULONG_PTR PULONG32 PULONG64 ' + 'PUSHORT PVOID PWCHAR PWORD PWSTR SC_HANDLE SC_LOCK SERVICE_STATUS_HANDLE SHORT ' + 'SIZE_T SSIZE_T TBYTE TCHAR UCHAR UHALF_PTR UINT UINT_PTR UINT32 UINT64 ULONG ' + 'ULONGLONG ULONG_PTR ULONG32 ULONG64 USHORT USN VOID WCHAR WORD WPARAM WPARAM WPARAM ' + 'char char16_t char32_t bool short int __int32 __int64 __int8 __int16 long float double __wchar_t ' + 'clock_t _complex _dev_t _diskfree_t div_t ldiv_t _exception _EXCEPTION_POINTERS ' + 'FILE _finddata_t _finddatai64_t _wfinddata_t _wfinddatai64_t __finddata64_t ' + '__wfinddata64_t _FPIEEE_RECORD fpos_t _HEAPINFO _HFILE lconv intptr_t ' + 'jmp_buf mbstate_t _off_t _onexit_t _PNH ptrdiff_t _purecall_handler ' + 'sig_atomic_t size_t _stat __stat64 _stati64 terminate_function ' + 'time_t __time64_t _timeb __timeb64 tm uintptr_t _utimbuf ' + 'va_list wchar_t wctrans_t wctype_t wint_t signed';
+
+          var keywords = 'alignas alignof auto break case catch class const constexpr decltype __finally __exception __try ' + 'const_cast continue private public protected __declspec ' + 'default delete deprecated dllexport dllimport do dynamic_cast ' + 'else enum explicit extern if for friend goto inline ' + 'mutable naked namespace new noinline noreturn nothrow noexcept nullptr ' + 'register reinterpret_cast return selectany ' + 'sizeof static static_cast static_assert struct switch template this ' + 'thread thread_local throw true false try typedef typeid typename union ' + 'using uuid virtual void volatile whcar_t while';
+
+          var functions = 'assert isalnum isalpha iscntrl isdigit isgraph islower isprint ' + 'ispunct isspace isupper isxdigit tolower toupper errno localeconv ' + 'setlocale acos asin atan atan2 ceil cos cosh exp fabs floor fmod ' + 'frexp ldexp log log10 modf pow sin sinh sqrt tan tanh jmp_buf ' + 'longjmp setjmp raise signal sig_atomic_t va_arg va_end va_start ' + 'clearerr fclose feof ferror fflush fgetc fgetpos fgets fopen ' + 'fprintf fputc fputs fread freopen fscanf fseek fsetpos ftell ' + 'fwrite getc getchar gets perror printf putc putchar puts remove ' + 'rename rewind scanf setbuf setvbuf sprintf sscanf tmpfile tmpnam ' + 'ungetc vfprintf vprintf vsprintf abort abs atexit atof atoi atol ' + 'bsearch calloc div exit free getenv labs ldiv malloc mblen mbstowcs ' + 'mbtowc qsort rand realloc srand strtod strtol strtoul system ' + 'wcstombs wctomb memchr memcmp memcpy memmove memset strcat strchr ' + 'strcmp strcoll strcpy strcspn strerror strlen strncat strncmp ' + 'strncpy strpbrk strrchr strspn strstr strtok strxfrm asctime ' + 'clock ctime difftime gmtime localtime mktime strftime time';
+
+          this.regexList = [{
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /^ *#.*/gm,
+            css: 'preprocessor'
+          }, {
+            regex: new RegExp(this.getKeywords(datatypes), 'gm'),
+            css: 'color1 bold'
+          }, {
+            regex: new RegExp(this.getKeywords(functions), 'gm'),
+            css: 'functions bold'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword bold'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['cpp', 'cc', 'c++', 'c', 'h', 'hpp', 'h++'];
+        module.exports = Brush;
+
+/***/ },
+/* 28 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+        var Match = __webpack_require__(5).Match;
+
+        function Brush() {
+          var keywords = 'abstract as base bool break byte case catch char checked class const ' + 'continue decimal default delegate do double else enum event explicit volatile ' + 'extern false finally fixed float for foreach get goto if implicit in int ' + 'interface internal is lock long namespace new null object operator out ' + 'override params private protected public readonly ref return sbyte sealed set ' + 'short sizeof stackalloc static string struct switch this throw true try ' + 'typeof uint ulong unchecked unsafe ushort using virtual void while var ' + 'from group by into select let where orderby join on equals ascending descending';
+
+          function fixComments(match, regexInfo) {
+            var css = match[0].indexOf("///") == 0 ? 'color1' : 'comments';
+            return [new Match(match[0], match.index, css)];
+          }
+
+          this.regexList = [{
+            regex: regexLib.singleLineCComments,
+            func: fixComments
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: /@"(?:[^"]|"")*"/g,
+            css: 'string'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /^\s*#.*/gm,
+            css: 'preprocessor'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: /\bpartial(?=\s+(?:class|interface|struct)\b)/g,
+            css: 'keyword'
+          }, {
+            regex: /\byield(?=\s+(?:return|break)\b)/g,
+            css: 'keyword'
+          }];
+
+          this.forHtmlScript(regexLib.aspScriptTags);
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['c#', 'c-sharp', 'csharp'];
+        module.exports = Brush;
+
+/***/ },
+/* 29 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          function getKeywordsCSS(str) {
+            return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b';
+          };
+
+          function getValuesCSS(str) {
+            return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b';
+          };
+
+          var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' + 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' + 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' + 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' + 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' + 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' + 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' + 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' + 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' + 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' + 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' + 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' + 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' + 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index';
+
+          var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder ' + 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed ' + 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero default digits disc dotted double ' + 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia ' + 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic ' + 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha ' + 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower ' + 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset ' + 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side ' + 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow ' + 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize ' + 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal ' + 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin ' + 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow';
+
+          var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif';
+
+          this.regexList = [{
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /\#[a-fA-F0-9]{3,6}/g,
+            css: 'value'
+          }, {
+            regex: /(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)/g,
+            css: 'value'
+          }, {
+            regex: /!important/g,
+            css: 'color3'
+          }, {
+            regex: new RegExp(getKeywordsCSS(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(getValuesCSS(values), 'g'),
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(fonts), 'g'),
+            css: 'color1'
+          }];
+
+          this.forHtmlScript({
+            left: /(&lt;|<)\s*style.*?(&gt;|>)/gi,
+            right: /(&lt;|<)\/\s*style\s*(&gt;|>)/gi
+          });
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['css'];
+        module.exports = Brush;
+
+/***/ },
+/* 30 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          var keywords = 'abs addr and ansichar ansistring array as asm begin boolean byte cardinal ' + 'case char class comp const constructor currency destructor div do double ' + 'downto else end except exports extended false file finalization finally ' + 'for function goto if implementation in inherited int64 initialization ' + 'integer interface is label library longint longword mod nil not object ' + 'of on or packed pansichar pansistring pchar pcurrency pdatetime pextended ' + 'pint64 pointer private procedure program property pshortstring pstring ' + 'pvariant pwidechar pwidestring protected public published raise real real48 ' + 'record repeat set shl shortint shortstring shr single smallint string then ' + 'threadvar to true try type unit until uses val var varirnt while widechar ' + 'widestring with word write writeln xor';
+
+          this.regexList = [{
+            regex: /\(\*[\s\S]*?\*\)/gm,
+            css: 'comments'
+          }, {
+            regex: /{(?!\$)[\s\S]*?}/gm,
+            css: 'comments'
+          }, {
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /\{\$[a-zA-Z]+ .+\}/g,
+            css: 'color1'
+          }, {
+            regex: /\b[\d\.]+\b/g,
+            css: 'value'
+          }, {
+            regex: /\$[a-zA-Z0-9]+\b/g,
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gmi'),
+            css: 'keyword'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['delphi', 'pascal', 'pas'];
+        module.exports = Brush;
+
+/***/ },
+/* 31 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          this.regexList = [{
+            regex: /^\+\+\+ .*$/gm,
+            css: 'color2'
+          }, {
+            regex: /^\-\-\- .*$/gm,
+            css: 'color2'
+          }, {
+            regex: /^\s.*$/gm,
+            css: 'color1'
+          }, {
+            regex: /^@@.*@@.*$/gm,
+            css: 'variable'
+          }, {
+            regex: /^\+.*$/gm,
+            css: 'string'
+          }, {
+            regex: /^\-.*$/gm,
+            css: 'color3'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['diff', 'patch'];
+        module.exports = Brush;
+
+/***/ },
+/* 32 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Jean-Lou Dupont
+          // http://jldupont.blogspot.com/2009/06/erlang-syntax-highlighter.html
+
+          // According to: http://erlang.org/doc/reference_manual/introduction.html#1.5
+          var keywords = 'after and andalso band begin bnot bor bsl bsr bxor ' + 'case catch cond div end fun if let not of or orelse ' + 'query receive rem try when xor' +
+          // additional
+          ' module export import define';
+
+          this.regexList = [{
+            regex: new RegExp("[A-Z][A-Za-z0-9_]+", 'g'),
+            css: 'constants'
+          }, {
+            regex: new RegExp("\\%.+", 'gm'),
+            css: 'comments'
+          }, {
+            regex: new RegExp("\\?[A-Za-z0-9_]+", 'g'),
+            css: 'preprocessor'
+          }, {
+            regex: new RegExp("[a-z0-9_]+:[a-z0-9_]+", 'g'),
+            css: 'functions'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['erl', 'erlang'];
+        module.exports = Brush;
+
+/***/ },
+/* 33 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Andres Almiray
+          // http://jroller.com/aalmiray/entry/nice_source_code_syntax_highlighter
+
+          var keywords = 'as assert break case catch class continue def default do else extends finally ' + 'if in implements import instanceof interface new package property return switch ' + 'throw throws try while public protected private static';
+          var types = 'void boolean byte char short int long float double';
+          var constants = 'null';
+          var methods = 'allProperties count get size ' + 'collect each eachProperty eachPropertyName eachWithIndex find findAll ' + 'findIndexOf grep inject max min reverseEach sort ' + 'asImmutable asSynchronized flatten intersect join pop reverse subMap toList ' + 'padRight padLeft contains eachMatch toCharacter toLong toUrl tokenize ' + 'eachFile eachFileRecurse eachB yte eachLine readBytes readLine getText ' + 'splitEachLine withReader append encodeBase64 decodeBase64 filterLine ' + 'transformChar transformLine withOutputStream withPrintWriter withStream ' + 'withStreams withWriter withWriterAppend write writeLine ' + 'dump inspect invokeMethod print println step times upto use waitForOrKill ' + 'getText';
+
+          this.regexList = [{
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /""".*"""/g,
+            css: 'string'
+          }, {
+            regex: new RegExp('\\b([\\d]+(\\.[\\d]+)?|0x[a-f0-9]+)\\b', 'gi'),
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(this.getKeywords(types), 'gm'),
+            css: 'color1'
+          }, {
+            regex: new RegExp(this.getKeywords(constants), 'gm'),
+            css: 'constants'
+          }, {
+            regex: new RegExp(this.getKeywords(methods), 'gm'),
+            css: 'functions'
+          }];
+
+          this.forHtmlScript(regexLib.aspScriptTags);
+        }
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['groovy'];
+        module.exports = Brush;
+
+/***/ },
+/* 34 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+
+          var inits = 'class interface package macro enum typedef extends implements dynamic in for if while else do try switch case catch';
+
+          var keywords = 'return break continue new throw cast using import function public private inline static untyped callback true false null Int Float String Void Std Bool Dynamic Array Vector';
+
+          this.regexList = [{
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi,
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(inits), 'gm'),
+            css: 'color3'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp('var', 'gm'),
+            css: 'variable'
+          }, {
+            regex: new RegExp('trace', 'gm'),
+            css: 'color1'
+          }, {
+            regex: new RegExp('#if', 'gm'),
+            css: 'comments'
+          }, {
+            regex: new RegExp('#elseif', 'gm'),
+            css: 'comments'
+          }, {
+            regex: new RegExp('#end', 'gm'),
+            css: 'comments'
+          }, {
+            regex: new RegExp('#error', 'gm'),
+            css: 'comments'
+          }];
+
+          //standard compiler conditionals flags
+          var flags = ["debug", "error", "cpp", "js", "neko", "php", "flash", "flash8", "flash9", "flash10", "flash10", "mobile", "desktop", "web", "ios", "android", "iphone"];
+
+          //append the flags to the array with a ! operator
+          var i;
+          var length = flags.length;
+          for (i = 0; i <= length - 1; i++) {
+            this.regexList.push({
+              regex: new RegExp(flags[i], 'gm'),
+              css: 'comments'
+            });
+            this.regexList.push({
+              regex: new RegExp('!' + flags[i], 'gm'),
+              css: 'comments'
+            });
+          }
+
+          this.forHtmlScript(regexLib.scriptScriptTags);
+        }
+
+        ;
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['haxe', 'hx'];
+        module.exports = Brush;
+
+/***/ },
+/* 35 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          var keywords = 'abstract assert boolean break byte case catch char class const ' + 'continue default do double else enum extends ' + 'false final finally float for goto if implements import ' + 'instanceof int interface long native new null ' + 'package private protected public return ' + 'short static strictfp super switch synchronized this throw throws true ' + 'transient try void volatile while';
+
+          this.regexList = [{
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: /\/\*([^\*][\s\S]*?)?\*\//gm,
+            css: 'comments'
+          }, {
+            regex: /\/\*(?!\*\/)\*[\s\S]*?\*\//gm,
+            css: 'preprocessor'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /\b([\d]+(\.[\d]+)?f?|[\d]+l?|0x[a-f0-9]+)\b/gi,
+            css: 'value'
+          }, {
+            regex: /(?!\@interface\b)\@[\$\w]+\b/g,
+            css: 'color1'
+          }, {
+            regex: /\@interface\b/g,
+            css: 'color2'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }];
+
+          this.forHtmlScript({
+            left: /(&lt;|<)%[@!=]?/g,
+            right: /%(&gt;|>)/g
+          });
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['java'];
+        module.exports = Brush;
+
+/***/ },
+/* 36 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Patrick Webster
+          // http://patrickwebster.blogspot.com/2009/04/javafx-brush-for-syntaxhighlighter.html
+          var datatypes = 'Boolean Byte Character Double Duration ' + 'Float Integer Long Number Short String Void';
+
+          var keywords = 'abstract after and as assert at before bind bound break catch class ' + 'continue def delete else exclusive extends false finally first for from ' + 'function if import in indexof init insert instanceof into inverse last ' + 'lazy mixin mod nativearray new not null on or override package postinit ' + 'protected public public-init public-read replace return reverse sizeof ' + 'step super then this throw true try tween typeof var where while with ' + 'attribute let private readonly static trigger';
+
+          this.regexList = [{
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: /(-?\.?)(\b(\d*\.?\d+|\d+\.?\d*)(e[+-]?\d+)?|0x[a-f\d]+)\b\.?/gi,
+            css: 'color2'
+          }, {
+            regex: new RegExp(this.getKeywords(datatypes), 'gm'),
+            css: 'variable'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }];
+          this.forHtmlScript(regexLib.aspScriptTags);
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['jfx', 'javafx'];
+        module.exports = Brush;
+
+/***/ },
+/* 37 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          var keywords = 'break case catch class continue ' + 'default delete do else enum export extends false  ' + 'for from as function if implements import in instanceof ' + 'interface let new null package private protected ' + 'static return super switch ' + 'this throw true try typeof var while with yield';
+
+          this.regexList = [{
+            regex: regexLib.multiLineDoubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.multiLineSingleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: /\s*#.*/gm,
+            css: 'preprocessor'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }];
+
+          this.forHtmlScript(regexLib.scriptScriptTags);
+        }
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['js', 'jscript', 'javascript', 'json'];
+        module.exports = Brush;
+
+/***/ },
+/* 38 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by David Simmons-Duffin and Marty Kube
+
+          var funcs = 'abs accept alarm atan2 bind binmode chdir chmod chomp chop chown chr ' + 'chroot close closedir connect cos crypt defined delete each endgrent ' + 'endhostent endnetent endprotoent endpwent endservent eof exec exists ' + 'exp fcntl fileno flock fork format formline getc getgrent getgrgid ' + 'getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr ' + 'getnetbyname getnetent getpeername getpgrp getppid getpriority ' + 'getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid ' + 'getservbyname getservbyport getservent getsockname getsockopt glob ' + 'gmtime grep hex index int ioctl join keys kill lc lcfirst length link ' + 'listen localtime lock log lstat map mkdir msgctl msgget msgrcv msgsnd ' + 'oct open opendir ord pack pipe pop pos print printf prototype push ' + 'quotemeta rand read readdir readline readlink readpipe recv rename ' + 'reset reverse rewinddir rindex rmdir scalar seek seekdir select semctl ' + 'semget semop send setgrent sethostent setnetent setpgrp setpriority ' + 'setprotoent setpwent setservent setsockopt shift shmctl shmget shmread ' + 'shmwrite shutdown sin sleep socket socketpair sort splice split sprintf ' + 'sqrt srand stat study substr symlink syscall sysopen sysread sysseek ' + 'system syswrite tell telldir time times tr truncate uc ucfirst umask ' + 'undef unlink unpack unshift utime values vec wait waitpid warn write ' +
+          // feature
+          'say';
+
+          var keywords = 'bless caller continue dbmclose dbmopen die do dump else elsif eval exit ' + 'for foreach goto if import last local my next no our package redo ref ' + 'require return sub tie tied unless untie until use wantarray while ' +
+          // feature
+          'given when default ' +
+          // Try::Tiny
+          'try catch finally ' +
+          // Moose
+          'has extends with before after around override augment';
+
+          this.regexList = [{
+            regex: /(<<|&lt;&lt;)((\w+)|(['"])(.+?)\4)[\s\S]+?\n\3\5\n/g,
+            css: 'string'
+          }, {
+            regex: /#.*$/gm,
+            css: 'comments'
+          }, {
+            regex: /^#!.*\n/g,
+            css: 'preprocessor'
+          }, {
+            regex: /-?\w+(?=\s*=(>|&gt;))/g,
+            css: 'string'
+          },
+
+          // is this too much?
+          {
+            regex: /\bq[qwxr]?\([\s\S]*?\)/g,
+            css: 'string'
+          }, {
+            regex: /\bq[qwxr]?\{[\s\S]*?\}/g,
+            css: 'string'
+          }, {
+            regex: /\bq[qwxr]?\[[\s\S]*?\]/g,
+            css: 'string'
+          }, {
+            regex: /\bq[qwxr]?(<|&lt;)[\s\S]*?(>|&gt;)/g,
+            css: 'string'
+          }, {
+            regex: /\bq[qwxr]?([^\w({<[])[\s\S]*?\1/g,
+            css: 'string'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /(?:&amp;|[$@%*]|\$#)\$?[a-zA-Z_](\w+|::)*/g,
+            css: 'variable'
+          }, {
+            regex: /\b__(?:END|DATA)__\b[\s\S]*$/g,
+            css: 'comments'
+          }, {
+            regex: /(^|\n)=\w[\s\S]*?(\n=cut\s*(?=\n)|$)/g,
+            css: 'comments'
+          }, {
+            regex: new RegExp(this.getKeywords(funcs), 'gm'),
+            css: 'functions'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }];
+
+          this.forHtmlScript(regexLib.phpScriptTags);
+        }
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['perl', 'Perl', 'pl'];
+        module.exports = Brush;
+
+/***/ },
+/* 39 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+
+        var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+        var _brushBase = __webpack_require__(22);
+
+        var _brushBase2 = _interopRequireDefault(_brushBase);
+
+        var _syntaxhighlighterRegex = __webpack_require__(3);
+
+        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+        function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+        function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
+
+        function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+        var functions = 'abs acos acosh addcslashes addslashes ' + 'array_change_key_case array_chunk array_combine array_count_values array_diff ' + 'array_diff_assoc array_diff_key array_diff_uassoc array_diff_ukey array_fill ' + 'array_filter array_flip array_intersect array_intersect_assoc array_intersect_key ' + 'array_intersect_uassoc array_intersect_ukey array_key_exists array_keys array_map ' + 'array_merge array_merge_recursive array_multisort array_pad array_pop array_product ' + 'array_push array_rand array_reduce array_reverse array_search array_shift ' + 'array_slice array_splice array_sum array_udiff array_udiff_assoc ' + 'array_udiff_uassoc array_uintersect array_uintersect_assoc ' + 'array_uintersect_uassoc array_unique array_unshift array_values array_walk ' + 'array_walk_recursive atan atan2 atanh base64_decode base64_encode base_convert ' + 'basename bcadd bccomp bcdiv bcmod bcmul bindec bindtextdomain bzclose bzcompress ' + 'bzdecompress bzerrno bzerror bzerrstr bzflush bzopen bzread bzwrite ceil chdir ' + 'checkdate checkdnsrr chgrp chmod chop chown chr chroot chunk_split class_exists ' + 'closedir closelog copy cos cosh count count_chars date decbin dechex decoct ' + 'deg2rad delete ebcdic2ascii echo empty end ereg ereg_replace eregi eregi_replace error_log ' + 'error_reporting escapeshellarg escapeshellcmd eval exec exit exp explode extension_loaded ' + 'feof fflush fgetc fgetcsv fgets fgetss file_exists file_get_contents file_put_contents ' + 'fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype ' + 'floatval flock floor flush fmod fnmatch fopen fpassthru fprintf fputcsv fputs fread fscanf ' + 'fseek fsockopen fstat ftell ftok getallheaders getcwd getdate getenv gethostbyaddr gethostbyname ' + 'gethostbynamel getimagesize getlastmod getmxrr getmygid getmyinode getmypid getmyuid getopt ' + 'getprotobyname getprotobynumber getrandmax getrusage getservbyname getservbyport gettext ' + 'gettimeofday gettype glob gmdate gmmktime ini_alter ini_get ini_get_all ini_restore ini_set ' + 'interface_exists intval ip2long is_a is_array is_bool is_callable is_dir is_double ' + 'is_executable is_file is_finite is_float is_infinite is_int is_integer is_link is_long ' + 'is_nan is_null is_numeric is_object is_readable is_real is_resource is_scalar is_soap_fault ' + 'is_string is_subclass_of is_uploaded_file is_writable is_writeable mkdir mktime nl2br ' + 'parse_ini_file parse_str parse_url passthru pathinfo print readlink realpath rewind rewinddir rmdir ' + 'round str_ireplace str_pad str_repeat str_replace str_rot13 str_shuffle str_split ' + 'str_word_count strcasecmp strchr strcmp strcoll strcspn strftime strip_tags stripcslashes ' + 'stripos stripslashes stristr strlen strnatcasecmp strnatcmp strncasecmp strncmp strpbrk ' + 'strpos strptime strrchr strrev strripos strrpos strspn strstr strtok strtolower strtotime ' + 'strtoupper strtr strval substr substr_compare';
+
+        var keywords = 'abstract and array as break case catch cfunction class clone const continue declare default die do ' + 'else elseif enddeclare endfor endforeach endif endswitch endwhile extends final finally for foreach ' + 'function global goto if implements include include_once interface instanceof insteadof namespace new ' + 'old_function or private protected public return require require_once static switch ' + 'trait throw try use const while xor yield ';
+
+        var constants = '__FILE__ __LINE__ __METHOD__ __FUNCTION__ __CLASS__';
+
+        var Brush = function (_BrushBase) {
+          _inherits(Brush, _BrushBase);
+
+          _createClass(Brush, null, [{
+            key: 'aliases',
+            get: function get() {
+              return ['php'];
+            }
+          }]);
+
+          function Brush() {
+            _classCallCheck(this, Brush);
+
+            var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Brush).call(this));
+
+            _this.regexList = [{ regex: _syntaxhighlighterRegex.commonRegExp.singleLineCComments, css: 'comments' }, { regex: _syntaxhighlighterRegex.commonRegExp.multiLineCComments, css: 'comments' }, { regex: _syntaxhighlighterRegex.commonRegExp.doubleQuotedString, css: 'string' }, { regex: _syntaxhighlighterRegex.commonRegExp.singleQuotedString, css: 'string' }, { regex: /\$\w+/g, css: 'variable' }, { regex: new RegExp(_this.getKeywords(functions), 'gmi'), css: 'functions' }, { regex: new RegExp(_this.getKeywords(constants), 'gmi'), css: 'constants' }, { regex: new RegExp(_this.getKeywords(keywords), 'gm'), css: 'keyword' }];
+
+            _this.forHtmlScript(_syntaxhighlighterRegex.commonRegExp.phpScriptTags);
+            return _this;
+          }
+
+          return Brush;
+        }(_brushBase2.default);
+
+        exports.default = Brush;
+
+/***/ },
+/* 40 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          this.regexList = [];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['text', 'plain'];
+        module.exports = Brush;
+
+/***/ },
+/* 41 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Joel 'Jaykul' Bennett, http://PoshCode.org | http://HuddledMasses.org
+          var keywords = 'while validateset validaterange validatepattern validatelength validatecount ' + 'until trap switch return ref process param parameter in if global: ' + 'function foreach for finally filter end elseif else dynamicparam do default ' + 'continue cmdletbinding break begin alias \\? % #script #private #local #global ' + 'mandatory parametersetname position valuefrompipeline ' + 'valuefrompipelinebypropertyname valuefromremainingarguments helpmessage ';
+
+          var operators = ' and as band bnot bor bxor casesensitive ccontains ceq cge cgt cle ' + 'clike clt cmatch cne cnotcontains cnotlike cnotmatch contains ' + 'creplace eq exact f file ge gt icontains ieq ige igt ile ilike ilt ' + 'imatch ine inotcontains inotlike inotmatch ireplace is isnot le like ' + 'lt match ne not notcontains notlike notmatch or regex replace wildcard';
+
+          var verbs = 'write where wait use update unregister undo trace test tee take suspend ' + 'stop start split sort skip show set send select scroll resume restore ' + 'restart resolve resize reset rename remove register receive read push ' + 'pop ping out new move measure limit join invoke import group get format ' + 'foreach export expand exit enter enable disconnect disable debug cxnew ' + 'copy convertto convertfrom convert connect complete compare clear ' + 'checkpoint aggregate add';
+
+          // I can't find a way to match the comment based help in multi-line comments, because SH won't highlight in highlights, and javascript doesn't support lookbehind
+          var commenthelp = ' component description example externalhelp forwardhelpcategory forwardhelptargetname forwardhelptargetname functionality inputs link notes outputs parameter remotehelprunspace role synopsis';
+
+          this.regexList = [{
+            regex: new RegExp('^\\s*#[#\\s]*\\.(' + this.getKeywords(commenthelp) + ').*$', 'gim'),
+            css: 'preprocessor help bold'
+          }, {
+            regex: regexLib.singleLinePerlComments,
+            css: 'comments'
+          }, {
+            regex: /(&lt;|<)#[\s\S]*?#(&gt;|>)/gm,
+            css: 'comments here'
+          }, {
+            regex: new RegExp('@"\\n[\\s\\S]*?\\n"@', 'gm'),
+            css: 'script string here'
+          }, {
+            regex: new RegExp("@'\\n[\\s\\S]*?\\n'@", 'gm'),
+            css: 'script string single here'
+          }, {
+            regex: new RegExp('"(?:\\$\\([^\\)]*\\)|[^"]|`"|"")*[^`]"', 'g'),
+            css: 'string'
+          }, {
+            regex: new RegExp("'(?:[^']|'')*'", 'g'),
+            css: 'string single'
+          }, {
+            regex: new RegExp('[\\$|@|@@](?:(?:global|script|private|env):)?[A-Z0-9_]+', 'gi'),
+            css: 'variable'
+          }, {
+            regex: new RegExp('(?:\\b' + verbs.replace(/ /g, '\\b|\\b') + ')-[a-zA-Z_][a-zA-Z0-9_]*', 'gmi'),
+            css: 'functions'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gmi'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp('-' + this.getKeywords(operators), 'gmi'),
+            css: 'operator value'
+          }, {
+            regex: new RegExp('\\[[A-Z_\\[][A-Z0-9_. `,\\[\\]]*\\]', 'gi'),
+            css: 'constants'
+          }, {
+            regex: new RegExp('\\s+-(?!' + this.getKeywords(operators) + ')[a-zA-Z_][a-zA-Z0-9_]*', 'gmi'),
+            css: 'color1'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['powershell', 'ps', 'posh'];
+        module.exports = Brush;
+
+/***/ },
+/* 42 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Gheorghe Milas and Ahmad Sherif
+
+          var keywords = 'and assert break class continue def del elif else ' + 'except exec finally for from global if import in is ' + 'lambda not or pass raise return try yield while';
+
+          var funcs = '__import__ abs all any apply basestring bin bool buffer callable ' + 'chr classmethod cmp coerce compile complex delattr dict dir ' + 'divmod enumerate eval execfile file filter float format frozenset ' + 'getattr globals hasattr hash help hex id input int intern ' + 'isinstance issubclass iter len list locals long map max min next ' + 'object oct open ord pow print property range raw_input reduce ' + 'reload repr reversed round set setattr slice sorted staticmethod ' + 'str sum super tuple type type unichr unicode vars xrange zip';
+
+          var special = 'None True False self cls class_';
+
+          this.regexList = [{
+            regex: regexLib.singleLinePerlComments,
+            css: 'comments'
+          }, {
+            regex: /^\s*@\w+/gm,
+            css: 'decorator'
+          }, {
+            regex: /(['\"]{3})([^\1])*?\1/gm,
+            css: 'comments'
+          }, {
+            regex: /"(?!")(?:\.|\\\"|[^\""\n])*"/gm,
+            css: 'string'
+          }, {
+            regex: /'(?!')(?:\.|(\\\')|[^\''\n])*'/gm,
+            css: 'string'
+          }, {
+            regex: /\+|\-|\*|\/|\%|=|==/gm,
+            css: 'keyword'
+          }, {
+            regex: /\b\d+\.?\w*/g,
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(funcs), 'gmi'),
+            css: 'functions'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(this.getKeywords(special), 'gm'),
+            css: 'color1'
+          }];
+
+          this.forHtmlScript(regexLib.aspScriptTags);
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['py', 'python'];
+        module.exports = Brush;
+
+/***/ },
+/* 43 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Erik Peterson.
+
+          var keywords = 'alias and BEGIN begin break case class def define_method defined do each else elsif ' + 'END end ensure false for if in module new next nil not or raise redo rescue retry return ' + 'self super then throw true undef unless until when while yield';
+
+          var builtins = 'Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload ' + 'Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol ' + 'ThreadGroup Thread Time TrueClass';
+
+          this.regexList = [{
+            regex: regexLib.singleLinePerlComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /\b[A-Z0-9_]+\b/g,
+            css: 'constants'
+          }, {
+            regex: /:[a-z][A-Za-z0-9_]*/g,
+            css: 'color2'
+          }, {
+            regex: /(\$|@@|@)\w+/g,
+            css: 'variable bold'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(this.getKeywords(builtins), 'gm'),
+            css: 'color1'
+          }];
+
+          this.forHtmlScript(regexLib.aspScriptTags);
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['ruby', 'rails', 'ror', 'rb'];
+        module.exports = Brush;
+
+/***/ },
+/* 44 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          function getKeywordsCSS(str) {
+            return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b';
+          };
+
+          function getValuesCSS(str) {
+            return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b';
+          };
+
+          function getKeywordsPrependedBy(keywords, by) {
+            return '(?:' + keywords.replace(/^\s+|\s+$/g, '').replace(/\s+/g, '|' + by + '\\b').replace(/^/, by + '\\b') + ')\\b';
+          }
+
+          var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' + 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' + 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' + 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' + 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' + 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' + 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' + 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' + 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' + 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' + 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' + 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' + 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' + 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index zoom';
+
+          var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder ' + 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed ' + 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero digits disc dotted double ' + 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia ' + 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic ' + 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha ' + 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower ' + 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset ' + 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side ' + 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow ' + 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize ' + 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal ' + 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin ' + 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow';
+
+          var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif';
+
+          var statements = 'important default';
+          var preprocessor = 'import extend debug warn if else for while mixin function include content media';
+
+          var r = regexLib;
+
+          this.regexList = [{
+            regex: r.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: r.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: r.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: r.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /\#[a-fA-F0-9]{3,6}/g,
+            css: 'value'
+          }, {
+            regex: /\b(-?\d+)(\.\d+)?(px|em|rem|pt|\:|\%|)\b/g,
+            css: 'value'
+          }, {
+            regex: /\$[\w-]+/g,
+            css: 'variable'
+          }, {
+            regex: new RegExp(getKeywordsPrependedBy(statements, '!'), 'g'),
+            css: 'color3'
+          }, {
+            regex: new RegExp(getKeywordsPrependedBy(preprocessor, '@'), 'g'),
+            css: 'preprocessor'
+          }, {
+            regex: new RegExp(getKeywordsCSS(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(getValuesCSS(values), 'g'),
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(fonts), 'g'),
+            css: 'color1'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['sass', 'scss'];
+        module.exports = Brush;
+
+/***/ },
+/* 45 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Yegor Jbanov and David Bernard.
+
+          var keywords = 'val sealed case def true trait implicit forSome import match object null finally super ' + 'override try lazy for var catch throw type extends class while with new final yield abstract ' + 'else do if return protected private this package false';
+
+          var keyops = '[_:=><%#@]+';
+
+          this.regexList = [{
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineSingleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.multiLineDoubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }, {
+            regex: /0x[a-f0-9]+|\d+(\.\d+)?/gi,
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(keyops, 'gm'),
+            css: 'keyword'
+          }];
+        }
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['scala'];
+        module.exports = Brush;
+
+/***/ },
+/* 46 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          var funcs = 'abs avg case cast coalesce convert count current_timestamp ' + 'current_user day isnull left lower month nullif replace right ' + 'session_user space substring sum system_user upper user year';
+
+          var keywords = 'absolute action add after alter as asc at authorization begin bigint ' + 'binary bit by cascade char character check checkpoint close collate ' + 'column commit committed connect connection constraint contains continue ' + 'create cube current current_date current_time cursor database date ' + 'deallocate dec decimal declare default delete desc distinct double drop ' + 'dynamic else end end-exec escape except exec execute false fetch first ' + 'float for force foreign forward free from full function global goto grant ' + 'group grouping having hour ignore index inner insensitive insert instead ' + 'int integer intersect into is isolation key last level load local max min ' + 'minute modify move name national nchar next no numeric of off on only ' + 'open option order out output partial password precision prepare primary ' + 'prior privileges procedure public read real references relative repeatable ' + 'restrict return returns revoke rollback rollup rows rule schema scroll ' + 'second section select sequence serializable set size smallint static ' + 'statistics table temp temporary then time timestamp to top transaction ' + 'translation trigger true truncate uncommitted union unique update values ' + 'varchar varying view when where with work';
+
+          var operators = 'all and any between cross in join like not null or outer some';
+
+          this.regexList = [{
+            regex: /--(.*)$/gm,
+            css: 'comments'
+          }, {
+            regex: /\/\*([^\*][\s\S]*?)?\*\//gm,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineDoubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.multiLineSingleQuotedString,
+            css: 'string'
+          }, {
+            regex: new RegExp(this.getKeywords(funcs), 'gmi'),
+            css: 'color2'
+          }, {
+            regex: new RegExp(this.getKeywords(operators), 'gmi'),
+            css: 'color1'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gmi'),
+            css: 'keyword'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['sql'];
+        module.exports = Brush;
+
+/***/ },
+/* 47 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+        var Match = __webpack_require__(5).Match;
+
+        function Brush() {
+          // Swift brush contributed by Nate Cook
+          // http://natecook.com/code/swift-syntax-highlighting
+
+          function getKeywordsPrependedBy(keywords, by) {
+            return '(?:' + keywords.replace(/^\s+|\s+$/g, '').replace(/\s+/g, '|' + by + '\\b').replace(/^/, by + '\\b') + ')\\b';
+          }
+
+          function multiLineCCommentsAdd(match, regexInfo) {
+            var str = match[0],
+                result = [],
+                pos = 0,
+                matchStart = 0,
+                level = 0;
+
+            while (pos < str.length - 1) {
+              var chunk = str.substr(pos, 2);
+              if (level == 0) {
+                if (chunk == "/*") {
+                  matchStart = pos;
+                  level++;
+                  pos += 2;
+                } else {
+                  pos++;
+                }
+              } else {
+                if (chunk == "/*") {
+                  level++;
+                  pos += 2;
+                } else if (chunk == "*/") {
+                  level--;
+                  if (level == 0) {
+                    result.push(new Match(str.substring(matchStart, pos + 2), matchStart + match.index, regexInfo.css));
+                  }
+                  pos += 2;
+                } else {
+                  pos++;
+                }
+              }
+            }
+
+            return result;
+          }
+
+          function stringAdd(match, regexInfo) {
+            var str = match[0],
+                result = [],
+                pos = 0,
+                matchStart = 0,
+                level = 0;
+
+            while (pos < str.length - 1) {
+              if (level == 0) {
+                if (str.substr(pos, 2) == "\\(") {
+                  result.push(new Match(str.substring(matchStart, pos + 2), matchStart + match.index, regexInfo.css));
+                  level++;
+                  pos += 2;
+                } else {
+                  pos++;
+                }
+              } else {
+                if (str[pos] == "(") {
+                  level++;
+                }
+                if (str[pos] == ")") {
+                  level--;
+                  if (level == 0) {
+                    matchStart = pos;
+                  }
+                }
+                pos++;
+              }
+            }
+            if (level == 0) {
+              result.push(new Match(str.substring(matchStart, str.length), matchStart + match.index, regexInfo.css));
+            }
+
+            return result;
+          };
+
+          // "Swift-native types" are all the protocols, classes, structs, enums, funcs, vars, and typealiases built into the language
+          var swiftTypes = 'AbsoluteValuable Any AnyClass Array ArrayBound ArrayBuffer ArrayBufferType ArrayLiteralConvertible ArrayType AutoreleasingUnsafePointer BidirectionalIndex Bit BitwiseOperations Bool C CBool CChar CChar16 CChar32 CConstPointer CConstVoidPointer CDouble CFloat CInt CLong CLongLong CMutablePointer CMutableVoidPointer COpaquePointer CShort CSignedChar CString CUnsignedChar CUnsignedInt CUnsignedLong CUnsignedLongLong CUnsignedShort CVaListPointer CVarArg CWideChar Character CharacterLiteralConvertible Collection CollectionOfOne Comparable ContiguousArray ContiguousArrayBuffer DebugPrintable Dictionary DictionaryGenerator DictionaryIndex DictionaryLiteralConvertible Double EmptyCollection EmptyGenerator EnumerateGenerator Equatable ExtendedGraphemeClusterLiteralConvertible ExtendedGraphemeClusterType ExtensibleCollection FilterCollectionView FilterCollectionViewIndex FilterGenerator FilterSequenceView Float Float32 Float64 Float80 FloatLiteralConvertible FloatLiteralType FloatingPointClassification FloatingPointNumber ForwardIndex Generator GeneratorOf GeneratorOfOne GeneratorSequence Hashable HeapBuffer HeapBufferStorage HeapBufferStorageBase ImplicitlyUnwrappedOptional IndexingGenerator Int Int16 Int32 Int64 Int8 IntEncoder IntMax Integer IntegerArithmetic IntegerLiteralConvertible IntegerLiteralType Less LifetimeManager LogicValue MapCollectionView MapSequenceGenerator MapSequenceView MaxBuiltinFloatType MaxBuiltinIntegerType Mirror MirrorDisposition MutableCollection MutableSliceable ObjectIdentifier OnHeap Optional OutputStream PermutationGenerator Printable QuickLookObject RandomAccessIndex Range RangeGenerator RawByte RawOptionSet RawRepresentable Reflectable Repeat ReverseIndex ReverseRange ReverseRangeGenerator ReverseView Sequence SequenceOf SignedInteger SignedNumber Sink SinkOf Slice SliceBuffer Sliceable StaticString Streamable StridedRangeGenerator String StringElement StringInterpolationConvertible StringLiteralConvertible StringLiteralType UInt UInt16 UInt32 UInt64 UInt8 UIntMax UTF16 UTF32 UTF8 UWord UnicodeCodec UnicodeScalar Unmanaged UnsafeArray UnsafePointer UnsignedInteger Void Word Zip2 ZipGenerator2 abs advance alignof alignofValue assert bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal false filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced join lexicographicalCompare map max maxElement min minElement nil numericCast partition posix print println quickSort reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith strideof strideofValue swap swift toString transcode true underestimateCount unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafePointers withVaList';
+
+          var keywords = 'as break case class continue default deinit do dynamicType else enum ' + 'extension fallthrough for func if import in init is let new protocol ' + 'return self Self static struct subscript super switch Type typealias ' + 'var where while __COLUMN__ __FILE__ __FUNCTION__ __LINE__ associativity ' + 'didSet get infix inout left mutating none nonmutating operator override ' + 'postfix precedence prefix right set unowned unowned(safe) unowned(unsafe) weak willSet';
+
+          var attributes = 'assignment class_protocol exported final lazy noreturn NSCopying NSManaged objc optional required auto_closure noreturn IBAction IBDesignable IBInspectable IBOutlet infix prefix postfix';
+
+          this.regexList = [
+          // html entities
+          {
+            regex: new RegExp('\&[a-z]+;', 'gi'),
+            css: 'plain'
+          }, {
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: new RegExp('\\/\\*[\\s\\S]*\\*\\/', 'g'),
+            css: 'comments',
+            func: multiLineCCommentsAdd
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string',
+            func: stringAdd
+          }, {
+            regex: new RegExp('\\b([\\d_]+(\\.[\\de_]+)?|0x[a-f0-9_]+(\\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b', 'gi'),
+            css: 'value'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp(getKeywordsPrependedBy(attributes, '@'), 'gm'),
+            css: 'color1'
+          }, {
+            regex: new RegExp(this.getKeywords(swiftTypes), 'gm'),
+            css: 'color2'
+          }, {
+            regex: new RegExp('\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b', 'gi'),
+            css: 'variable'
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['swift'];
+        module.exports = Brush;
+
+/***/ },
+/* 48 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          // Contributed by Chad Granum
+          this.regexList = [{
+            regex: new RegExp('^1..\\d+', 'gm'),
+            css: 'plain bold italic'
+          }, {
+            regex: new RegExp('^ok( \\d+)?', 'gm'),
+            css: 'keyword'
+          }, {
+            regex: new RegExp('^not ok( \\d+)?', 'gm'),
+            css: 'color3 bold'
+          }, {
+            regex: new RegExp('(?!^\\s*)#.*$', 'gm'),
+            css: 'variable bold'
+          }, {
+            regex: new RegExp('^#.*$', 'gm'),
+            css: 'comments bold'
+          }, {
+            regex: new RegExp('^(?!(not )?ok)[^1].*$', 'gm'),
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleQuotedString,
+            css: 'string'
+          }];
+        }
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['tap', 'Tap', 'TAP'];
+        module.exports = Brush;
+
+/***/ },
+/* 49 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          var keywords = 'break case catch class continue ' + 'default delete do else enum export extends false  ' + 'for function if implements import in instanceof ' + 'interface let new null package private protected ' + 'static return super switch ' + 'this throw true try typeof var while with yield' + ' any bool declare get module number public set string'; // TypeScript-specific, everything above is common with JavaScript
+
+          this.regexList = [{
+            regex: regexLib.multiLineDoubleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.multiLineSingleQuotedString,
+            css: 'string'
+          }, {
+            regex: regexLib.singleLineCComments,
+            css: 'comments'
+          }, {
+            regex: regexLib.multiLineCComments,
+            css: 'comments'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }];
+
+          this.forHtmlScript(regexLib.scriptScriptTags);
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['ts', 'typescript'];
+        module.exports = Brush;
+
+/***/ },
+/* 50 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+
+        function Brush() {
+          var keywords = 'AddHandler AddressOf AndAlso Alias And Ansi As Assembly Auto ' + 'Boolean ByRef Byte ByVal Call Case Catch CBool CByte CChar CDate ' + 'CDec CDbl Char CInt Class CLng CObj Const CShort CSng CStr CType ' + 'Date Decimal Declare Default Delegate Dim DirectCast Do Double Each ' + 'Else ElseIf End Enum Erase Error Event Exit False Finally For Friend ' + 'Function Get GetType GoSub GoTo Handles If Implements Imports In ' + 'Inherits Integer Interface Is Let Lib Like Long Loop Me Mod Module ' + 'MustInherit MustOverride MyBase MyClass Namespace New Next Not Nothing ' + 'NotInheritable NotOverridable Object On Option Optional Or OrElse ' + 'Overloads Overridable Overrides ParamArray Preserve Private Property ' + 'Protected Public RaiseEvent ReadOnly ReDim REM RemoveHandler Resume ' + 'Return Select Set Shadows Shared Short Single Static Step Stop String ' + 'Structure Sub SyncLock Then Throw To True Try TypeOf Unicode Until ' + 'Variant When While With WithEvents WriteOnly Xor';
+
+          this.regexList = [{
+            regex: /'.*$/gm,
+            css: 'comments'
+          }, {
+            regex: regexLib.doubleQuotedString,
+            css: 'string'
+          }, {
+            regex: /^\s*#.*$/gm,
+            css: 'preprocessor'
+          }, {
+            regex: new RegExp(this.getKeywords(keywords), 'gm'),
+            css: 'keyword'
+          }];
+
+          this.forHtmlScript(regexLib.aspScriptTags);
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['vb', 'vbnet'];
+        module.exports = Brush;
+
+/***/ },
+/* 51 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var BrushBase = __webpack_require__(22);
+        var regexLib = __webpack_require__(3).commonRegExp;
+        var XRegExp = __webpack_require__(3).XRegExp;
+        var Match = __webpack_require__(5).Match;
+
+        function Brush() {
+          function process(match, regexInfo) {
+            var code = match[0],
+                tag = XRegExp.exec(code, XRegExp('(&lt;|<)[\\s\\/\\?!]*(?<name>[:\\w-\\.]+)', 'xg')),
+                result = [];
+
+            if (match.attributes != null) {
+              var attributes,
+                  pos = 0,
+                  regex = XRegExp('(?<name> [\\w:.-]+)' + '\\s*=\\s*' + '(?<value> ".*?"|\'.*?\'|\\w+)', 'xg');
+
+              while ((attributes = XRegExp.exec(code, regex, pos)) != null) {
+                result.push(new Match(attributes.name, match.index + attributes.index, 'color1'));
+                result.push(new Match(attributes.value, match.index + attributes.index + attributes[0].indexOf(attributes.value), 'string'));
+                pos = attributes.index + attributes[0].length;
+              }
+            }
+
+            if (tag != null) result.push(new Match(tag.name, match.index + tag[0].indexOf(tag.name), 'keyword'));
+
+            return result;
+          }
+
+          this.regexList = [{
+            regex: XRegExp('(\\&lt;|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\&gt;|>)', 'gm'),
+            css: 'color2'
+          }, {
+            regex: regexLib.xmlComments,
+            css: 'comments'
+          }, {
+            regex: XRegExp('(&lt;|<)[\\s\\/\\?!]*(\\w+)(?<attributes>.*?)[\\s\\/\\?]*(&gt;|>)', 'sg'),
+            func: process
+          }];
+        };
+
+        Brush.prototype = new BrushBase();
+        Brush.aliases = ['xml', 'xhtml', 'xslt', 'html', 'plist'];
+        module.exports = Brush;
+
+/***/ },
+/* 52 */
+/***/ function(module, exports, __webpack_require__) {
+
+        'use strict';
+
+        var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
+
+        /*!
+          * domready (c) Dustin Diaz 2014 - License MIT
+          */
+        !function (name, definition) {
+
+          if (true) module.exports = definition();else if (typeof define == 'function' && _typeof(define.amd) == 'object') define(definition);else this[name] = definition();
+        }('domready', function () {
+
+          var fns = [],
+              _listener,
+              doc = document,
+              hack = doc.documentElement.doScroll,
+              domContentLoaded = 'DOMContentLoaded',
+              loaded = (hack ? /^loaded|^c/ : /^loaded|^i|^c/).test(doc.readyState);
+
+          if (!loaded) doc.addEventListener(domContentLoaded, _listener = function listener() {
+            doc.removeEventListener(domContentLoaded, _listener);
+            loaded = 1;
+            while (_listener = fns.shift()) {
+              _listener();
+            }
+          });
+
+          return function (fn) {
+            loaded ? setTimeout(fn, 0) : fns.push(fn);
+          };
+        });
+
+/***/ },
+/* 53 */
+/***/ function(module, exports) {
+
+        'use strict';
+
+        Object.defineProperty(exports, "__esModule", {
+          value: true
+        });
+        var string = exports.string = function string(value) {
+          return value.replace(/^([A-Z])/g, function (_, character) {
+            return character.toLowerCase();
+          }).replace(/([A-Z])/g, function (_, character) {
+            return '-' + character.toLowerCase();
+          });
+        };
+
+        var object = exports.object = function object(value) {
+          var result = {};
+          Object.keys(value).forEach(function (key) {
+            return result[string(key)] = value[key];
+          });
+          return result;
+        };
+
+/***/ }
+/******/ ]);
+//# sourceMappingURL=syntaxhighlighter.js.map
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/theme.html b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/theme.html new file mode 100644 index 0000000..70e6dd4 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/artifacts/spring-boot-web/theme.html @@ -0,0 +1,827 @@ + + + + + + + +Untitled + + + + + + + + + + + + + + + +
+
+
src/main/resources/static/css/theme.css
+
+
.syntaxhighlighter a,
+.syntaxhighlighter div,
+.syntaxhighlighter code,
+.syntaxhighlighter table,
+.syntaxhighlighter table td,
+.syntaxhighlighter table tr,
+.syntaxhighlighter table tbody,
+.syntaxhighlighter table thead,
+.syntaxhighlighter table caption,
+.syntaxhighlighter textarea {
+  -moz-border-radius: 0 0 0 0 !important;
+  -webkit-border-radius: 0 0 0 0 !important;
+  background: none !important;
+  border: 0 !important;
+  bottom: auto !important;
+  float: none !important;
+  height: auto !important;
+  left: auto !important;
+  line-height: 1.1em !important;
+  margin: 0 !important;
+  outline: 0 !important;
+  overflow: visible !important;
+  padding: 0 !important;
+  position: static !important;
+  right: auto !important;
+  text-align: left !important;
+  top: auto !important;
+  vertical-align: baseline !important;
+  width: auto !important;
+  box-sizing: content-box !important;
+  font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
+  font-weight: normal !important;
+  font-style: normal !important;
+  font-size: 1em !important;
+  min-height: inherit !important;
+  min-height: auto !important; }
+
+.syntaxhighlighter {
+  width: 100% !important;
+  margin: 1em 0 1em 0 !important;
+  position: relative !important;
+  overflow: auto !important;
+  font-size: 1em !important; }
+  .syntaxhighlighter .container:before, .syntaxhighlighter .container:after {
+    content: none !important; }
+  .syntaxhighlighter.source {
+    overflow: hidden !important; }
+  .syntaxhighlighter .bold {
+    font-weight: bold !important; }
+  .syntaxhighlighter .italic {
+    font-style: italic !important; }
+  .syntaxhighlighter .line {
+    white-space: pre !important; }
+  .syntaxhighlighter table {
+    width: 100% !important; }
+    .syntaxhighlighter table caption {
+      text-align: left !important;
+      padding: .5em 0 0.5em 1em !important; }
+    .syntaxhighlighter table td.code {
+      width: 100% !important; }
+      .syntaxhighlighter table td.code .container {
+        position: relative !important; }
+        .syntaxhighlighter table td.code .container textarea {
+          box-sizing: border-box !important;
+          position: absolute !important;
+          left: 0 !important;
+          top: 0 !important;
+          width: 100% !important;
+          height: 100% !important;
+          border: none !important;
+          background: white !important;
+          padding-left: 1em !important;
+          overflow: hidden !important;
+          white-space: pre !important; }
+    .syntaxhighlighter table td.gutter .line {
+      text-align: right !important;
+      padding: 0 0.5em 0 1em !important; }
+    .syntaxhighlighter table td.code .line {
+      padding: 0 1em !important; }
+  .syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
+    padding-left: 0em !important; }
+  .syntaxhighlighter.show {
+    display: block !important; }
+  .syntaxhighlighter.collapsed table {
+    display: none !important; }
+  .syntaxhighlighter.collapsed .toolbar {
+    padding: 0.1em 0.8em 0em 0.8em !important;
+    font-size: 1em !important;
+    position: static !important;
+    width: auto !important;
+    height: auto !important; }
+    .syntaxhighlighter.collapsed .toolbar span {
+      display: inline !important;
+      margin-right: 1em !important; }
+      .syntaxhighlighter.collapsed .toolbar span a {
+        padding: 0 !important;
+        display: none !important; }
+        .syntaxhighlighter.collapsed .toolbar span a.expandSource {
+          display: inline !important; }
+  .syntaxhighlighter .toolbar {
+    position: absolute !important;
+    right: 1px !important;
+    top: 1px !important;
+    width: 11px !important;
+    height: 11px !important;
+    font-size: 10px !important;
+    z-index: 10 !important; }
+    .syntaxhighlighter .toolbar span.title {
+      display: inline !important; }
+    .syntaxhighlighter .toolbar a {
+      display: block !important;
+      text-align: center !important;
+      text-decoration: none !important;
+      padding-top: 1px !important; }
+      .syntaxhighlighter .toolbar a.expandSource {
+        display: none !important; }
+  .syntaxhighlighter.ie {
+    font-size: .9em !important;
+    padding: 1px 0 1px 0 !important; }
+    .syntaxhighlighter.ie .toolbar {
+      line-height: 8px !important; }
+      .syntaxhighlighter.ie .toolbar a {
+        padding-top: 0px !important; }
+  .syntaxhighlighter.printing .line.alt1 .content,
+  .syntaxhighlighter.printing .line.alt2 .content,
+  .syntaxhighlighter.printing .line.highlighted .number,
+  .syntaxhighlighter.printing .line.highlighted.alt1 .content,
+  .syntaxhighlighter.printing .line.highlighted.alt2 .content {
+    background: none !important; }
+  .syntaxhighlighter.printing .line .number {
+    color: #bbbbbb !important; }
+  .syntaxhighlighter.printing .line .content {
+    color: black !important; }
+  .syntaxhighlighter.printing .toolbar {
+    display: none !important; }
+  .syntaxhighlighter.printing a {
+    text-decoration: none !important; }
+  .syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
+    color: black !important; }
+  .syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
+    color: #008200 !important; }
+  .syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
+    color: blue !important; }
+  .syntaxhighlighter.printing .keyword {
+    color: #006699 !important;
+    font-weight: bold !important; }
+  .syntaxhighlighter.printing .preprocessor {
+    color: gray !important; }
+  .syntaxhighlighter.printing .variable {
+    color: #aa7700 !important; }
+  .syntaxhighlighter.printing .value {
+    color: #009900 !important; }
+  .syntaxhighlighter.printing .functions {
+    color: #ff1493 !important; }
+  .syntaxhighlighter.printing .constants {
+    color: #0066cc !important; }
+  .syntaxhighlighter.printing .script {
+    font-weight: bold !important; }
+  .syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
+    color: gray !important; }
+  .syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
+    color: #ff1493 !important; }
+  .syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
+    color: red !important; }
+  .syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
+    color: black !important; }
+
+.syntaxhighlighter {
+  background-color: white !important; }
+  .syntaxhighlighter .line.alt1 {
+    background-color: white !important; }
+  .syntaxhighlighter .line.alt2 {
+    background-color: white !important; }
+  .syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
+    background-color: #e0e0e0 !important; }
+  .syntaxhighlighter .line.highlighted.number {
+    color: black !important; }
+  .syntaxhighlighter table caption {
+    color: black !important; }
+  .syntaxhighlighter table td.code .container textarea {
+    background: white;
+    color: black; }
+  .syntaxhighlighter .gutter {
+    color: #afafaf !important; }
+    .syntaxhighlighter .gutter .line {
+      border-right: 3px solid #6ce26c !important; }
+      .syntaxhighlighter .gutter .line.highlighted {
+        background-color: #6ce26c !important;
+        color: white !important; }
+  .syntaxhighlighter.printing .line .content {
+    border: none !important; }
+  .syntaxhighlighter.collapsed {
+    overflow: visible !important; }
+    .syntaxhighlighter.collapsed .toolbar {
+      color: #00f !important;
+      background: #fff !important;
+      border: 1px solid #6ce26c !important; }
+      .syntaxhighlighter.collapsed .toolbar a {
+        color: #00f !important; }
+        .syntaxhighlighter.collapsed .toolbar a:hover {
+          color: #f00 !important; }
+  .syntaxhighlighter .toolbar {
+    color: #fff !important;
+    background: #6ce26c !important;
+    border: none !important; }
+    .syntaxhighlighter .toolbar a {
+      color: #fff !important; }
+      .syntaxhighlighter .toolbar a:hover {
+        color: #000 !important; }
+  .syntaxhighlighter .plain, .syntaxhighlighter .plain a {
+    color: black !important; }
+  .syntaxhighlighter .comments, .syntaxhighlighter .comments a {
+    color: #008200 !important; }
+  .syntaxhighlighter .string, .syntaxhighlighter .string a {
+    color: blue !important; }
+  .syntaxhighlighter .keyword {
+    font-weight: bold !important;
+    color: #006699 !important; }
+  .syntaxhighlighter .preprocessor {
+    color: gray !important; }
+  .syntaxhighlighter .variable {
+    color: #aa7700 !important; }
+  .syntaxhighlighter .value {
+    color: #009900 !important; }
+  .syntaxhighlighter .functions {
+    color: #ff1493 !important; }
+  .syntaxhighlighter .constants {
+    color: #0066cc !important; }
+  .syntaxhighlighter .script {
+    font-weight: bold !important;
+    color: #006699 !important;
+    background-color: none !important; }
+  .syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
+    color: gray !important; }
+  .syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
+    color: #ff1493 !important; }
+  .syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
+    color: red !important; }
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/images/02-spring-boot-01.png b/src/main/UploadedContent/student-lab-instructions/images/02-spring-boot-01.png new file mode 100644 index 0000000..d075159 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/02-spring-boot-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/03-spring-boot-internals-01.png b/src/main/UploadedContent/student-lab-instructions/images/03-spring-boot-internals-01.png new file mode 100644 index 0000000..0c6db5a Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/03-spring-boot-internals-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/03-spring-boot-internals-02.png b/src/main/UploadedContent/student-lab-instructions/images/03-spring-boot-internals-02.png new file mode 100644 index 0000000..3710843 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/03-spring-boot-internals-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/04-spring-boot-features-01.png b/src/main/UploadedContent/student-lab-instructions/images/04-spring-boot-features-01.png new file mode 100644 index 0000000..596eb57 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/04-spring-boot-features-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-01.png b/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-01.png new file mode 100644 index 0000000..5b07594 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-02.png b/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-02.png new file mode 100644 index 0000000..b2b9afa Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-03.png b/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-03.png new file mode 100644 index 0000000..974fc52 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/05-spring-boot-web-03.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-01.png b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-01.png new file mode 100644 index 0000000..3a906a8 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-02.png b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-02.png new file mode 100644 index 0000000..b46a84b Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-03.png b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-03.png new file mode 100644 index 0000000..52c305f Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-03.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-04.png b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-04.png new file mode 100644 index 0000000..974fc52 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-04.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-05.png b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-05.png new file mode 100644 index 0000000..f951047 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-05.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-06.png b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-06.png new file mode 100644 index 0000000..18e63ea Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/06-spring-boot-data-06.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/07-spring-boot-test-01.png b/src/main/UploadedContent/student-lab-instructions/images/07-spring-boot-test-01.png new file mode 100644 index 0000000..0e511bd Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/07-spring-boot-test-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-01.png b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-01.png new file mode 100644 index 0000000..ad3024a Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-02.png b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-02.png new file mode 100644 index 0000000..7f1c3ad Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-03.png b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-03.png new file mode 100644 index 0000000..9dc4d4f Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-03.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-04.png b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-04.png new file mode 100644 index 0000000..33ea381 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-04.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-05.png b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-05.png new file mode 100644 index 0000000..f5eaa7a Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-05.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-06.png b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-06.png new file mode 100644 index 0000000..34625ee Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/08-spring-boot-actuator-06.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-01.png b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-01.png new file mode 100644 index 0000000..b901562 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-02.png b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-02.png new file mode 100644 index 0000000..b32b227 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-03.png b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-03.png new file mode 100644 index 0000000..2f5a02c Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-03.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-04.png b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-04.png new file mode 100644 index 0000000..e314b45 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-04.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-xx.png b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-xx.png new file mode 100644 index 0000000..a25f91e Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/09-spring-boot-security-xx.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-00.png b/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-00.png new file mode 100644 index 0000000..ff4a21a Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-00.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-01.png b/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-01.png new file mode 100644 index 0000000..b8945af Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-02.png b/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-02.png new file mode 100644 index 0000000..2957519 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/10-spring-boot-messaging-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-01.png b/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-01.png new file mode 100644 index 0000000..4cf29c6 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-02.png b/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-02.png new file mode 100644 index 0000000..a59007e Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-03.png b/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-03.png new file mode 100644 index 0000000..2f8f839 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/11-spring-boot-webflux-03.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/12-spring-boot-cloud-stream-01.png b/src/main/UploadedContent/student-lab-instructions/images/12-spring-boot-cloud-stream-01.png new file mode 100644 index 0000000..fa8ae98 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/12-spring-boot-cloud-stream-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/12-spring-boot-cloud-stream-02.png b/src/main/UploadedContent/student-lab-instructions/images/12-spring-boot-cloud-stream-02.png new file mode 100644 index 0000000..6dcc225 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/12-spring-boot-cloud-stream-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/14-spring-boot-extending-01.png b/src/main/UploadedContent/student-lab-instructions/images/14-spring-boot-extending-01.png new file mode 100644 index 0000000..98033f9 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/14-spring-boot-extending-01.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/images/14-spring-boot-extending-02.png b/src/main/UploadedContent/student-lab-instructions/images/14-spring-boot-extending-02.png new file mode 100644 index 0000000..a17c211 Binary files /dev/null and b/src/main/UploadedContent/student-lab-instructions/images/14-spring-boot-extending-02.png differ diff --git a/src/main/UploadedContent/student-lab-instructions/index.html b/src/main/UploadedContent/student-lab-instructions/index.html new file mode 100644 index 0000000..45a6957 --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/index.html @@ -0,0 +1,661 @@ + + + + + + + +Spring Boot Developer Course Labs + + + + + + + + + + + + + + + +
+
+
+
+

Welcome to the Spring Boot Developer Labs, here you will find all the links to all the Labs that are covered in this Course.

+
+ +
+
+
+

Docker

+ +
+
+

Material

+
+ +
+
+
+ + + \ No newline at end of file diff --git a/src/main/UploadedContent/student-lab-instructions/js/jquery-3.2.1.min.js b/src/main/UploadedContent/student-lab-instructions/js/jquery-3.2.1.min.js new file mode 100644 index 0000000..644d35e --- /dev/null +++ b/src/main/UploadedContent/student-lab-instructions/js/jquery-3.2.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + + + + + +
+
+

Goal

+
+
+

Docker is one of the most popular container technology nowadays, so the purpose of this document is to illustrate how to use Docker for the Spring Boot Course Labs

+
+
+
+
+

Pivotal Edu Images

+
+
+

You can get all the images from Docker Hub: https://hub.docker.com/u/pivotaledu/

+
+
+

Spring Boot

+
+

This image is based on pivotaledu/base:latest. This images contains the following programs/packages:

+
+
+
    +
  • +

    OpenJDK 8 - 1.8.0_131

    +
  • +
  • +

    Spring Boot 2.0.0.M6 (latest)

    +
  • +
  • +

    Groovy 2.4.12

    +
  • +
  • +

    Cloud Foundry CLI 6.32

    +
  • +
  • +

    Maven and Gradle wrappers

    +
  • +
+
+
+

Exposes from port 7000-9999

+
+
+

Usage

+
+
+
docker run --name spring-boot -it --rm pivotaledu/spring-boot bash
+
+
+
+ + + + + +
+
Tip
+
+You can use the -p 8080:8080 to expose the port 8080 to your localhost. +
+
+
+
+
+

Tomcat 8.5.3

+
+

This image is base from pivotaledu/java8:latest. It provides Tomcat 8.5.3

+
+
+

Usage

+
+
+
docker run -it --rm -p 8080:8080 --name tomcat8 pivotaledu/tomcat:8.5.3 /opt/tomcat/bin/catalina.sh run
+
+
+
+

If you want to copy a war to the container:

+
+
+
+
docker cp ./sample.war tomcat8:/opt/tomcat/webapps/sample.war
+
+
+
+
+
+

RabbitMQ 3.6.2

+
+

This image is base from pivotaledu/base:latest. It provides RabbitMQ 3.6.2

+
+
+

Default credentials:

+
+
+
+
    username: admin
+    password: admin
+
+
+
+

You can use environment variables with the -e option:

+
+
+
+
    RMQ_USER
+    RMQ_PASS
+
+
+
+

Usage

+
+
+
$ docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 pivotaledu/rabbitmq
+
+
+
+
+

NOTES

+
+

In the labs we are overriding a Listener and a RabbitTemplate so in order to use a custom username and password, you can package your app and the execute:

+
+
+
+
java -jar target/code-snippet-manager-amqp-0.0.1-SNAPSHOT.jar --spring.rabbitmq.username=admin --spring.rabbitmq.password=admin
+
+
+
+

Or another alternative is that we can add also a ConnectionFactory:

+
+
+
+
@Bean
+public ConnectionFactory connectionFactory(RabbitProperties props){
+    CachingConnectionFactory cf = new CachingConnectionFactory();
+    cf.setUsername(props.getUsername());
+    cf.setPassword(props.getPassword());
+    return cf;
+}
+
+
+
+

then you can add this to your application.properties

+
+
+
+
spring.rabbitmq.username=admin
+spring.rabbitmq.password=admin
+
+
+
+
+
+

MySQL (MariaDB)

+
+

This image is base from pivotaledu/base:latest. It provides MariaDB 10.1.26.

+
+
+

Defautl credentials:

+
+
+
+
    user: root
+    password: pivotal
+
+
+
+

You can add this environment variables with the -e option:

+
+
+
+
    MYSQL_DATABASE
+    MYSQL_USER
+    MYSQL_PASSWORD
+    MYSQL_ROOT_PASSWORD
+
+
+
+

Usage

+
+
+
docker run -d -p 3306:3306 --name mysql pivotaledu/mariadb
+
+
+
+

NOTES:

+
+
+

If using a local mysql client, is necessary to add the --protocol=TCP

+
+
+

Example:

+
+
+
+
mysql --protocol=TCP -uroot -p
+
+
+
+
+
+

Redis 4.0.2

+
+

This image base from pivotaledu/base:latest. It provides Redis 4.0.2 server.

+
+
+

Usage

+
+
+
docker run -d --name redis -p 6379:6379 pivotaledu/redis:4.0.2
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/java/io/zipcoder/ZcwbankApplication.java b/src/main/java/io/zipcoder/ZcwbankApplication.java index 60df46b..7fa3936 100644 --- a/src/main/java/io/zipcoder/ZcwbankApplication.java +++ b/src/main/java/io/zipcoder/ZcwbankApplication.java @@ -1,12 +1,24 @@ package io.zipcoder; +import org.h2.server.web.WebServlet; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; + + @SpringBootApplication public class ZcwbankApplication { public static void main(String[] args) { + SpringApplication.run(ZcwbankApplication.class, args); } + @Bean + ServletRegistrationBean h2servletRegistration(){ + ServletRegistrationBean registrationBean = new ServletRegistrationBean( new WebServlet()); + registrationBean.addUrlMappings("/console/*"); + return registrationBean; + } } diff --git a/src/main/java/io/zipcoder/bank/controller/AccountController.java b/src/main/java/io/zipcoder/bank/controller/AccountController.java new file mode 100644 index 0000000..838acca --- /dev/null +++ b/src/main/java/io/zipcoder/bank/controller/AccountController.java @@ -0,0 +1,55 @@ +package io.zipcoder.bank.controller; + +import io.zipcoder.bank.model.Account; +import io.zipcoder.bank.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Collection; + +@RestController +@RequestMapping("/api/accounts") +public class AccountController { + + private AccountService accountService; + + @Autowired + public AccountController(AccountService accountService) { + + this.accountService = accountService; + } + + @RequestMapping(method = RequestMethod.GET) + public ResponseEntity> getAccount() { + Collection accounts = accountService.findAllAccounts(); + return new ResponseEntity<>(accounts, HttpStatus.OK); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET) + public ResponseEntity findAccountById(@PathVariable("id") Integer id) { + Account account = accountService.findAccountById(id); + return new ResponseEntity<>(account, HttpStatus.OK); + } + + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createAccount(@RequestBody Account account) { + Account savedAccount = accountService.createAccount(account); + return new ResponseEntity<>(savedAccount, HttpStatus.CREATED); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.PUT) + public ResponseEntity updateAccountById(@RequestBody Account account, @PathVariable("id") Integer id) { + Account returnAccount = accountService.updateAccountById(id, account); + return new ResponseEntity<>(returnAccount, HttpStatus.OK); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public ResponseEntity deleteAccountById(@PathVariable("id") Integer id) { + accountService.deleteAccountById(id); + return new ResponseEntity<>(HttpStatus.OK); + } + +} diff --git a/src/main/java/io/zipcoder/bank/controller/BillController.java b/src/main/java/io/zipcoder/bank/controller/BillController.java new file mode 100644 index 0000000..a5ae26d --- /dev/null +++ b/src/main/java/io/zipcoder/bank/controller/BillController.java @@ -0,0 +1,60 @@ +package io.zipcoder.bank.controller; + +import io.zipcoder.bank.model.Bill; +import io.zipcoder.bank.service.BillService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Collection; + +@RestController +@RequestMapping(value = "/api/bills") +public class BillController { + + private BillService billService; + + @Autowired + public BillController(BillService billService) { + this.billService = billService; + } + + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createBill(@RequestBody Bill bill) { + Bill savedBill = billService.createBill(bill); + return new ResponseEntity<>(savedBill, HttpStatus.CREATED); + } + + @RequestMapping(method = RequestMethod.GET) + public ResponseEntity> findAllBills() { + Collection accounts = billService.findAllBills(); + return new ResponseEntity<>(accounts, HttpStatus.OK); + } + + @RequestMapping(value = "/{billId}", method = RequestMethod.GET) + public ResponseEntity findBillById(@PathVariable("billId") Integer id) { + Bill bill = billService.findBillByBillId(id); + return new ResponseEntity<>(bill, HttpStatus.OK); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.PUT) + public ResponseEntity updateAccountById(@RequestBody Bill bill, @PathVariable("id") Integer id) { + Bill returnBill = billService.updateBillByBillId(id, bill); + return new ResponseEntity<>(returnBill, HttpStatus.OK); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public ResponseEntity deleteBillById(@PathVariable("id") Integer id) { + billService.deleteBillByBillId(id); + return new ResponseEntity<>(HttpStatus.OK); + } + +// @RequestMapping(value = "/accounts/{accountId}/bills", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) +// public ResponseEntity createBill(@PathVariable("accountId") @RequestBody Bill bill) { +// Bill savedBill = billService.createBill(bill); +// return new ResponseEntity<>(savedBill, HttpStatus.CREATED); +// } + +} diff --git a/src/main/java/io/zipcoder/bank/controller/CustomerController.java b/src/main/java/io/zipcoder/bank/controller/CustomerController.java new file mode 100644 index 0000000..f0cf36b --- /dev/null +++ b/src/main/java/io/zipcoder/bank/controller/CustomerController.java @@ -0,0 +1,55 @@ +package io.zipcoder.bank.controller; + +import io.zipcoder.bank.model.Customer; +import io.zipcoder.bank.service.CustomerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Collection; + +@RestController +@RequestMapping("/api/customers") +public class CustomerController { + + private CustomerService customerService; + + @Autowired + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + @RequestMapping(method = RequestMethod.GET) + public ResponseEntity> getCustomer() { + Collection customers = customerService.findAllCustomers(); + return new ResponseEntity<>(customers, HttpStatus.OK); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET) + public ResponseEntity findCustomerById(@PathVariable("id") Integer id) { + Customer customer = customerService.findCustomerById(id); + return new ResponseEntity<>(customer, HttpStatus.OK); + } + + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createCustomer(@RequestBody Customer customer) { + Customer savedCustomer = customerService.createCustomer(customer); + return new ResponseEntity<>(savedCustomer, HttpStatus.CREATED); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.PUT) + + public ResponseEntity updateCustomerById(@RequestBody Customer customer, @PathVariable("id") Integer id) { + + + + Customer returnCustomer = customerService.updateCustomerById(id, customer); + return new ResponseEntity<>(returnCustomer, HttpStatus.OK); + } + + //Need Get customer that owns the specified account + //findCustomerByAccount(/accounts/{accountId}/customer + +} diff --git a/src/main/java/io/zipcoder/bank/controller/DepositController.java b/src/main/java/io/zipcoder/bank/controller/DepositController.java new file mode 100644 index 0000000..b807cf2 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/controller/DepositController.java @@ -0,0 +1,45 @@ +package io.zipcoder.bank.controller; + +import io.zipcoder.bank.model.Deposit; +import io.zipcoder.bank.service.DepositService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@RestController +public class DepositController { + + private DepositService depositService; + + public DepositController(DepositService depositService){ + this.depositService = depositService; + } + + @RequestMapping(value = "/deposits/{id}", method = RequestMethod.GET) + public ResponseEntity findDepositByDepositId(@PathVariable("id") Integer id) { + Deposit deposit = depositService.findDepositByDepositId(id); + return new ResponseEntity<>(deposit, HttpStatus.OK); + } + + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createDeposit(@RequestBody Deposit deposit) { + Deposit savedDeposit = depositService.createDeposit(deposit); + return new ResponseEntity<>(savedDeposit, HttpStatus.CREATED); + } + + @RequestMapping(value = "/deposits/{id}", method = RequestMethod.PUT) + public ResponseEntity updateDepositById(@RequestBody Deposit deposit, @PathVariable("id") Integer id) { + Deposit returnDeposit = depositService.updateDepositByDepositId(id, deposit); + return new ResponseEntity<>(returnDeposit, HttpStatus.OK); + } + + @RequestMapping(value = "/deposits/{id}", method = RequestMethod.DELETE) + public ResponseEntity deleteDepositByDepositId(@PathVariable("id") Integer id) { + depositService.deleteDepositByDepositId(id); + return new ResponseEntity<>(HttpStatus.OK); + } + + // Get all deposits for a specific account +} diff --git a/src/main/java/io/zipcoder/bank/controller/WithdrawalController.java b/src/main/java/io/zipcoder/bank/controller/WithdrawalController.java new file mode 100644 index 0000000..92c203d --- /dev/null +++ b/src/main/java/io/zipcoder/bank/controller/WithdrawalController.java @@ -0,0 +1,47 @@ +package io.zipcoder.bank.controller; + +import io.zipcoder.bank.model.Deposit; +import io.zipcoder.bank.model.Withdrawal; +import io.zipcoder.bank.service.WithdrawalService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(value = "/api/withdrawals") +public class WithdrawalController { + + private WithdrawalService withdrawalService; + + public WithdrawalController(WithdrawalService withdrawalService){ + this.withdrawalService = withdrawalService; + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET) + public ResponseEntity findAccountById(@PathVariable("id") Integer id) { + Withdrawal withdrawal = withdrawalService.findWithdrawalByWithdrawalId(id); + return new ResponseEntity<>(withdrawal, HttpStatus.OK); + } + + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createAccount(@RequestBody Withdrawal withdrawal) { + Withdrawal savedWithdrawal = withdrawalService.createWithdrawal(withdrawal); + return new ResponseEntity<>(savedWithdrawal, HttpStatus.CREATED); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.PUT) + public ResponseEntity updateAccountById(@RequestBody Withdrawal withdrawal, @PathVariable("id") Integer id) { + Withdrawal returnWithdrawal = withdrawalService.updateWithdrawalByWithdrawalId(id, withdrawal); + return new ResponseEntity<>(returnWithdrawal, HttpStatus.OK); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public ResponseEntity deleteAccountByDepositId(@PathVariable("id") Integer id) { + withdrawalService.deleteWithdrawalByWithdrawalId(id); + return new ResponseEntity<>(HttpStatus.OK); + } + + //Get all withdrawals for a specific account + +} diff --git a/src/main/java/io/zipcoder/bank/enums/Medium.java b/src/main/java/io/zipcoder/bank/enums/Medium.java new file mode 100644 index 0000000..e612317 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/enums/Medium.java @@ -0,0 +1,21 @@ +package io.zipcoder.bank.enums; + +public enum Medium { + + BALANCE("BALANCE"), + REWARDS("REWARDS"); + + private String value; + + Medium(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/java/io/zipcoder/bank/enums/Status.java b/src/main/java/io/zipcoder/bank/enums/Status.java new file mode 100644 index 0000000..71dd987 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/enums/Status.java @@ -0,0 +1,23 @@ +package io.zipcoder.bank.enums; + +public enum Status { + + PENDING("PENDING"), + CANCELLED("CANCELLED"), + COMPLETED("COMPLETED"), + RECURRING("RECURRING"); + + private String value; + + Status(String value){ + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/java/io/zipcoder/bank/enums/Type.java b/src/main/java/io/zipcoder/bank/enums/Type.java new file mode 100644 index 0000000..ce41a4e --- /dev/null +++ b/src/main/java/io/zipcoder/bank/enums/Type.java @@ -0,0 +1,32 @@ +package io.zipcoder.bank.enums; + +public enum Type { + +// SAVINGS("SAVINGS"), +// CHECKING("CHECKING"), +// CREDIT("CREDIT"), +// P2P("P2P"), +// DEPOSIT("DEPOSIT"), +// WITHDRAWAL("WITHDRAWAL"); + + SAVINGS, + CHECKING, + CREDIT, + P2P, + DEPOSIT, + WITHDRAWAL; + + //private String value; + +// Type(String value) { +// this.value = value; +// } +// +// public String getValue() { +// return value; +// } +// +// public void setValue(String value) { +// this.value = value; +// } +} diff --git a/src/main/java/io/zipcoder/bank/model/Account.java b/src/main/java/io/zipcoder/bank/model/Account.java new file mode 100644 index 0000000..3337a9a --- /dev/null +++ b/src/main/java/io/zipcoder/bank/model/Account.java @@ -0,0 +1,100 @@ +package io.zipcoder.bank.model; + +import io.zipcoder.bank.enums.Type; +import io.zipcoder.core.model.BaseEntity; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.persistence.*; + +@Entity +public class Account extends BaseEntity { + + private static final Integer serialVersionUID = 12345; + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "ID") + private Integer id; + + + @Column(name = "TYPE") + @Enumerated(value = EnumType.STRING) + private Type type; + + @NotEmpty + @Column(name = "NICKNAME") + private String nickname; + + @Column(name = "REWARDS") + private Integer rewards; + + @Column(name = "BALANCE") + private Double balance; + + @Column(name = "CUSTOMER_ID") + private Integer customerId; + + @ManyToOne + @JoinColumn(name = "CUSTOMER_ID", insertable = false, updatable = false) + private Customer customer; + + public Account(){ + + } + + public Account(Integer id, String nickname, Integer rewards, Double balance) { + this.id = id; + this.nickname = nickname; + this.rewards = rewards; + this.balance = balance; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public Integer getRewards() { + return rewards; + } + + public void setRewards(Integer rewards) { + this.rewards = rewards; + } + + public Double getBalance() { + return balance; + } + + public void setBalance(Double balance) { + this.balance = balance; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public Integer getCustomerId() { + return customerId; + } + + public void setCustomerId(Integer customerId) { + this.customerId = customerId; + } + +} diff --git a/src/main/java/io/zipcoder/bank/model/Address.java b/src/main/java/io/zipcoder/bank/model/Address.java new file mode 100644 index 0000000..7a2ea67 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/model/Address.java @@ -0,0 +1,89 @@ +package io.zipcoder.bank.model; + +import javax.persistence.*; +import java.util.Collection; + +@Entity +public class Address { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "ID") + private Integer id; + + @Column(name = "STREET_NUMBER") + private String street_number; + + @Column(name = "STREET_NAME") + private String street_name; + + @Column(name = "CITY") + private String city; + + @Column(name = "STATE") + private String state; + + @Column(name = "ZIP") + private String zip; + + @OneToMany(mappedBy = "address") + private Collection customers; + + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getStreet_number() { + return street_number; + } + + public void setStreet_number(String street_number) { + this.street_number = street_number; + } + + public String getStreet_name() { + return street_name; + } + + public void setStreet_name(String street_name) { + this.street_name = street_name; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getZip() { + return zip; + } + + public void setZip(String zip) { + this.zip = zip; + } + + public Collection getCustomers() { + return customers; + } + + public void setCustomers(Collection customers) { + this.customers = customers; + } + +} diff --git a/src/main/java/io/zipcoder/bank/model/Bill.java b/src/main/java/io/zipcoder/bank/model/Bill.java new file mode 100644 index 0000000..1249a2f --- /dev/null +++ b/src/main/java/io/zipcoder/bank/model/Bill.java @@ -0,0 +1,123 @@ +package io.zipcoder.bank.model; + +import io.zipcoder.bank.enums.Status; + +import javax.persistence.*; + + +@Entity +public class Bill { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "ID") + private Integer Id; + + @Column(name = "STATUS") + private Status status; + + @Column(name = "PAYEE") + private String payee; + + @Column(name = "NICKNAME") + private String nickname; + + @Column(name = "CREATION_DATE") + private String creation_date; + + @Column(name = "PAYMENT_DATE") + private String payment_date; + + @Column(name = "RECURRING_DATE") + private Integer recurring_date; + + @Column(name = "UPCOMING_PAYMENT_DATE") + private String upcoming_payment_date; + + @Column(name = "PAYMENT_AMOUNT") + private Double payment_amount; + + @Column(name = "ACCOUNT_ID") + private Integer account_id; + + + public Integer getId() { + return Id; + } + + public void setId(Integer id) { + Id = id; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getPayee() { + return payee; + } + + public void setPayee(String payee) { + this.payee = payee; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getCreation_date() { + return creation_date; + } + + public void setCreation_date(String creation_date) { + this.creation_date = creation_date; + } + + public String getPayment_date() { + return payment_date; + } + + public void setPayment_date(String payment_date) { + this.payment_date = payment_date; + } + + public Integer getRecurring_date() { + return recurring_date; + } + + public void setRecurring_date(Integer recurring_date) { + this.recurring_date = recurring_date; + } + + public String getUpcoming_payment_date() { + return upcoming_payment_date; + } + + public void setUpcoming_payment_date(String upcoming_payment_date) { + this.upcoming_payment_date = upcoming_payment_date; + } + + public Double getPayment_amount() { + return payment_amount; + } + + public void setPayment_amount(Double payment_amount) { + this.payment_amount = payment_amount; + } + + public Integer getAccount_id() { + return account_id; + } + + public void setAccount_id(Integer account_id) { + this.account_id = account_id; + } +} diff --git a/src/main/java/io/zipcoder/bank/model/Customer.java b/src/main/java/io/zipcoder/bank/model/Customer.java new file mode 100644 index 0000000..11809fb --- /dev/null +++ b/src/main/java/io/zipcoder/bank/model/Customer.java @@ -0,0 +1,71 @@ +package io.zipcoder.bank.model; + +import javax.persistence.*; +import java.util.Collection; + +@Entity +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "ID") + private Integer id; + + @Column(name = "FIRST_NAME") + private String first_name; + + @Column(name = "LAST_NAME") + private String last_name; + + @Column(name = "ADDRESS_ID") + private Integer addressId; + + @OneToMany(mappedBy = "customer") + private Collection accounts; + + @ManyToOne + @JoinColumn(name = "ADDRESS_ID", insertable = false, updatable = false) + private Address address; + + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getFirst_name() { + return first_name; + } + + public void setFirst_name(String first_name) { + this.first_name = first_name; + } + + public String getLast_name() { + return last_name; + } + + public void setLast_name(String last_name) { + this.last_name = last_name; + } + + public Integer getAddressId() { + return addressId; + } + + public void setAddressId(Integer addressId) { + this.addressId = addressId; + } + + public Collection getAccounts() { + return accounts; + } + + public void setAccounts(Collection accounts) { + this.accounts = accounts; + } + +} diff --git a/src/main/java/io/zipcoder/bank/model/Deposit.java b/src/main/java/io/zipcoder/bank/model/Deposit.java new file mode 100644 index 0000000..c930445 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/model/Deposit.java @@ -0,0 +1,103 @@ +package io.zipcoder.bank.model; + +import io.zipcoder.bank.enums.Medium; +import io.zipcoder.bank.enums.Status; +import io.zipcoder.bank.enums.Type; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +public class Deposit { + + @Id + @Column(name = "ID") + private Integer id; + + @Column(name = "TYPE") + private Type type; + + @Column(name = "TRANSACTION_DATE") + private String transaction_date; + + @Column(name = "STATUS") + private Status status; + + @Column(name = "PAYEE_ID") + private Integer payee_id; + + @Column(name = "MEDIUM") + private Medium medium; + + @Column(name = "AMOUNT") + private Double amount; + + @Column(name = "DESCRIPTION") + private String description; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public String getTransaction_date() { + return transaction_date; + } + + public void setTransaction_date(String transaction_date) { + this.transaction_date = transaction_date; + } + + public Status getStatus() { + return this.status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public Integer getPayee_id() { + return payee_id; + } + + public void setPayee_id(Integer payee_id) { + this.payee_id = payee_id; + } + + public Medium getMedium() { + return this.medium; + } + + public void setMedium(Medium medium) { + this.medium = medium; + } + + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/src/main/java/io/zipcoder/bank/model/Withdrawal.java b/src/main/java/io/zipcoder/bank/model/Withdrawal.java new file mode 100644 index 0000000..03fe163 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/model/Withdrawal.java @@ -0,0 +1,104 @@ +package io.zipcoder.bank.model; + + +import io.zipcoder.bank.enums.Medium; +import io.zipcoder.bank.enums.Status; +import io.zipcoder.bank.enums.Type; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +public class Withdrawal { + + @Id + @Column(name = "ID") + private Integer id; + + @Column(name = "TYPE") + private Type type; + + @Column(name = "TRANSACTION_DATE") + private String transaction_date; + + @Column(name = "STATUS") + private Status status; + + @Column(name = "PAYER_ID") + private Integer payer_id; + + @Column(name = "MEDIUM") + private Medium medium; + + @Column(name = "AMOUNT") + private Double amount; + + @Column(name = "DESCRIPTION") + private String description; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public String getTransaction_date() { + return transaction_date; + } + + public void setTransaction_date(String transaction_date) { + this.transaction_date = transaction_date; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public Integer getPayer_id() { + return payer_id; + } + + public void setPayer_id(Integer payer_id) { + this.payer_id = payer_id; + } + + public Medium getMedium() { + return medium; + } + + public void setMedium(Medium medium) { + this.medium = medium; + } + + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/src/main/java/io/zipcoder/bank/repository/AccountRepository.java b/src/main/java/io/zipcoder/bank/repository/AccountRepository.java new file mode 100644 index 0000000..ee0e76c --- /dev/null +++ b/src/main/java/io/zipcoder/bank/repository/AccountRepository.java @@ -0,0 +1,10 @@ +package io.zipcoder.bank.repository; + +import io.zipcoder.bank.model.Account; +import io.zipcoder.core.repository.BaseRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AccountRepository extends BaseRepository { + +} diff --git a/src/main/java/io/zipcoder/bank/repository/BillRepository.java b/src/main/java/io/zipcoder/bank/repository/BillRepository.java new file mode 100644 index 0000000..483aef4 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/repository/BillRepository.java @@ -0,0 +1,14 @@ +package io.zipcoder.bank.repository; + +import io.zipcoder.bank.model.Bill; +import io.zipcoder.core.repository.BaseRepository; +import org.springframework.stereotype.Repository; + +import java.util.Collection; + +@Repository +public interface BillRepository extends BaseRepository { + +// Collection findByAccount_id(int accountId); + +} diff --git a/src/main/java/io/zipcoder/bank/repository/CustomerRepository.java b/src/main/java/io/zipcoder/bank/repository/CustomerRepository.java new file mode 100644 index 0000000..3aa13f8 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/repository/CustomerRepository.java @@ -0,0 +1,10 @@ +package io.zipcoder.bank.repository; + +import io.zipcoder.bank.model.Customer; +import io.zipcoder.core.repository.BaseRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CustomerRepository extends BaseRepository{ + +} diff --git a/src/main/java/io/zipcoder/bank/repository/DepositRepository.java b/src/main/java/io/zipcoder/bank/repository/DepositRepository.java new file mode 100644 index 0000000..a613f1f --- /dev/null +++ b/src/main/java/io/zipcoder/bank/repository/DepositRepository.java @@ -0,0 +1,10 @@ +package io.zipcoder.bank.repository; + +import io.zipcoder.bank.model.Bill; +import io.zipcoder.bank.model.Deposit; +import io.zipcoder.core.repository.BaseRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DepositRepository extends BaseRepository { +} diff --git a/src/main/java/io/zipcoder/bank/repository/WithdrawalRepo.java b/src/main/java/io/zipcoder/bank/repository/WithdrawalRepo.java new file mode 100644 index 0000000..a15157f --- /dev/null +++ b/src/main/java/io/zipcoder/bank/repository/WithdrawalRepo.java @@ -0,0 +1,10 @@ +package io.zipcoder.bank.repository; + +import io.zipcoder.bank.model.Withdrawal; +import io.zipcoder.core.repository.BaseRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface WithdrawalRepo extends BaseRepository { + +} diff --git a/src/main/java/io/zipcoder/bank/service/AccountService.java b/src/main/java/io/zipcoder/bank/service/AccountService.java new file mode 100644 index 0000000..f832cb4 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/service/AccountService.java @@ -0,0 +1,41 @@ +package io.zipcoder.bank.service; + +import io.zipcoder.bank.model.Account; +import io.zipcoder.bank.repository.AccountRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collection; + +@Service +public class AccountService { + + private AccountRepository accountRepository; + + @Autowired + public AccountService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + public Account createAccount(Account account) { + return accountRepository.save(account); + } + + public Collection findAllAccounts() { + return (Collection) accountRepository.findAll(); + } + + public Account findAccountById(Integer id) { + return accountRepository.findOne(id); + } + + public Account updateAccountById(Integer id, Account account) { + account.setId(id); + return accountRepository.save(account); + } + + public void deleteAccountById(Integer id) { + accountRepository.delete(id); + } + +} diff --git a/src/main/java/io/zipcoder/bank/service/BillService.java b/src/main/java/io/zipcoder/bank/service/BillService.java new file mode 100644 index 0000000..0f41bb9 --- /dev/null +++ b/src/main/java/io/zipcoder/bank/service/BillService.java @@ -0,0 +1,52 @@ +package io.zipcoder.bank.service; + +import io.zipcoder.bank.model.Account; +import io.zipcoder.bank.model.Bill; +import io.zipcoder.bank.repository.AccountRepository; +import io.zipcoder.bank.repository.BillRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collection; + +@Service +public class BillService { + + private BillRepository billRepository; + private AccountService accountService; + + @Autowired + public BillService(BillRepository billRepository, AccountService accountService) { + this.billRepository = billRepository; + this.accountService = accountService; + } + + public Bill createBill(Bill bill) { + return billRepository.save(bill); + } + + public Bill findBillByBillId(Integer id) { + return billRepository.findOne(id); + } + + public Bill updateBillByBillId(Integer id, Bill bill) { + bill.setId(id); + return billRepository.save(bill); + } + + public Collection findAllBills() { + + return (Collection) billRepository.findAll(); + } + + public void deleteBillByBillId(Integer id) { + billRepository.delete(id); + } + +// public Collection findAllBillsByAccount(Integer id) { +// return billRepository.findByAccount_id(id); +// } + +} diff --git a/src/main/java/io/zipcoder/bank/service/CustomerService.java b/src/main/java/io/zipcoder/bank/service/CustomerService.java new file mode 100644 index 0000000..67cd1ed --- /dev/null +++ b/src/main/java/io/zipcoder/bank/service/CustomerService.java @@ -0,0 +1,48 @@ +package io.zipcoder.bank.service; + +import io.zipcoder.bank.model.Account; +import io.zipcoder.bank.model.Customer; +import io.zipcoder.bank.repository.CustomerRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collection; + +@Service +public class CustomerService { + + private CustomerRepository customerRepository; + + @Autowired + public CustomerService(CustomerRepository customerRepository) { + + this.customerRepository = customerRepository; + } + + public Customer createCustomer(Customer customer) { + return customerRepository.save(customer); + } + + public Collection findAllCustomers() { + return (Collection) customerRepository.findAll(); + } + + public Customer findCustomerById(Integer id) { + return customerRepository.findOne(id); + } + + public Customer updateCustomerById(Integer id, Customer customer) { + customer.setId(id); + return customerRepository.save(customer); + } + + public void deleteCustomerById(Integer id) { + customerRepository.delete(id); + } + +// public Customer findCustomerByAccount(Account account) { +// return customerRepository.findOne(account); +// } + + +} diff --git a/src/main/java/io/zipcoder/bank/service/DepositService.java b/src/main/java/io/zipcoder/bank/service/DepositService.java new file mode 100644 index 0000000..b9a726c --- /dev/null +++ b/src/main/java/io/zipcoder/bank/service/DepositService.java @@ -0,0 +1,38 @@ +package io.zipcoder.bank.service; + +import io.zipcoder.bank.model.Deposit; +import io.zipcoder.bank.repository.DepositRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DepositService { + + private DepositRepository depositRepository; + + @Autowired + public DepositService(DepositRepository depositRepository){ + this.depositRepository = depositRepository; + } + + public Deposit createDeposit(Deposit deposit) { + return depositRepository.save(deposit); + } + + public Deposit findDepositByDepositId(Integer id) { + return depositRepository.findOne(id); + } + + public Deposit updateDepositByDepositId(Integer id, Deposit deposit) { + deposit.setId(id); + return depositRepository.save(deposit); + } + + public void deleteDepositByDepositId(Integer id) { + depositRepository.delete(id); + } + + + //Get all deposits for a specific account + +} diff --git a/src/main/java/io/zipcoder/bank/service/WithdrawalService.java b/src/main/java/io/zipcoder/bank/service/WithdrawalService.java new file mode 100644 index 0000000..6eb1a1d --- /dev/null +++ b/src/main/java/io/zipcoder/bank/service/WithdrawalService.java @@ -0,0 +1,38 @@ +package io.zipcoder.bank.service; + +import io.zipcoder.bank.model.Withdrawal; +import io.zipcoder.bank.repository.WithdrawalRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class WithdrawalService { + + private WithdrawalRepo withdrawalRepo; + + @Autowired + public WithdrawalService(WithdrawalRepo withdrawalRepo){ + this.withdrawalRepo = withdrawalRepo; + } + + public Withdrawal createWithdrawal(Withdrawal withdrawal) { + return withdrawalRepo.save(withdrawal); + } + + public Withdrawal findWithdrawalByWithdrawalId(Integer id) { + return withdrawalRepo.findOne(id); + } + + public Withdrawal updateWithdrawalByWithdrawalId(Integer id, Withdrawal withdrawal) { + withdrawal.setId(id); + return withdrawalRepo.save(withdrawal); + } + + public void deleteWithdrawalByWithdrawalId(Integer id) { + withdrawalRepo.delete(id); + } + + + //Get all deposits for a specific account + +} diff --git a/src/main/java/io/zipcoder/core/model/BaseEntity.java b/src/main/java/io/zipcoder/core/model/BaseEntity.java new file mode 100644 index 0000000..65b9ee2 --- /dev/null +++ b/src/main/java/io/zipcoder/core/model/BaseEntity.java @@ -0,0 +1,6 @@ +package io.zipcoder.core.model; + +import java.io.Serializable; + +public abstract class BaseEntity implements Serializable { +} diff --git a/src/main/java/io/zipcoder/core/repository/BaseRepository.java b/src/main/java/io/zipcoder/core/repository/BaseRepository.java new file mode 100644 index 0000000..6709415 --- /dev/null +++ b/src/main/java/io/zipcoder/core/repository/BaseRepository.java @@ -0,0 +1,10 @@ +package io.zipcoder.core.repository; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.NoRepositoryBean; + +import java.io.Serializable; + +@NoRepositoryBean +public interface BaseRepository extends CrudRepository { +} diff --git a/src/main/java/io/zipcoder/core/security/WebSecurityConfig.java b/src/main/java/io/zipcoder/core/security/WebSecurityConfig.java new file mode 100644 index 0000000..cd12c12 --- /dev/null +++ b/src/main/java/io/zipcoder/core/security/WebSecurityConfig.java @@ -0,0 +1,34 @@ +package io.zipcoder.core.security; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity + .authorizeRequests() + .anyRequest() + .authenticated() + .and() + .formLogin() + .and() + .httpBasic(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("Toofast") + .password("xxx") + .roles("USER"); + } + +} diff --git a/src/main/resources/application-h2.properties b/src/main/resources/application-h2.properties new file mode 100644 index 0000000..74765cc --- /dev/null +++ b/src/main/resources/application-h2.properties @@ -0,0 +1,4 @@ +spring.datasource.url=jdbc:h2:mem:testdb;Mode=Oracle +spring.datasource.platform=h2 +spring.jpa.hibernate.ddl-auto=none +spring.datasource.continue-on-error=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29..dde67ef 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.profiles.active=h2 + +spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect \ No newline at end of file diff --git a/src/main/resources/schema-h2.sql b/src/main/resources/schema-h2.sql new file mode 100644 index 0000000..6c58b04 --- /dev/null +++ b/src/main/resources/schema-h2.sql @@ -0,0 +1,90 @@ +DROP TABLE IF EXISTS ACCOUNT; + +CREATE TABLE ACCOUNTS ( + ID INT NOT NULL AUTO_INCREMENT, + TYPE VARCHAR2(255) NOT NULL DEFAULT '', + NICKNAME VARCHAR2(255) NOT NULL DEFAULT '', + REWARDS INT DEFAULT NULL, + BALANCE DOUBLE DEFAULT NULL, + CUSTOMER_ID INT NOT NULL DEFAULT '', + PRIMARY KEY (ID), + FOREIGN KEY (CUSTOMER_ID) REFERENCES CUSTOMER(ID) +); + + + +DROP TABLE IF EXISTS CUSTOMER; + +CREATE TABLE CUSTOMER ( + ID INT NOT NULL AUTO_INCREMENT, + FIRST_NAME VARCHAR(255), + LAST_NAME VARCHAR(255), + ADDRESS_ID INT NOT NULL DEFAULT '', + PRIMARY KEY (ID), + FOREIGN KEY (ADDRESS_ID) REFERENCES ADDRESS(ID) +); + + + +DROP TABLE IF EXISTS ADDRESS; + +CREATE TABLE ADDRESS ( + ID INT NOT NULL AUTO_INCREMENT, + STREET_NUMBER VARCHAR(255), + STREET_NAME VARCHAR(255), + CITY VARCHAR(255), + STATE VARCHAR (2), + ZIP VARCHAR(5) +); + + + +DROP TABLE IF EXISTS BILL; + +CREATE TABLE BILL ( + ID INT NOT NULL AUTO_INCREMENT, + STATUS VARCHAR (255), + PAYEE VARCHAR (255), + NICKNAME VARCHAR (255), + CREATION_DATE VARCHAR (255), + PAYMENT_DATE VARCHAR (255), + RECURRING_DATE VARCHAR (255), + UPCOMING_PAYMENT_DATE VARCHAR(255), + PAYMENT_AMOUNT DOUBLE (2), + ACCOUNT_ID INT, + PRIMARY KEY (ID), + FOREIGN KEY (ACCOUNT_ID) REFERENCES ACCOUNTS(ID) +); + + + +DROP TABLE IF EXISTS DEPOSIT; + +CREATE TABLE DEPOSIT ( + ID INT NOT NULL AUTO_INCREMENT, + TYPE VARCHAR(255), + TRANSACTION_DATE VARCHAR(255), + STATUS VARCHAR(255), + PAYEE_ID INT, + MEDIUM VARCHAR(255), + AMOUNT DOUBLE, + DESCRIPTION TEXT +); + + + +DROP TABLE IF EXISTS WITHDRAWAL; + +CREATE TABLE WITHDRAWAL ( + ID INT NOT NULL AUTO_INCREMENT, + TYPE VARCHAR(255), + TRANSACTION_DATE VARCHAR(255), + STATUS VARCHAR(255), + PAYER_ID INT, + MEDIUM VARCHAR(255), + AMOUNT DOUBLE, + DESCRIPTION TEXT +); + + + diff --git a/src/test/java/io/zipcoder/controller/AccountControllerTest.java b/src/test/java/io/zipcoder/controller/AccountControllerTest.java new file mode 100644 index 0000000..65871f3 --- /dev/null +++ b/src/test/java/io/zipcoder/controller/AccountControllerTest.java @@ -0,0 +1,76 @@ +package io.zipcoder.controller; + +import io.zipcoder.bank.controller.AccountController; +import io.zipcoder.bank.model.Account; +import io.zipcoder.bank.service.AccountService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import util.BaseControllerTest; + +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +public class AccountControllerTest extends BaseControllerTest { + + @MockBean + private AccountService accountService; + + @InjectMocks + private AccountController accountController; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + mvc = MockMvcBuilders.standaloneSetup(accountController).build(); + baseEndpointUrl = "/api/accounts"; + entity = new Account(3,"Joey", 4, 3.0); + initDependentVariables(); + } + + @Test + public void testCreateAccount() throws Exception { + when(accountService.createAccount(entity)) + .thenReturn(entity); + mvcPerformPostWithNoPathVariables(baseEndpointUrl, entityAsJsonString); + System.out.println(entityAsJsonString); + } + + @Test + public void testFindAllAccounts() throws Exception { + when(accountService.findAllAccounts()) + .thenReturn(entityCollection); + returnedEntity = mvcPerformGetWithNoPathVariables(baseEndpointUrl); + Assert.assertEquals(entityNotReturnedMessage, entityCollectionAsJsonString, returnedEntity); + System.out.println(returnedEntity); + } + + @Test + public void testFindAccountById() throws Exception { + when(accountService.findAccountById(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformGetWithOnePathVariable(idEndpointUrl, childEntityId); + Assert.assertEquals(entityNotReturnedMessage, entityAsJsonString, returnedEntity); + } + + @Test + public void testUpdateAccountById() throws Exception { + when(accountService.updateAccountById(childEntityId, entity)) + .thenReturn(entity); + mvcPerformUpdateWithOnePathVariable(idEndpointUrl, childEntityId, entityAsJsonString); + } + + @Test + public void testDeleteAccountById() throws Exception { + when(accountService.findAccountById(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformDeleteWithOnePathVariable(idEndpointUrl, childEntityId); + } + +} diff --git a/src/test/java/io/zipcoder/controller/BillControllerTest.java b/src/test/java/io/zipcoder/controller/BillControllerTest.java new file mode 100644 index 0000000..182cd87 --- /dev/null +++ b/src/test/java/io/zipcoder/controller/BillControllerTest.java @@ -0,0 +1,76 @@ +package io.zipcoder.controller; + +import io.zipcoder.bank.controller.BillController; +import io.zipcoder.bank.model.Bill; +import io.zipcoder.bank.service.BillService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import util.BaseControllerTest; + +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +public class BillControllerTest extends BaseControllerTest { + + @MockBean + private BillService billService; + + @InjectMocks + private BillController billController; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + mvc = MockMvcBuilders.standaloneSetup(billController).build(); + baseEndpointUrl = "/api/bills"; + entity = new Bill(); + initDependentVariables(); + } + + @Test + public void testCreateBill() throws Exception { + when(billService.createBill(entity)) + .thenReturn(entity); + mvcPerformPostWithNoPathVariables(baseEndpointUrl, entityAsJsonString); + System.out.println(entityAsJsonString); + } + + @Test + public void testFindAllBills() throws Exception { + when(billService.findAllBills()) + .thenReturn(entityCollection); + returnedEntity = mvcPerformGetWithNoPathVariables(baseEndpointUrl); + Assert.assertEquals(entityNotReturnedMessage, entityCollectionAsJsonString, returnedEntity); + System.out.println(returnedEntity); + } + + @Test + public void testFindBillById() throws Exception { + when(billService.findBillByBillId(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformGetWithOnePathVariable(idEndpointUrl, childEntityId); + Assert.assertEquals(entityNotReturnedMessage, entityAsJsonString, returnedEntity); + } + + @Test + public void testUpdateBillById() throws Exception { + when(billService.updateBillByBillId(childEntityId, entity)) + .thenReturn(entity); + mvcPerformUpdateWithOnePathVariable(idEndpointUrl, childEntityId, entityAsJsonString); + } + + @Test + public void testDeleteAccountById() throws Exception { + when(billService.findBillByBillId(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformDeleteWithOnePathVariable(idEndpointUrl, childEntityId); + } + +} diff --git a/src/test/java/io/zipcoder/controller/CustomerControllerTest.java b/src/test/java/io/zipcoder/controller/CustomerControllerTest.java new file mode 100644 index 0000000..7b7bfc5 --- /dev/null +++ b/src/test/java/io/zipcoder/controller/CustomerControllerTest.java @@ -0,0 +1,77 @@ +package io.zipcoder.controller; + +import io.zipcoder.bank.controller.CustomerController; +import io.zipcoder.bank.model.Customer; +import io.zipcoder.bank.service.CustomerService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import util.BaseControllerTest; + +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +public class CustomerControllerTest extends BaseControllerTest { + + @MockBean + private CustomerService customerService; + + @InjectMocks + private CustomerController customerController; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + mvc = MockMvcBuilders.standaloneSetup(customerController).build(); + baseEndpointUrl = "/api/customers"; + entity = new Customer(); + initDependentVariables(); + } + + @Test + public void testCreateCustomer() throws Exception { + when(customerService.createCustomer(entity)) + .thenReturn(entity); + mvcPerformPostWithNoPathVariables(baseEndpointUrl, entityAsJsonString); + System.out.println(entityAsJsonString); + } + + @Test + public void testFindAllCustomers() throws Exception { + when(customerService.findAllCustomers()) + .thenReturn(entityCollection); + returnedEntity = mvcPerformGetWithNoPathVariables(baseEndpointUrl); + Assert.assertEquals(entityNotReturnedMessage, entityCollectionAsJsonString, returnedEntity); + System.out.println(returnedEntity); + } + + @Test + public void testFindCustomerById() throws Exception { + when(customerService.findCustomerById(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformGetWithOnePathVariable(idEndpointUrl, childEntityId); + Assert.assertEquals(entityNotReturnedMessage, entityAsJsonString, returnedEntity); + } + + @Test + public void testUpdateCustomerById() throws Exception { + when(customerService.updateCustomerById(childEntityId, entity)) + .thenReturn(entity); + mvcPerformUpdateWithOnePathVariable(idEndpointUrl, childEntityId, entityAsJsonString); + } + + // No ability to remove customer yet, un-note this test if that is added +// @Test +// public void testDeleteCustomerById() throws Exception { +// when(customerService.findCustomerById(childEntityId)) +// .thenReturn(entity); +// returnedEntity = mvcPerformDeleteWithOnePathVariable(idEndpointUrl, childEntityId); +// } + +} diff --git a/src/test/java/io/zipcoder/controller/DepositControllerTest.java b/src/test/java/io/zipcoder/controller/DepositControllerTest.java new file mode 100644 index 0000000..fa4bfe3 --- /dev/null +++ b/src/test/java/io/zipcoder/controller/DepositControllerTest.java @@ -0,0 +1,67 @@ +package io.zipcoder.controller; + +import io.zipcoder.bank.controller.DepositController; +import io.zipcoder.bank.model.Deposit; +import io.zipcoder.bank.service.DepositService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import util.BaseControllerTest; + +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +public class DepositControllerTest extends BaseControllerTest { + + @MockBean + private DepositService depositService; + + @InjectMocks + private DepositController depositController; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + mvc = MockMvcBuilders.standaloneSetup(depositController).build(); + baseEndpointUrl = "/deposits"; + entity = new Deposit(); + initDependentVariables(); + } + + @Test + public void testCreateDeposit() throws Exception { + when(depositService.createDeposit(entity)) + .thenReturn(entity); + mvcPerformPostWithNoPathVariables(baseEndpointUrl, entityAsJsonString); + System.out.println(entityAsJsonString); + } + + @Test + public void testFindDepositById() throws Exception { + when(depositService.findDepositByDepositId(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformGetWithOnePathVariable(idEndpointUrl, childEntityId); + Assert.assertEquals(entityNotReturnedMessage, entityAsJsonString, returnedEntity); + } + + @Test + public void testUpdateDepositById() throws Exception { + when(depositService.updateDepositByDepositId(childEntityId, entity)) + .thenReturn(entity); + mvcPerformUpdateWithOnePathVariable(idEndpointUrl, childEntityId, entityAsJsonString); + } + + @Test + public void testDeleteDepositById() throws Exception { + when(depositService.findDepositByDepositId(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformDeleteWithOnePathVariable(idEndpointUrl, childEntityId); + } + +} diff --git a/src/test/java/io/zipcoder/controller/WithdrawalControllerTest.java b/src/test/java/io/zipcoder/controller/WithdrawalControllerTest.java new file mode 100644 index 0000000..b14f926 --- /dev/null +++ b/src/test/java/io/zipcoder/controller/WithdrawalControllerTest.java @@ -0,0 +1,75 @@ +package io.zipcoder.controller; + +import io.zipcoder.bank.controller.WithdrawalController; +import io.zipcoder.bank.model.Withdrawal; +import io.zipcoder.bank.service.WithdrawalService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import util.BaseControllerTest; + +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +public class WithdrawalControllerTest extends BaseControllerTest { + + @MockBean + private WithdrawalService withdrawalService; + + @InjectMocks + private WithdrawalController withdrawalController; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + mvc = MockMvcBuilders.standaloneSetup(withdrawalController).build(); + baseEndpointUrl = "/api/withdrawals"; + entity = new Withdrawal(); + initDependentVariables(); + } + + @Test + public void testCreateWithdrawal() throws Exception { + when(withdrawalService.createWithdrawal(entity)) + .thenReturn(entity); + mvcPerformPostWithNoPathVariables(baseEndpointUrl, entityAsJsonString); + System.out.println(entityAsJsonString); + } + +// @Test +// public void testFindAllAccounts() throws Exception { +// when(accountService.findAllAccounts()) +// .thenReturn(entityCollection); +// returnedEntity = mvcPerformGetWithNoPathVariables(baseEndpointUrl); +// Assert.assertEquals(entityNotReturnedMessage, entityCollectionAsJsonString, returnedEntity); +// System.out.println(returnedEntity); +// } + + @Test + public void testFindWithdrawalById() throws Exception { + when(withdrawalService.findWithdrawalByWithdrawalId(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformGetWithOnePathVariable(idEndpointUrl, childEntityId); + Assert.assertEquals(entityNotReturnedMessage, entityAsJsonString, returnedEntity); + } + + @Test + public void testUpdateWithdrawalById() throws Exception { + when(withdrawalService.updateWithdrawalByWithdrawalId(childEntityId, entity)) + .thenReturn(entity); + mvcPerformUpdateWithOnePathVariable(idEndpointUrl, childEntityId, entityAsJsonString); + } + + @Test + public void testDeleteAccountById() throws Exception { + when(withdrawalService.findWithdrawalByWithdrawalId(childEntityId)) + .thenReturn(entity); + returnedEntity = mvcPerformDeleteWithOnePathVariable(idEndpointUrl, childEntityId); + } +} diff --git a/src/test/java/io/zipcoder/model/AccountTest.java b/src/test/java/io/zipcoder/model/AccountTest.java new file mode 100644 index 0000000..c56c7c3 --- /dev/null +++ b/src/test/java/io/zipcoder/model/AccountTest.java @@ -0,0 +1,59 @@ +package io.zipcoder.model; + +import io.zipcoder.bank.enums.Type; +import io.zipcoder.bank.model.Account; +import io.zipcoder.bank.model.Customer; +import org.junit.Assert; +import org.junit.Test; + +public class AccountTest { + + @Test + public void accountIdTest() { + Account account = new Account(); + account.setId(1); + Integer expected = 1; + Assert.assertEquals(expected, account.getId()); + } + + @Test + public void accountNicknameTest() { + Account account = new Account(); + account.setNickname("Account One"); + String expected = "Account One"; + Assert.assertEquals(expected, account.getNickname()); + } + + @Test + public void accountRewardsTest() { + Account account = new Account(); + account.setRewards(22); + Integer expected = 22; + Assert.assertEquals(expected, account.getRewards()); + } + + @Test + public void accountBalanceTest() { + Account account = new Account(); + account.setBalance(1923.86); + Double expected = 1923.86; + Assert.assertEquals(expected, account.getBalance()); + } + + @Test + public void accountTypeTest() { + Account account = new Account(); + account.setType(Type.CHECKING); + Type expected = Type.CHECKING; + Assert.assertEquals(expected, account.getType()); + } + + @Test + public void accountCustomerTest() { + Account account = new Account(); + account.setCustomerId(1); + Integer expected = 1; + Assert.assertEquals(expected, account.getCustomerId()); + } + +} diff --git a/src/test/java/io/zipcoder/model/AddressTest.java b/src/test/java/io/zipcoder/model/AddressTest.java new file mode 100644 index 0000000..3bd4994 --- /dev/null +++ b/src/test/java/io/zipcoder/model/AddressTest.java @@ -0,0 +1,57 @@ +package io.zipcoder.model; + +import io.zipcoder.bank.model.Address; +import org.junit.Assert; +import org.junit.Test; + +public class AddressTest { + + @Test + public void addressIdTest() { + Address address = new Address(); + address.setId(1); + Integer expected = 1; + Assert.assertEquals(expected, address.getId()); + } + + @Test + public void addressStreet_numberTest() { + Address address = new Address(); + address.setStreet_number("555"); + String expected = "555"; + Assert.assertEquals(expected, address.getStreet_number()); + } + + @Test + public void addressStreet_nameTest() { + Address address = new Address(); + address.setStreet_name("Highfive Drive"); + String expected = "Highfive Drive"; + Assert.assertEquals(expected, address.getStreet_name()); + } + + @Test + public void addressCityTest() { + Address address = new Address(); + address.setCity("Here"); + String expected = "Here"; + Assert.assertEquals(expected, address.getCity()); + } + + @Test + public void addressStateTest() { + Address address = new Address(); + address.setState("Here"); + String expected = "Here"; + Assert.assertEquals(expected, address.getState()); + } + + @Test + public void addressZipTest() { + Address address = new Address(); + address.setZip("19901"); + String expected = "19901"; + Assert.assertEquals(expected, address.getZip()); + } + +} diff --git a/src/test/java/io/zipcoder/model/BillTest.java b/src/test/java/io/zipcoder/model/BillTest.java new file mode 100644 index 0000000..eb394dc --- /dev/null +++ b/src/test/java/io/zipcoder/model/BillTest.java @@ -0,0 +1,90 @@ +package io.zipcoder.model; + +import io.zipcoder.bank.enums.Status; +import io.zipcoder.bank.model.Bill; +import org.junit.Assert; +import org.junit.Test; + +public class BillTest { + + @Test + public void billIdTest() { + Bill bill = new Bill(); + bill.setId(1234); + Integer expected = 1234; + Assert.assertEquals(expected, bill.getId()); + } + + @Test + public void billStatusTest() { + Bill bill = new Bill(); + bill.setStatus(Status.PENDING); + Status expected = Status.PENDING; + Assert.assertEquals(expected, bill.getStatus()); + } + + @Test + public void billPayeeTest() { + Bill bill = new Bill(); + bill.setPayee("Mitch"); + String expected = "Mitch"; + Assert.assertEquals(expected, bill.getPayee()); + } + + @Test + public void billNicknameTest() { + Bill bill = new Bill(); + bill.setNickname("College Fund"); + String expected = "College Fund"; + Assert.assertEquals(expected, bill.getNickname()); + } + + @Test + public void billCreation_dateTest() { + Bill bill = new Bill(); + bill.setCreation_date("12/10/2015"); + String expected = "12/10/2015"; + Assert.assertEquals(expected, bill.getCreation_date()); + } + + @Test + public void billPayment_dateTest() { + Bill bill = new Bill(); + bill.setPayment_date("12/10/2015"); + String expected = "12/10/2015"; + Assert.assertEquals(expected, bill.getPayment_date()); + } + + @Test + public void billRecurring_dateTest() { + Bill bill = new Bill(); + bill.setRecurring_date(30); + Integer expected = 30; + Assert.assertEquals(expected, bill.getRecurring_date()); + } + + @Test + public void billUpcoming_payment_dateTest() { + Bill bill = new Bill(); + bill.setUpcoming_payment_date("12/10/2015"); + String expected = "12/10/2015"; + Assert.assertEquals(expected, bill.getUpcoming_payment_date()); + } + + @Test + public void billPayment_amountTest() { + Bill bill = new Bill(); + bill.setPayment_amount(55.97); + Double expected = 55.97; + Assert.assertEquals(expected, bill.getPayment_amount()); + } + + @Test + public void billAccount_idTest() { + Bill bill = new Bill(); + bill.setAccount_id(1); + Integer expected = 1; + Assert.assertEquals(expected, bill.getAccount_id()); + } + +} diff --git a/src/test/java/io/zipcoder/model/CustomerTest.java b/src/test/java/io/zipcoder/model/CustomerTest.java new file mode 100644 index 0000000..1d8adef --- /dev/null +++ b/src/test/java/io/zipcoder/model/CustomerTest.java @@ -0,0 +1,41 @@ +package io.zipcoder.model; + +import io.zipcoder.bank.model.Customer; +import org.junit.Assert; +import org.junit.Test; + +public class CustomerTest { + + @Test + public void customerIdTest() { + Customer customer = new Customer(); + customer.setId(1234); + Integer expected = 1234; + Assert.assertEquals(expected, customer.getId()); + } + + @Test + public void customerFirstNameTest() { + Customer customer = new Customer(); + customer.setFirst_name("Joe"); + String expected = "Joe"; + Assert.assertEquals(expected, customer.getFirst_name()); + } + + @Test + public void customerLastNameTest() { + Customer customer = new Customer(); + customer.setLast_name("Joe"); + String expected = "Joe"; + Assert.assertEquals(expected, customer.getLast_name()); + } + + @Test + public void customerAddressTest() { + Customer customer = new Customer(); + customer.setAddressId(1); + Integer expected = 1; + Assert.assertEquals(expected.getClass(), customer.getAddressId().getClass()); + } + +} diff --git a/src/test/java/io/zipcoder/model/DepositTest.java b/src/test/java/io/zipcoder/model/DepositTest.java new file mode 100644 index 0000000..6bd8a50 --- /dev/null +++ b/src/test/java/io/zipcoder/model/DepositTest.java @@ -0,0 +1,87 @@ +package io.zipcoder.model; + +import io.zipcoder.bank.enums.Medium; +import io.zipcoder.bank.enums.Status; +import io.zipcoder.bank.enums.Type; +import io.zipcoder.bank.model.Deposit; +import org.junit.Assert; +import org.junit.Test; + +public class DepositTest { + + @Test + public void depositIdTest() { + Deposit deposit = new Deposit(); + deposit.setId(1); + Integer expected = 1; + Integer actual = deposit.getId(); + Assert.assertEquals(expected, actual); + } + + @Test + public void depositTypeTest() { + Deposit deposit = new Deposit(); + Type type = Type.CHECKING; + deposit.setType(type); + Type expected = Type.CHECKING; + Type actual = deposit.getType(); + Assert.assertEquals(expected, actual); + } + + @Test + public void depositTransactionDateTest() { + Deposit deposit = new Deposit(); + deposit.setTransaction_date("today"); + String expected = "today"; + String actual = deposit.getTransaction_date(); + Assert.assertEquals(expected, actual); + } + + @Test + public void depositStatusTest() { + Deposit deposit = new Deposit(); + Status status = Status.PENDING; + deposit.setStatus(status); + Status expected = Status.PENDING; + Status actual = deposit.getStatus(); + Assert.assertEquals(expected, actual); + } + + @Test + public void depositPayeeIdTest() { + Deposit deposit = new Deposit(); + deposit.setPayee_id(1); + Integer expected = 1; + Integer actual = deposit.getPayee_id(); + Assert.assertEquals(expected, actual); + } + + @Test + public void depositMediumTest() { + Deposit deposit = new Deposit(); + Medium medium = Medium.BALANCE; + deposit.setMedium(medium); + Medium expected = Medium.BALANCE; + Medium actual = deposit.getMedium(); + Assert.assertEquals(expected, actual); + } + + @Test + public void depositAmountTest() { + Deposit deposit = new Deposit(); + deposit.setAmount(1.0); + Double expected = 1.0; + Double actual = deposit.getAmount(); + Assert.assertEquals(expected, actual); + } + + @Test + public void depositDescriptionTest() { + Deposit deposit = new Deposit(); + deposit.setDescription("uh"); + String expected = "uh"; + String actual = deposit.getDescription(); + Assert.assertEquals(expected, actual); + } + +} diff --git a/src/test/java/io/zipcoder/model/WithdrawalTest.java b/src/test/java/io/zipcoder/model/WithdrawalTest.java new file mode 100644 index 0000000..77ac494 --- /dev/null +++ b/src/test/java/io/zipcoder/model/WithdrawalTest.java @@ -0,0 +1,77 @@ +package io.zipcoder.model; + +import io.zipcoder.bank.enums.Medium; +import io.zipcoder.bank.enums.Status; +import io.zipcoder.bank.enums.Type; +import io.zipcoder.bank.model.Withdrawal; +import org.junit.Assert; +import org.junit.Test; + +import javax.persistence.Temporal; + +public class WithdrawalTest { + + @Test + public void withdrawalIdTest(){ + Withdrawal withdrawal = new Withdrawal(); + withdrawal.setId(8); + Integer expected = 8; + Assert.assertEquals(expected,withdrawal.getId()); + } + + @Test + public void withdrawalTypeTest() { + Withdrawal withdrawal = new Withdrawal(); + withdrawal.setType(Type.CHECKING); + Type expected = Type.CHECKING; + Assert.assertEquals(expected, withdrawal.getType()); + } + + @Test + public void WithdrawalTransactionDateTest(){ + Withdrawal withdrawal = new Withdrawal(); + withdrawal.setTransaction_date("2017-07-25"); + String expected = "2017-07-25"; + Assert.assertEquals(expected, withdrawal.getTransaction_date()); + } + + @Test + public void withdrawalStatusTest() { + Withdrawal withdrawal = new Withdrawal(); + withdrawal.setStatus(Status.PENDING); + Status expected = Status.PENDING; + Assert.assertEquals(expected, withdrawal.getStatus()); + } + + @Test + public void WithdrawalPayerIDTest(){ + Withdrawal withdrawal = new Withdrawal(); + withdrawal.setPayer_id(3); + Integer expected = 3; + Assert.assertEquals(expected, withdrawal.getPayer_id()); + } + + @Test + public void WithdrawalMediumTest(){ + Withdrawal withdrawal = new Withdrawal(); + withdrawal.setMedium(Medium.REWARDS); + Medium expected = Medium.REWARDS; + Assert.assertEquals(expected, withdrawal.getMedium()); + } + + @Test + public void WithdrawalAmountTest(){ + Withdrawal withdrawal = new Withdrawal(); + withdrawal.setAmount(100.0); + Double expected = 100.0; + Assert.assertEquals(expected, withdrawal.getAmount()); + } + + @Test + public void WithdrawalDescriptionTest(){ + Withdrawal withdrawal = new Withdrawal(); + withdrawal.setDescription("ATM Withdrawal"); + String expected = "ATM Withdrawal"; + Assert.assertEquals(expected, withdrawal.getDescription()); + } +} diff --git a/src/test/java/io/zipcoder/service/AccountServiceTest.java b/src/test/java/io/zipcoder/service/AccountServiceTest.java new file mode 100644 index 0000000..41ba6dc --- /dev/null +++ b/src/test/java/io/zipcoder/service/AccountServiceTest.java @@ -0,0 +1,74 @@ +package io.zipcoder.service; + +import io.zipcoder.bank.model.Account; +import io.zipcoder.bank.repository.AccountRepository; +import io.zipcoder.bank.service.AccountService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import util.BaseServiceTest; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class AccountServiceTest extends BaseServiceTest { + + @Mock + private static AccountRepository accountRepository; + + @InjectMocks + private static AccountService accountService = new AccountService(accountRepository); + + @Before + public void init() { + entity = new Account(); + initDependentVariables(); + } + + @Test + public void testCreateAccount() { + when(accountRepository.save(entity)) + .thenReturn(entity); + returnedEntity = accountService.createAccount(entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testFindAllAccounts() { + when(accountRepository.findAll()) + .thenReturn(entityCollection); + returnedEntityCollection = accountService.findAllAccounts(); + Assert.assertEquals(entityNotReturnedMessage, entityCollection, returnedEntityCollection); + } + + @Test + public void testFindAccountById() { + when(accountRepository.findOne(entityId)) + .thenReturn(entity); + returnedEntity = accountService.findAccountById(entityId); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testUpdateAccountById() { + when(accountRepository.save(entity)) + .thenReturn(entity); + returnedEntity = accountService.updateAccountById(entityId, entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testDeleteAccountById() { + accountService.deleteAccountById(entityId); + verify(accountRepository, times(1)) + .delete(eq(entityId)); + } + +} diff --git a/src/test/java/io/zipcoder/service/BillServiceTest.java b/src/test/java/io/zipcoder/service/BillServiceTest.java new file mode 100644 index 0000000..bb03101 --- /dev/null +++ b/src/test/java/io/zipcoder/service/BillServiceTest.java @@ -0,0 +1,78 @@ +package io.zipcoder.service; + +import io.zipcoder.bank.model.Bill; +import io.zipcoder.bank.repository.BillRepository; +import io.zipcoder.bank.service.AccountService; +import io.zipcoder.bank.service.BillService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import util.BaseServiceTest; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class BillServiceTest extends BaseServiceTest { + + + @Mock + private static BillRepository billRepository; + private static AccountService accountService; + + @InjectMocks + private static BillService billService = new BillService(billRepository, accountService); + + @Before + public void init() { + entity = new Bill(); + initDependentVariables(); + } + + @Test + public void testCreateBill() { + when(billRepository.save(entity)) + .thenReturn(entity); + returnedEntity = billService.createBill(entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testFindBillByBillId() { + when(billRepository.findOne(entityId)) + .thenReturn(entity); + returnedEntity = billService.findBillByBillId(entityId); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testUpdateBillByBillId() { + when(billRepository.save(entity)) + .thenReturn(entity); + returnedEntity = billService.updateBillByBillId(entityId, entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testDeleteBillByBillId() { + billService.deleteBillByBillId(entityId); + verify(billRepository, times(1)) + .delete(eq(entityId)); + } + +// @Test +// public void testFindBillsByAccount() { +// when(billRepository.findByAccount_id(entityId)) +// .thenReturn(entityCollection); +// returnedEntityCollection = billService.findAllBillsByAccount(entityId); +// Assert.assertEquals(entityNotReturnedMessage, entityCollection, returnedEntityCollection); +// } + + +} diff --git a/src/test/java/io/zipcoder/service/CustomerServiceTest.java b/src/test/java/io/zipcoder/service/CustomerServiceTest.java new file mode 100644 index 0000000..e8b3fdf --- /dev/null +++ b/src/test/java/io/zipcoder/service/CustomerServiceTest.java @@ -0,0 +1,74 @@ +package io.zipcoder.service; + +import io.zipcoder.bank.model.Customer; +import io.zipcoder.bank.repository.CustomerRepository; +import io.zipcoder.bank.service.CustomerService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import util.BaseServiceTest; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CustomerServiceTest extends BaseServiceTest { + + @Mock + private static CustomerRepository customerRepository; + + @InjectMocks + private static CustomerService customerService = new CustomerService(customerRepository); + + @Before + public void init() { + entity = new Customer(); + initDependentVariables(); + } + + @Test + public void testCreateCustomer() { + when(customerRepository.save(entity)) + .thenReturn(entity); + returnedEntity = customerService.createCustomer(entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testFindAllCustomers() { + when(customerRepository.findAll()) + .thenReturn(entityCollection); + returnedEntityCollection = customerService.findAllCustomers(); + Assert.assertEquals(entityNotReturnedMessage, entityCollection, returnedEntityCollection); + } + + @Test + public void testFindCustomerById() { + when(customerRepository.findOne(entityId)) + .thenReturn(entity); + returnedEntity = customerService.findCustomerById(entityId); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testUpdateCustomerById() { + when(customerRepository.save(entity)) + .thenReturn(entity); + returnedEntity = customerService.updateCustomerById(entityId, entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testDeleteCustomerById() { + customerService.deleteCustomerById(entityId); + verify(customerRepository, times(1)) + .delete(eq(entityId)); + } + +} diff --git a/src/test/java/io/zipcoder/service/DepositServiceTest.java b/src/test/java/io/zipcoder/service/DepositServiceTest.java new file mode 100644 index 0000000..19671cc --- /dev/null +++ b/src/test/java/io/zipcoder/service/DepositServiceTest.java @@ -0,0 +1,66 @@ +package io.zipcoder.service; + +import io.zipcoder.bank.model.Deposit; +import io.zipcoder.bank.repository.DepositRepository; +import io.zipcoder.bank.service.DepositService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import util.BaseServiceTest; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DepositServiceTest extends BaseServiceTest { + + @Mock + private static DepositRepository depositRepository; + + @InjectMocks + private static DepositService depositService; + + @Before + public void init() { + entity = new Deposit(); + initDependentVariables(); + } + + @Test + public void testCreateDeposit() { + when(depositRepository.save(entity)) + .thenReturn(entity); + returnedEntity = depositService.createDeposit(entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testFindDepositById() { + when(depositRepository.findOne(entityId)) + .thenReturn(entity); + returnedEntity = depositService.findDepositByDepositId(entityId); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testUpdateDepositById() { + when(depositRepository.save(entity)) + .thenReturn(entity); + returnedEntity = depositService.updateDepositByDepositId(entityId, entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testDeleteDepositById() { + depositService.deleteDepositByDepositId(entityId); + verify(depositRepository, times(1)) + .delete(eq(entityId)); + } + +} diff --git a/src/test/java/io/zipcoder/service/WithdrawalServiceTest.java b/src/test/java/io/zipcoder/service/WithdrawalServiceTest.java new file mode 100644 index 0000000..530e26a --- /dev/null +++ b/src/test/java/io/zipcoder/service/WithdrawalServiceTest.java @@ -0,0 +1,76 @@ +package io.zipcoder.service; + +import io.zipcoder.bank.model.Withdrawal; +import io.zipcoder.bank.repository.WithdrawalRepo; +import io.zipcoder.bank.service.WithdrawalService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import util.BaseServiceTest; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class WithdrawalServiceTest extends BaseServiceTest { + + @Mock + private static WithdrawalRepo withdrawalRepo; + + @InjectMocks + private static WithdrawalService withdrawalService = new WithdrawalService(withdrawalRepo); + + @Before + public void init() { + entity = new Withdrawal(); + initDependentVariables(); + } + + @Test + public void testCreateWithdrawal() { + when(withdrawalRepo.save(entity)) + .thenReturn(entity); + returnedEntity = withdrawalService.createWithdrawal(entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + +// @Test +// public void testFindAllAccounts() { +// when(accountRepository.findAll()) +// .thenReturn(entityCollection); +// returnedEntityCollection = accountService.findAllAccounts(); +// Assert.assertEquals(entityNotReturnedMessage, entityCollection, returnedEntityCollection); +// } + + @Test + public void testFindWithdrawalById() { + when(withdrawalRepo.findOne(entityId)) + .thenReturn(entity); + returnedEntity = withdrawalService.findWithdrawalByWithdrawalId(entityId); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testUpdateWithdrawalById() { + when(withdrawalRepo.save(entity)) + .thenReturn(entity); + returnedEntity = withdrawalService.updateWithdrawalByWithdrawalId(entityId, entity); + Assert.assertEquals(entityNotReturnedMessage, entity, returnedEntity); + } + + @Test + public void testDeleteWithdrawalById() { + withdrawalService.deleteWithdrawalByWithdrawalId(entityId); + verify(withdrawalRepo, times(1)) + .delete(eq(entityId)); + } + + +} diff --git a/src/test/java/util/BaseControllerTest.java b/src/test/java/util/BaseControllerTest.java new file mode 100644 index 0000000..953e5f4 --- /dev/null +++ b/src/test/java/util/BaseControllerTest.java @@ -0,0 +1,115 @@ +package util; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.Collections; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseControllerTest { + + public MockMvc mvc; + public String baseEndpointUrl; + public String idEndpointUrl; + public String entityNotReturnedMessage = "The expected entity is not being returned"; + public String entityNotDeletedMessage = "The entity is not being deleted"; + public String predictedEntity; + public String returnedEntity; + public int parentEntityId; + public int childEntityId; + public E entity; + public Collection entityCollection = Collections.singleton(entity); + public String entityAsJsonString; + public String entityCollectionAsJsonString; + + public void initDependentVariables() { + parentEntityId = 1; + childEntityId = 3; + idEndpointUrl = baseEndpointUrl + "/{id}"; + entityAsJsonString = JsonTestUtil.writeEntityAsJsonString(entity); + entityCollection = Collections.singleton(entity); + entityCollectionAsJsonString = JsonTestUtil.writeEntityAsJsonString(entityCollection); + } + + public ResultActions mvcPerformPostWithNoPathVariables + (String endpointUrl, String entityAsJsonString) throws Exception { + return mvc.perform(post(endpointUrl) + .contentType(MediaType.APPLICATION_JSON) + .content(entityAsJsonString)) + .andExpect(status().isCreated()); + } + + public ResultActions mvcPerformPostWithOnePathVariable + (String endpointUrl, Integer parentId, String entityAsJsonString) throws Exception { + return mvc.perform(post(endpointUrl, parentId) + .contentType(MediaType.APPLICATION_JSON) + .content(entityAsJsonString)) + .andExpect(status().isCreated()); + } + + public String mvcPerformGetWithNoPathVariables(String endpointUrl) throws Exception { + return convertMvcResultToString(mvc.perform(get(endpointUrl) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk())); + } + + public String mvcPerformGetWithOnePathVariable(String endpointUrl, Integer parentId) throws Exception { + return convertMvcResultToString(mvc.perform(get(endpointUrl, parentId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk())); + } + + public String mvcPerformGetWithOnePathVariable(String endpointUrl, String parentId) throws Exception { + return convertMvcResultToString(mvc.perform(get(endpointUrl, parentId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk())); + } + + public String mvcPerformGetWithTwoPathVariables(String endpointUrl, Integer parentId, Integer childId) + throws Exception { + return convertMvcResultToString(mvc.perform(get(endpointUrl, parentId, childId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk())); + } + + public ResultActions mvcPerformUpdateWithOnePathVariable + (String endpointUrl, Integer parentId, String entityAsJsonString) throws Exception { + return mvc.perform(put(endpointUrl, parentId) + .contentType(MediaType.APPLICATION_JSON) + .content(entityAsJsonString)) + .andExpect(status().isOk()); + } + + public ResultActions mvcPerformUpdateWithTwoPathVariables + (String endpointUrl, Integer parentId, Integer childId, String entityAsJsonString) throws Exception { + return mvc.perform(put(endpointUrl, parentId, childId) + .contentType(MediaType.APPLICATION_JSON) + .content(entityAsJsonString)) + .andExpect(status().isOk()); + } + + public String mvcPerformDeleteWithOnePathVariable(String endpointUrl, Integer parentId) + throws Exception { + return convertMvcResultToString(mvc.perform(delete(endpointUrl, parentId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk())); + } + + public String mvcPerformDeleteWithTwoPathVariables(String endpointUrl, Integer parentId, Integer childId) + throws Exception { + return convertMvcResultToString(mvc.perform(delete(endpointUrl, parentId, childId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk())); + } + + private String convertMvcResultToString(ResultActions resultActions) throws UnsupportedEncodingException { + return resultActions.andReturn().getResponse().getContentAsString(); + } + +} diff --git a/src/test/java/util/BaseServiceTest.java b/src/test/java/util/BaseServiceTest.java new file mode 100644 index 0000000..da031f0 --- /dev/null +++ b/src/test/java/util/BaseServiceTest.java @@ -0,0 +1,34 @@ +package util; + +import org.springframework.data.domain.Sort; + +import java.util.Collection; +import java.util.Collections; + +public abstract class BaseServiceTest { + + public String entityNotReturnedMessage = "The expected entity is not being returned"; + public String entityNotDeletedMessage = "The entity is not being deleted"; + public E entity; + public Integer entityId; + public Integer parentEntityId; + public Collection entityCollection; + public E returnedEntity; + public Collection returnedEntityCollection; + public Sort sort; + + public void initDependentVariables() { + entityCollection = Collections.singleton(entity); + entityId = 10; + parentEntityId = 20; + } + + public void initSortAsc(String sortCode) { + sort = new Sort(Sort.Direction.ASC, sortCode); + } + + public void initSortDesc(String sortCode) { + sort = new Sort(Sort.Direction.DESC, sortCode); + } + +} diff --git a/src/test/java/util/JsonTestUtil.java b/src/test/java/util/JsonTestUtil.java new file mode 100644 index 0000000..5320eb2 --- /dev/null +++ b/src/test/java/util/JsonTestUtil.java @@ -0,0 +1,15 @@ +package util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonTestUtil { + public static String writeEntityAsJsonString(E entity) { + try { + return new ObjectMapper().writeValueAsString(entity); + } catch (JsonProcessingException e) { + System.out.println(e.getMessage()); + return "JsonProcessingException"; + } + } +}