Browse Source

Reimport project

hotfix/20220724-no-user-id
LO Kam Tao Leo 3 years ago
commit
3d80c53180
  1. 35
      .gitignore
  2. 316
      mvnw
  3. 188
      mvnw.cmd
  4. 61
      pom.xml
  5. 95
      src/main/java/org/leolo/nrapi/AuthDataCache.java
  6. 139
      src/main/java/org/leolo/nrapi/AuthFilter.java
  7. 5
      src/main/java/org/leolo/nrapi/Constants.java
  8. 31
      src/main/java/org/leolo/nrapi/NrapiApplication.java
  9. 19
      src/main/java/org/leolo/nrapi/admin/AdminAPI.java
  10. 55
      src/main/java/org/leolo/nrapi/db/SearchParameter.java
  11. 31
      src/main/java/org/leolo/nrapi/manager/CacheManager.java
  12. 7
      src/main/java/org/leolo/nrapi/manager/CacheProvider.java
  13. 59
      src/main/java/org/leolo/nrapi/manager/DatabaseManager.java
  14. 33
      src/main/java/org/leolo/nrapi/manager/PropertyManager.java
  15. 63
      src/main/java/org/leolo/nrapi/util/HttpReqRespUtils.java
  16. 201
      src/main/java/org/leolo/nrapi/util/MiscUtilAPI.java
  17. 15
      src/main/java/org/leolo/nrapi/util/ScheduleJobs.java
  18. 11
      src/main/java/org/leolo/nrapi/util/StringUtil.java
  19. 162
      src/main/java/org/leolo/nrapi/v0/api/BackupFileAPI.java
  20. 127
      src/main/java/org/leolo/nrapi/v0/api/LocationSearchAPI.java
  21. 7
      src/main/java/org/leolo/nrapi/v0/api/LocationType.java
  22. 358
      src/main/java/org/leolo/nrapi/v0/api/ScheduleAPI.java
  23. 493
      src/main/java/org/leolo/nrapi/v0/api/ScheduleSearchAPI.java
  24. 289
      src/main/java/org/leolo/nrapi/v0/api/ScheduleSearchQueryBuilder.java
  25. 29
      src/main/java/org/leolo/nrapi/v0/api/TableGroup.java
  26. 57
      src/main/java/org/leolo/nrapi/v0/cache/TrainCategoryCache.java
  27. 57
      src/main/java/org/leolo/nrapi/v0/cache/TrainOperatorCache.java
  28. 42
      src/main/java/org/leolo/nrapi/v0/model/BackupEntry.java
  29. 52
      src/main/java/org/leolo/nrapi/v0/model/DisplayableError.java
  30. 84
      src/main/java/org/leolo/nrapi/v0/model/LocationSearchResult.java
  31. 9
      src/main/java/org/leolo/nrapi/v0/model/ReturnSet.java
  32. 126
      src/main/java/org/leolo/nrapi/v0/model/ScheduleSummary.java
  33. 23
      src/main/java/org/leolo/nrapi/v0/model/TrainAssociationInfo.java
  34. 184
      src/main/java/org/leolo/nrapi/v0/model/TrainSchedule.java
  35. 151
      src/main/java/org/leolo/nrapi/v0/model/TrainScheduleDetails.java
  36. 34
      src/main/java/org/leolo/nrapi/web/LoginAPI.java
  37. 159
      src/main/java/org/leolo/nrapi/web/TokenStore.java
  38. 20
      src/main/java/org/leolo/nrapi/web/TokenUtil.java
  39. 41
      src/main/resources/static/docs/backup_file.html
  40. 25
      src/main/resources/static/docs/base.html
  41. 11
      src/main/resources/static/docs/index.html
  42. 86
      src/main/resources/static/docs/location_search.html
  43. 79
      src/main/resources/static/docs/main.css
  44. 26
      src/main/resources/static/docs/nav.html
  45. 26
      src/main/resources/static/docs/overview.html
  46. 43
      src/main/resources/static/docs/retv-BackupEntry.html
  47. 56
      src/main/resources/static/docs/retv-LocationSearchResult.html
  48. 83
      src/main/resources/static/docs/retv-ScheduleSummary.html
  49. 35
      src/main/resources/static/docs/retv-TrainAssociation.html
  50. 193
      src/main/resources/static/docs/retv-TrainSchedule.html
  51. 98
      src/main/resources/static/docs/retv-TrainScheduleDetail.html
  52. 23
      src/main/resources/static/docs/retv-base.html
  53. 36
      src/main/resources/static/docs/schedule.html
  54. 32
      src/main/resources/static/docs/schedule_all.html
  55. 39
      src/main/resources/static/docs/schedule_base.html
  56. 75
      src/main/resources/static/docs/schedule_search.html
  57. 4
      src/main/resources/static/docs/script.js
  58. 12
      src/main/resources/static/web/index.html
  59. 23
      src/test/java/org/leolo/nrapi/NrapiApplicationTests.java
  60. 104
      src/test/java/org/leolo/nrapi/TokenGenerateTest.java

35
.gitignore vendored

@ -0,0 +1,35 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
src/main/resources/application.properties
.mvn/

316
mvnw vendored

@ -0,0 +1,316 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
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
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
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
else
JAVACMD="`\\unset -f command; \\command -v java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
fi
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if $cygwin; then
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
fi
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl -o "$wrapperJarPath" "$jarUrl" -f
else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaClass=`cygpath --path --windows "$javaClass"`
fi
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

188
mvnw.cmd vendored

@ -0,0 +1,188 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

61
pom.xml

@ -0,0 +1,61 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.leolo</groupId>
<artifactId>NRAPI</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>NRAPI</name>
<description>NRAPI</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.239</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

95
src/main/java/org/leolo/nrapi/AuthDataCache.java

@ -0,0 +1,95 @@
package org.leolo.nrapi;
import org.jetbrains.annotations.NotNull;
import org.leolo.nrapi.manager.CacheManager;
import org.leolo.nrapi.manager.CacheProvider;
import java.util.HashMap;
public class AuthDataCache implements CacheProvider {
private static AuthDataCache instance;
public static final long AUTH_CACHE_TIME = 300_000;
private static final Object ST_IP = new Object();
private static final Object ST_DID = new Object();
private HashMap<String, IPAuthCache> ips = new HashMap<>();
private HashMap<String, DIDAuthCache> dids = new HashMap<>();
public static synchronized AuthDataCache getInstance(){
if(instance==null){
instance = new AuthDataCache();
}
return instance;
}
private AuthDataCache(){
CacheManager.getInstance().addProvider(this);
}
@Override
public void clearCache() {
synchronized (ST_IP) {
ips.clear();
}
synchronized (ST_DID){
dids.clear();
}
}
public Boolean getIpAuth(@NotNull String ip){
synchronized (ST_IP) {
IPAuthCache ipAuthCache = this.ips.get(ip);
if (ipAuthCache != null && ipAuthCache.expiry > System.currentTimeMillis()) {
return ipAuthCache.isAuth;
}
this.ips.put(ip, null);
return null;
}
}
public void addIpAuth(@NotNull String ip, boolean result, String userId){
IPAuthCache ipac = new IPAuthCache();
ipac.expiry = System.currentTimeMillis() + AUTH_CACHE_TIME;
ipac.ipAddress = ip;
ipac.user = userId;
ipac.isAuth = result;
synchronized (ST_IP){
ips.put(ip, ipac);
}
}
public Boolean getDidAuth(@NotNull String did){
synchronized (ST_IP) {
DIDAuthCache didAuthCache = this.dids.get(did);
if (didAuthCache != null && didAuthCache.expiry > System.currentTimeMillis()) {
return didAuthCache.isAuth;
}
this.ips.put(did, null);
return null;
}
}
public void addDidAuth(@NotNull String deviceId, boolean result, String userId){
DIDAuthCache ipac = new DIDAuthCache();
ipac.expiry = System.currentTimeMillis() + AUTH_CACHE_TIME;
ipac.deviceId = deviceId;
ipac.user = userId;
ipac.isAuth = result;
synchronized (ST_IP){
dids.put(deviceId, ipac);
}
}
private class IPAuthCache{
long expiry;
String ipAddress;
String user;
boolean isAuth;
}private class DIDAuthCache{
long expiry;
String deviceId;
String user;
boolean isAuth;
}
}

139
src/main/java/org/leolo/nrapi/AuthFilter.java

@ -0,0 +1,139 @@
package org.leolo.nrapi;
import org.leolo.nrapi.manager.DatabaseManager;
import org.leolo.nrapi.util.HttpReqRespUtils;
import org.leolo.nrapi.web.TokenStore;
import org.leolo.nrapi.web.TokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@Component
@Order(1)
public class AuthFilter implements Filter {
private final Logger log = LoggerFactory.getLogger(AuthFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(servletRequest instanceof HttpServletRequest){
HttpServletRequest req = (HttpServletRequest) servletRequest;
log.info("Context path {}; path info = {}; uri={}", req.getContextPath(), req.getPathInfo(), req.getRequestURI());
if(req.getRequestURI().startsWith("/util/")){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(req.getRequestURI().startsWith("/docs")){
log.info("Accessing documents");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(req.getRequestURI().startsWith("/web")){
log.info("Accessing documents");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(req.getRequestURI().startsWith("/web-api")){
log.info("Accessing documents");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(req.getRequestURI().equals("/")){
log.info("Accessing documents");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(req.getRequestURI().startsWith("/favicon.ico")){
log.info("Accessing /favicon.ico");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String ip = HttpReqRespUtils.getClientIpAddressIfServletRequestExist(req);
long userId = -1;
log.info("User from {}", ip);
//TODO: need to refine the process
Boolean authResult = null;
if(authResult==null) {
try (
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT user_id FROM user_host WHERE host_ip = ?")
) {
pstmt.setString(1, ip);
try(ResultSet rs = pstmt.executeQuery()){
if(rs.next()){
log.info("Auth by IP");
userId = rs.getLong(1);
authResult = true;
}else{
// AuthDataCache.getInstance().addIpAuth(ip, false, null);
}
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
}
if(authResult==null && req.getParameter("device_key") != null){
try (
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT user_id FROM device WHERE device_id = ?")
) {
pstmt.setString(1, req.getParameter("device_key"));
try(ResultSet rs = pstmt.executeQuery()){
if(rs.next()){
log.info("auth by device_key");
userId = rs.getLong(1);
authResult = true;
}else{
// AuthDataCache.getInstance().addDidAuth(req.getParameter("device_key") , false, null);
}
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
}
if(authResult==null){
authResult = false;
}
//Check is there a valid token. Token are expected to be used via web interface only and
//will not be documented.
if(!authResult){
String token = req.getParameter("token");
authResult = TokenStore.getInstance().isTokenValid(token);
userId = TokenStore.getInstance().getUserIdForToken(token);
}
if(authResult) {
filterChain.doFilter(servletRequest, servletResponse);
servletRequest.setAttribute(Constants.REQ_ATTR_USER_ID, userId);
}else{
log.warn("{} is not authorized to use the system", ip);
((HttpServletResponse)servletResponse).sendError(403);
}
}else{
log.warn("Unknown request type {}", servletRequest.getClass().getName());
((HttpServletResponse)servletResponse).sendError(500);
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}

5
src/main/java/org/leolo/nrapi/Constants.java

@ -0,0 +1,5 @@
package org.leolo.nrapi;
public class Constants {
public static final String REQ_ATTR_USER_ID = "auth-result-user-id";
}

31
src/main/java/org/leolo/nrapi/NrapiApplication.java

@ -0,0 +1,31 @@
package org.leolo.nrapi;
import org.leolo.nrapi.manager.DatabaseManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@RestController
public class NrapiApplication {
public static void main(String[] args) {
SpringApplication.run(NrapiApplication.class, args);
}
@GetMapping("/")
public void main(HttpServletResponse response) throws IOException {
//This main entry point will redirect the user to the main page, to maintain the expected behaviour of
//a regular user.
response.sendRedirect("web/index.html");
}
}

19
src/main/java/org/leolo/nrapi/admin/AdminAPI.java

@ -0,0 +1,19 @@
package org.leolo.nrapi.admin;
import org.leolo.nrapi.manager.CacheManager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = {"/admin", "/v0/admin"})
public class AdminAPI {
@RequestMapping(value="/cache/clear")
public Object clearCache(){
CacheManager.getInstance().clear();
return new Object(){
public String getResult(){
return "success";
}
};
}
}

55
src/main/java/org/leolo/nrapi/db/SearchParameter.java

@ -0,0 +1,55 @@
package org.leolo.nrapi.db;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
public abstract class SearchParameter {
public abstract void setParam(PreparedStatement preparedStatement, int pos) throws SQLException;
public static SearchParameter stringParameter(String val){
return new SearchParameter() {
@Override
public void setParam(PreparedStatement preparedStatement, int pos) throws SQLException {
preparedStatement.setString(pos, val);
}
};
}
public static SearchParameter numberParameter(int val){
return new SearchParameter() {
@Override
public void setParam(PreparedStatement preparedStatement, int pos) throws SQLException {
preparedStatement.setInt(pos, val);
}
};
}
public static SearchParameter dateParameter(Date val){
return new SearchParameter() {
@Override
public void setParam(PreparedStatement preparedStatement, int pos) throws SQLException {
preparedStatement.setDate(pos, new java.sql.Date(val.getTime()));
}
};
}
public static SearchParameter timeParameter(Date val){
return new SearchParameter() {
@Override
public void setParam(PreparedStatement preparedStatement, int pos) throws SQLException {
preparedStatement.setTime(pos, new java.sql.Time(val.getTime()));
}
};
}
public static SearchParameter timestampParameter(Date val){
return new SearchParameter() {
@Override
public void setParam(PreparedStatement preparedStatement, int pos) throws SQLException {
preparedStatement.setTimestamp(pos, new java.sql.Timestamp(val.getTime()));
}
};
}
}

31
src/main/java/org/leolo/nrapi/manager/CacheManager.java

@ -0,0 +1,31 @@
package org.leolo.nrapi.manager;
import java.util.Vector;
public class CacheManager {
private static CacheManager instance;
private Vector<CacheProvider> providers = new Vector<>();
public static synchronized CacheManager getInstance(){
if(instance==null){
instance = new CacheManager();
}
return instance;
}
private CacheManager(){
}
public void addProvider(CacheProvider provider){
providers.add(provider);
}
public void clear(){
for(CacheProvider provider:providers){
provider.clearCache();
}
}
}

7
src/main/java/org/leolo/nrapi/manager/CacheProvider.java

@ -0,0 +1,7 @@
package org.leolo.nrapi.manager;
public interface CacheProvider {
public void clearCache();
}

59
src/main/java/org/leolo/nrapi/manager/DatabaseManager.java

@ -0,0 +1,59 @@
package org.leolo.nrapi.manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class DatabaseManager {
private static DatabaseManager instance;
private final Logger log = LoggerFactory.getLogger(DatabaseManager.class);
private final Properties prop;
private final DataSource ds;
public synchronized static DatabaseManager getInstance(){
if(instance==null){
instance = new DatabaseManager();
}
return instance;
}
private DatabaseManager(){
prop = new Properties();
try {
prop.load(getClass().getClassLoader().getResourceAsStream("application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
ds = getDataSource();
}
@Bean
@ConfigurationProperties("app.datasource")
private DataSource getDataSource(){
log.info("jdbcUrl={}", prop.getProperty("app.datasource.jdbcUrl"));
return DataSourceBuilder
.create()
.url(prop.getProperty("app.datasource.jdbcUrl"))
.driverClassName(prop.getProperty("app.datasource.dataSourceClassName"))
.username(prop.getProperty("app.datasource.username"))
.password(prop.getProperty("app.datasource.password"))
.build();
}
public Connection getConnection() throws SQLException{
Connection conn = ds.getConnection();
conn.setAutoCommit(false);
return conn;
}
}

33
src/main/java/org/leolo/nrapi/manager/PropertyManager.java

@ -0,0 +1,33 @@
package org.leolo.nrapi.manager;
import com.fasterxml.jackson.databind.annotation.JsonAppend;
import java.io.IOException;
import java.util.Properties;
public class PropertyManager {
private static PropertyManager instance;
private Properties properties;
public static synchronized PropertyManager getInstance(){
if(instance==null){
instance = new PropertyManager();
}
return instance;
}
private PropertyManager(){
properties = new Properties();
try {
properties.load(getClass().getClassLoader().getResourceAsStream("application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public Properties getProperties(){
return properties;
}
}

63
src/main/java/org/leolo/nrapi/util/HttpReqRespUtils.java

@ -0,0 +1,63 @@
package org.leolo.nrapi.util;
import org.leolo.nrapi.Constants;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class HttpReqRespUtils {
public static final String[] IP_HEADER_CANDIDATES = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR"
};
public static String getClientIpAddressIfServletRequestExist() {
if (RequestContextHolder.getRequestAttributes() == null) {
return "0.0.0.0";
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return getClientIpAddressIfServletRequestExist(request);
}
public static String getContextPath() {
if (RequestContextHolder.getRequestAttributes() == null) {
return "/";
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getContextPath();
}
public static long getUserId(){
if(RequestContextHolder.getRequestAttributes() == null) {
return -1;
}
return (long)((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getAttribute(Constants.REQ_ATTR_USER_ID);
}
public static String getClientIpAddressIfServletRequestExist(HttpServletRequest request) {
for (String header: IP_HEADER_CANDIDATES) {
String ipList = request.getHeader(header);
if (ipList != null && ipList.length() != 0 && !"unknown".equalsIgnoreCase(ipList)) {
String ip = ipList.split(",")[0];
return ip;
}
}
return request.getRemoteAddr();
}
}

201
src/main/java/org/leolo/nrapi/util/MiscUtilAPI.java

@ -0,0 +1,201 @@
package org.leolo.nrapi.util;
import org.jetbrains.annotations.NotNull;
import org.leolo.nrapi.manager.DatabaseManager;
import org.leolo.nrapi.v0.model.ReturnSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.GZIPOutputStream;
@RestController
@RequestMapping(value = {"/util","/v0/util"})
public class MiscUtilAPI {
private final Logger log = LoggerFactory.getLogger(MiscUtilAPI.class);
private static Random random = new Random();
private static final String DEVICE_ID_CHARS = "0123456789ABCDEFGHJKLMNPRSTUVWXY";
@RequestMapping(value = "testAuth", produces = "text/plain")
public String testAuth(
@RequestParam(name="device_key", required = false, defaultValue = "") String deviceKey,
@NotNull HttpServletResponse response,
@NotNull HttpServletRequest request
){
StringBuilder sb = new StringBuilder();
boolean authed = false;
HashSet<String> authMethod = new HashSet<>();
String selectedAuthMethod = null;
sb.append("{");
sb.append("\"request-date-time\":\"").append(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").format(new Date())).append("\"");
sb.append(",\"server-ip\":\"").append(request.getServerName()).append("\"");
sb.append(",\"request-uri\":\"").append(request.getRequestURI()).append("\"");
sb.append(",\"has-device-id\":").append(!"".equals(deviceKey));
if(!"".equals(deviceKey)){
//Has device key
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT user_id FROM device WHERE device_id = ?")
){
pstmt.setString(1, deviceKey);
sb.append(",\"device-key\":\"").append(deviceKey).append("\"");
try(ResultSet rs = pstmt.executeQuery()){
boolean authByDevKey = rs.next();
sb.append(",\"device-key-result\":").append(authByDevKey);
if(authByDevKey){
sb.append(",\"device-key-user-id\":").append(rs.getString(1));
selectedAuthMethod = "device-key";
authMethod.add("device-key");
authed = true;
}
}
}catch (SQLException e){
log.error(e.getMessage(), e);
}
}
//Stage 2: list all IP related headers
String selectedIP = HttpReqRespUtils.getClientIpAddressIfServletRequestExist(request);
sb.append(",\"ip-addr\":{");
sb.append("\"request\":\"").append(request.getRemoteAddr()).append("\"");
sb.append(",\"selected\":\"").append(selectedIP).append("\"");
for(String header:HttpReqRespUtils.IP_HEADER_CANDIDATES){
String ipList = request.getHeader(header);
if (ipList != null && ipList.length() != 0 && !"unknown".equalsIgnoreCase(ipList)) {
String ips[] = ipList.split(",");
sb.append(",\"").append(header).append("\":[");
for(int i=0;i<ips.length;i++){
if(i!=0){
sb.append(",");
}
sb.append("\"").append(ips[i]).append("\"");
}
sb.append("]");
}
}
sb.append("}");
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT user_id FROM user_host WHERE host_ip = ?");
){
pstmt.setString(1, selectedIP);
try(ResultSet rs = pstmt.executeQuery()){
boolean authByIP = rs.next();
sb.append(",\"ip-result\":").append(authByIP);
if(authByIP){
if(!authed){
selectedAuthMethod = "ip";
sb.append(",\"ip-user-id\":").append(rs.getString(1));
authed = true;
}
authMethod.add("ip");
}
}
}catch (SQLException e){
log.error(e.getMessage(), e);
}
sb.append(",\"final-auth-result\":").append(authed);
if(authed) {
sb.append(",\"selected-auth-method\":\"").append(selectedAuthMethod).append("\"");
}
sb.append(",\"auth-methods\":[");
Iterator<String> iAuthMethods = authMethod.iterator();
while(iAuthMethods.hasNext()){
sb.append("\"").append(iAuthMethods.next()).append("\"");
if(iAuthMethods.hasNext()){
sb.append(",");
}
}
sb.append("]");
sb.append(",\"request-method\":\"").append(request.getMethod()).append("\"");
sb.append(",\"headers\":{");
Enumeration<String> headers = request.getHeaderNames();
while(headers.hasMoreElements()){
String header = headers.nextElement();
ArrayList<String> values = new ArrayList<>();
Enumeration<String> vals = request.getHeaders(header);
while(vals.hasMoreElements()){
values.add(vals.nextElement());
}
sb.append("\"").append(header).append("\":");
if(values.size()==0){
sb.append("null");
}else if(values.size()==1){
sb.append("\"").append(values.get(0)).append("\"");
}else{
sb.append("[");
for(int i=0;i<values.size();i++){
if(i!=0){
sb.append(",");
}
sb.append("\"").append(values.get(i)).append("\"");
}
sb.append("]");
}
if(headers.hasMoreElements()){
sb.append(",");
}
}
sb.append("}");
sb.append("}");
String result = "";
try(
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintWriter out = new PrintWriter(new GZIPOutputStream(baos))
){
out.println(sb);
out.flush();
out.close();
result = Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return result;
}
@RequestMapping(value="generate/did")
public ArrayList<String> generateDeviceId(@RequestParam(name = "count", required = false, defaultValue = "1")int count){
ArrayList<String> dids = new ArrayList<>();
log.info("Generating {} device IDs", count);
//Limit to generate 1-100 DIDs
if(count > 100){
count = 100;
}else if(count <1){
count = 1;
}
for (int i=0;i<count;i++){
StringBuilder sb = new StringBuilder();
sb.append("S0-");
int num = 0x118483b7;
for(int j=0;j<24;j++){
int nextKey = random.nextInt(DEVICE_ID_CHARS.length());
num = num * 31 + nextKey;
sb.append(DEVICE_ID_CHARS.charAt(nextKey));
}
sb.append("-");
int upper = (num / DEVICE_ID_CHARS.length())%DEVICE_ID_CHARS.length();
int lower = num % DEVICE_ID_CHARS.length();
if(upper<0) upper+=DEVICE_ID_CHARS.length();
if(lower<0) lower+=DEVICE_ID_CHARS.length();
sb.append(DEVICE_ID_CHARS.charAt(upper)).append(DEVICE_ID_CHARS.charAt(lower));
dids.add(sb.toString());
}
return dids;
}
}

15
src/main/java/org/leolo/nrapi/util/ScheduleJobs.java

@ -0,0 +1,15 @@
package org.leolo.nrapi.util;
import org.leolo.nrapi.web.TokenStore;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
@Configuration
@EnableScheduling
public class ScheduleJobs {
@Scheduled(fixedDelay = 60000)
public void removeExpiredTokens(){
TokenStore.getInstance().clearExpiredEntry();
}
}

11
src/main/java/org/leolo/nrapi/util/StringUtil.java

@ -0,0 +1,11 @@
package org.leolo.nrapi.util;
public class StringUtil {
public static String lpad(String str, int length, char padChar){
StringBuilder sb = new StringBuilder();
for(int i=0;length<(sb.length()+ str.toString().length());i++){
sb.append(padChar);
}
return sb.append(str).toString();
}
}

162
src/main/java/org/leolo/nrapi/v0/api/BackupFileAPI.java

@ -0,0 +1,162 @@
package org.leolo.nrapi.v0.api;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import org.leolo.nrapi.manager.DatabaseManager;
import org.leolo.nrapi.util.HttpReqRespUtils;
import org.leolo.nrapi.v0.model.BackupEntry;
import org.leolo.nrapi.v0.model.DisplayableError;
import org.leolo.nrapi.v0.model.ReturnSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
@RestController
@RequestMapping(value = {"v0/backup","backup"})
public class BackupFileAPI {
private Logger log = LoggerFactory.getLogger(BackupFileAPI.class);
private final Properties prop;
public static final int MAX_SEARCH_DAYS = 90;
public BackupFileAPI(){
prop = new Properties();
try {
prop.load(getClass().getClassLoader().getResourceAsStream("application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@RequestMapping(value = "get/{id}")
public void getFile(@PathVariable(name = "id") String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
if(HttpReqRespUtils.getUserId()==-1){
response.sendError(403);
return;
}
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT 1 FROM backup_data WHERE file_name= ?")
){
pstmt.setString(1, id);
try(ResultSet rs = pstmt.executeQuery()){
if(!rs.next()){
response.sendError(404);
return;
}
}
}catch (SQLException e){
log.error(e.getMessage(), e);
}
AmazonS3 s3 = AmazonS3ClientBuilder
.standard()
.withRegion(prop.getProperty("aws.region"))
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(
prop.getProperty("aws.keyid"),
prop.getProperty("aws.secret"))))
.build();
GeneratePresignedUrlRequest gpur = new GeneratePresignedUrlRequest(
prop.getProperty("aws.bucket.name"),
id)
.withMethod(com.amazonaws.HttpMethod.GET)
.withExpiration(new java.util.Date(System.currentTimeMillis()+3_600_000));
response.sendRedirect(s3.generatePresignedUrl(gpur).toString());
}
@RequestMapping(value = "list")
public Object listFile(
@RequestParam(name = "startDate", required = false) String strStartDate,
@RequestParam(name = "endDate", required = false) String strEndDate,
HttpServletRequest request
){
Date startDate = null;
Date endDate = null;
try{
if(strStartDate!=null) {
startDate = new SimpleDateFormat("yyyy-MM-dd").parse(strStartDate);
}
if(strEndDate!=null) {
endDate = new SimpleDateFormat("yyyy-MM-dd").parse(strEndDate);
}
}catch(ParseException e){
return new DisplayableError(e);
}
if(startDate==null && endDate==null){
//Default ends today
endDate = new Date();
}
if(startDate==null){
Calendar c = GregorianCalendar.getInstance();
c.setTime(endDate);
c.add(Calendar.DAY_OF_MONTH, -1*MAX_SEARCH_DAYS);
startDate = c.getTime();
} else if (endDate == null) {
Calendar c = GregorianCalendar.getInstance();
c.setTime(startDate);
c.add(Calendar.DAY_OF_MONTH, MAX_SEARCH_DAYS);
endDate = c.getTime();
}else{
//Check is the range less than 90 days
Calendar c = GregorianCalendar.getInstance();
c.setTime(startDate);
c.add(Calendar.DAY_OF_MONTH, -1*MAX_SEARCH_DAYS);
if(endDate.after(c.getTime())){
return new DisplayableError(
"Search range too large",
String.format("Search range cannot be more than %i days", MAX_SEARCH_DAYS)
);
}
}
ArrayList<BackupEntry> array = new ArrayList<>();
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM backup_data WHERE created_date BETWEEN ? AND ?")
){
pstmt.setDate(1, new java.sql.Date(startDate.getTime()));
pstmt.setDate(2, new java.sql.Date(endDate.getTime()));
try(ResultSet rs = pstmt.executeQuery()){
while (rs.next()){
BackupEntry backupEntry = new BackupEntry();
backupEntry.setKey(rs.getString(3));
backupEntry.setCreatedDate(rs.getTimestamp(2));
backupEntry.setFileSize(rs.getInt(4));
StringBuilder url = new StringBuilder();
url.append(request.getScheme())
.append("://")
.append(request.getServerName());
if("http".equals(request.getScheme()) && request.getServerPort()!= 80){
url.append(":").append(request.getServerPort());
}else if("https".equals(request.getScheme()) && request.getServerPort()!= 443){
url.append(":").append(request.getServerPort());
}
url.append(request.getContextPath())
.append("/v0/backup/get/")
.append(rs.getString(3));
backupEntry.setUrl(url.toString());
array.add(backupEntry);
}
}
}catch(SQLException e) {
return new DisplayableError(e);
}
return array;
}
}

127
src/main/java/org/leolo/nrapi/v0/api/LocationSearchAPI.java

@ -0,0 +1,127 @@
package org.leolo.nrapi.v0.api;
import org.leolo.nrapi.manager.DatabaseManager;
import org.leolo.nrapi.v0.model.DisplayableError;
import org.leolo.nrapi.v0.model.LocationSearchResult;
import org.leolo.nrapi.v0.model.ReturnSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@RestController
@RequestMapping(value = {"/location","/v0/location"})
public class LocationSearchAPI {
private final Logger log = LoggerFactory.getLogger(LocationSearchAPI.class);
@RequestMapping(value = "search")
public Object searchLocation(
@RequestParam(name="term", required = false) String term,
@RequestParam(name="maxCount", required = false, defaultValue = "10") int maxCount,
javax.servlet.ServletRequest request
){
if(term==null||term.strip().length()==0){
return new DisplayableError("Missing Parameter","Missing required parameter 'term'");
}
if(maxCount<0) maxCount = 10;
log.info("Searching for {}", term);
HashSet<LocationSearchResult> results = new HashSet<>();
try(
Connection conn = DatabaseManager.getInstance().getConnection();
){
try(PreparedStatement ps1 = conn.prepareStatement(
"SELECT tiploc_code, nalco, stanox, crs, MATCH(tps_desc) AGAINST (?) " +
"FROM tiploc " +
"WHERE MATCH(tps_desc) AGAINST (? IN BOOLEAN MODE) OR crs = UPPER(?)");
PreparedStatement ps2 = conn.prepareStatement(
"SELECT tiploc_code, nalco, stanox, crs, tps_desc " +
"FROM tiploc " +
"WHERE (" +
"tiploc_code = ? OR nalco = ? OR stanox = ? OR (" +
"crs IS NOT NULL AND crs = ?" +
")" +
")");
){
ps1.setString(1, term);
ps1.setString(2, term);
ps1.setString(3, term);
try(ResultSet rs1 = ps1.executeQuery()){
while(rs1.next()){
ps2.setString(1,rs1.getString(1));
ps2.setString(2,rs1.getString(2));
ps2.setString(3,rs1.getString(3));
ps2.setString(4,rs1.getString(4));
try(ResultSet rs2 = ps2.executeQuery()){
while(rs2.next()){
LocationSearchResult locationSearchResult = new LocationSearchResult();
locationSearchResult.setTiplocCode(rs2.getString(1));
locationSearchResult.setNalco(rs2.getString(2));
locationSearchResult.setStanox(rs2.getString(3));
locationSearchResult.setCrsCode(rs2.getString(4));
locationSearchResult.setDisplayName(rs2.getString(5));
if(term.length()==3 && term.equalsIgnoreCase(locationSearchResult.getCrsCode())){
//Matching based on CRS code
log.debug("Matched CRS {}", term);
locationSearchResult.setMatchScore(100);
}else {
locationSearchResult.setMatchScore(rs1.getDouble(5));
}
locationSearchResult.setResultGroup(rs1.getString(1).hashCode());
results.add(locationSearchResult);
}
}
}
}
}
}catch(SQLException e){
log.error(e.getMessage(), e);
return new DisplayableError("Database Error", "Exception caught when accessing the database. Please consults the server log for details.");
}
ArrayList<LocationSearchResult> list = new ArrayList<>();
for(LocationSearchResult lsr:results){
list.add(lsr);
}
Collections.sort(list, (r1, r2)->{
int result = Double.compare(r2.getMatchScore(), r1.getMatchScore());
if(result!=0) return result;
int v1 = r1.getCrsCode()==null?1:0;
int v2 = r2.getCrsCode()==null?1:0;
result = Integer.compare(v1, v2);
if(result!=0) return result;
return r1.getTiplocCode().compareTo(r2.getTiplocCode());
});
ArrayList<LocationSearchResult> retList = new ArrayList<>();
for(int i=0;i<maxCount && i<list.size();i++){
retList.add(list.get(i));
}
return retList;
}
private class TiplocSearchEntry{
String tiploc;
double matchScore;
public TiplocSearchEntry(String tiploc, double matchScore) {
this.tiploc = tiploc;
this.matchScore = matchScore;
}
public TiplocSearchEntry() {
}
}
}

7
src/main/java/org/leolo/nrapi/v0/api/LocationType.java

@ -0,0 +1,7 @@
package org.leolo.nrapi.v0.api;
public enum LocationType {
TIPLOC,
CRS,
BOTH;
}

358
src/main/java/org/leolo/nrapi/v0/api/ScheduleAPI.java

@ -0,0 +1,358 @@
package org.leolo.nrapi.v0.api;
import org.jetbrains.annotations.NotNull;
import org.leolo.nrapi.manager.DatabaseManager;
import org.leolo.nrapi.v0.cache.TrainCategoryCache;
import org.leolo.nrapi.v0.cache.TrainOperatorCache;
import org.leolo.nrapi.v0.model.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@RequestMapping(value={"/v0/schedule","/schedule"})
@RestController
public class ScheduleAPI {
private final Logger LOG = LoggerFactory.getLogger(ScheduleAPI.class);
private String daysRunToHumanReadableFormat(String days){
String [] weekDayId = {"M","Tu","W","Th","F", "S", "Su"};
char[] daysRun = days.toCharArray();
if(daysRun.length!=7){
return "error";
}
int runDays = 0;
for(char ch:daysRun){
if(ch=='1')
runDays++;
else if(ch!='0'){
return "error";
}
}
StringBuilder sb = new StringBuilder();
if(runDays==7){
return "Every day";
}else if(runDays>4){
//Use X format
for(int i=0;i<7;i++){
if('0'==daysRun[i]){
sb.append(weekDayId[i]);
}
}
sb.append('X');
}else{
//Use O format
for(int i=0;i<7;i++){
if('1'==daysRun[i]){
sb.append(weekDayId[i]);
}
}
sb.append('O');
}
return sb.toString();
}
@RequestMapping(value={"{id}/wtt","{id}/{date}/wtt"}, method= RequestMethod.GET, produces = "application/json")
public Object getBaseSchedules(
@PathVariable(value="id") String id,
@PathVariable(value = "date", required = false) String date,
@RequestParam(value = "stopOnly", required = false, defaultValue = "false") boolean stopOnly
){
Date scheduleDate = null;
if(date!=null){
try {
scheduleDate = new SimpleDateFormat("yyyy-MM-dd").parse(date);
} catch (ParseException e) {
return new DisplayableError(e);
}
}else{
//Fill in default value
scheduleDate = new Date();
}
java.sql.Date sqlDate = new java.sql.Date(scheduleDate.getTime());
TrainSchedule ts = new TrainSchedule();
try(Connection conn = DatabaseManager.getInstance().getConnection()){
String suid = null;
String schType = null;
String table = null;
try(PreparedStatement pstmt = conn.prepareStatement(
"SELECT 'l' AS tbl, `suid`, sch_type FROM ltp_schedule " +
"WHERE train_uid=? AND ? BETWEEN start_date AND end_date AND sch_type IN ('STP','WTT')" +
"UNION ALL SELECT 's', suid, sch_type FROM stp_schedule " +
"WHERE train_uid=? AND ? BETWEEN start_date AND end_date AND sch_type IN ('STP','WTT')")){
pstmt.setString(1, id);
pstmt.setDate(2, sqlDate);
pstmt.setString(3, id);
pstmt.setDate(4, sqlDate);
try(ResultSet rs = pstmt.executeQuery()){
while(rs.next()){
LOG.info("{} is on table {}, is type {}", rs.getString(2), rs.getString(1), rs.getString(3));
String tSuid = rs.getString(2);
String tTable = rs.getString(1);
String tSchType = rs.getString(3);
suid = tSuid;
schType = tSchType;
table = tTable;
}
}
LOG.info("[WTT]SUID={}, table={}", suid, table);
if(suid==null){
return new DisplayableError("Train not found", "Provided train ID does not found, or does not runs on the specified day, if no date were specified, today's date will be used. Past train record only being kept for 7 days.");
}
}
return getScheduleBySUID(suid, table, stopOnly);
}catch(SQLException e){
LOG.error(e.getMessage(), e);
return new DisplayableError("Database Error", "Exception caught when accessing the database. Please consults the server log for details.");
}
}
@RequestMapping (value = {"{id}/all"}, produces = "application/json", method= RequestMethod.GET)
public Object getAllSchedule(
@PathVariable(value = "id") String id,
@RequestParam(value = "stopOnly", required = false, defaultValue = "false") boolean stopOnly
){
ArrayList<TrainSchedule> scheduleArrayList = new ArrayList<>();
try(Connection conn = DatabaseManager.getInstance().getConnection()){
try(PreparedStatement psSch = conn.prepareStatement(
"SELECT 'l' AS tbl, `suid`, sch_type FROM ltp_schedule " +
"WHERE train_uid=? " +
"UNION ALL SELECT 's', suid, sch_type FROM stp_schedule " +
"WHERE train_uid=? ")){
psSch.setString(1, id);
psSch.setString(2, id);
try(ResultSet rsSch = psSch.executeQuery()){
while(rsSch.next()){
ReturnSet rs = getScheduleBySUID(rsSch.getString(2), rsSch.getString(1), stopOnly);
if(rs instanceof TrainSchedule){
scheduleArrayList.add((TrainSchedule) rs);
}
}
}
}
}catch (SQLException e){
LOG.error(e.getMessage(), e);
return new DisplayableError("Database Error", "Exception caught when accessing the database. Please consults the server log for details.");
}
Collections.sort(scheduleArrayList, (v1, v2)->{
int t1 = "WTT".equals(v1.getScheduleType())?0:1;
int t2 = "WTT".equals(v2.getScheduleType())?0:1;
int rv = Integer.compare(t1, t2);
if(rv!=0) return rv;
rv = v1.getStartDate().compareTo(v2.getStartDate());
if(rv!=0) return rv;
return v1.getEndDate().compareTo(v2.getEndDate());
});
return scheduleArrayList;
}
@RequestMapping(value={"{id}","{id}/{date}"}, method= RequestMethod.GET, produces = "application/json")
public Object getSchedules(
@PathVariable(value="id") String id,
@PathVariable(value = "date", required = false) String date,
@RequestParam(value = "stopOnly", required = false, defaultValue = "false") boolean stopOnly
){
Date scheduleDate = null;
if(date!=null){
try {
scheduleDate = new SimpleDateFormat("yyyy-MM-dd").parse(date);
} catch (ParseException e) {
return new DisplayableError(e);
}
}else{
//Fill in default value
scheduleDate = new Date();
}
if(id.length()==5){
id = " "+id;
}
LOG.info("Searching for train '{}'", id);
java.sql.Date sqlDate = new java.sql.Date(scheduleDate.getTime());
TrainSchedule ts = new TrainSchedule();
try(Connection conn = DatabaseManager.getInstance().getConnection()){
String suid = null;
String schType = null;
String table = null;
try(PreparedStatement pstmt = conn.prepareStatement(
"SELECT 'l' AS tbl, `suid`, sch_type FROM ltp_schedule " +
"WHERE train_uid=? AND ? BETWEEN start_date AND end_date AND days LIKE get_wd_str(?)" +
"UNION ALL SELECT 's', suid, sch_type FROM stp_schedule " +
"WHERE train_uid=? AND ? BETWEEN start_date AND end_date AND days LIKE get_wd_str(?)")){
pstmt.setString(1, id);
pstmt.setDate(2, sqlDate);
pstmt.setDate(3, sqlDate);
pstmt.setString(4, id);
pstmt.setDate(5, sqlDate);
pstmt.setDate(6, sqlDate);
try(ResultSet rs = pstmt.executeQuery()){
while(rs.next()){
LOG.info("{} is on table {}, is type {}", rs.getString(2), rs.getString(1), rs.getString(3));
String tSuid = rs.getString(2);
String tTable = rs.getString(1);
String tSchType = rs.getString(3);
if(suid==null){
suid = tSuid;
schType = tSchType;
table = tTable;
continue;
}else if("STP".equals(schType)||"WTT".equals(schType)){
suid = tSuid;
schType = tSchType;
table = tTable;
}
}
}
LOG.info("SUID={}, table={}", suid, table);
if(suid==null){
return new DisplayableError("Train not found", "Provided train ID does not found, or does not runs on the specified day, if no date were specified, today's date will be used. Past train record only being kept for 7 days.");
}
}
ts = (TrainSchedule) getScheduleBySUID(suid, table, stopOnly);
if(!"CAN".equals(schType)){
//Cancelled trains has no association data
fillTrainAssociation(ts, sqlDate, schType);
}
}catch(SQLException e){
LOG.error(e.getMessage(), e);
return new DisplayableError("Database Error", "Exception caught when accessing the database. Please consults the server log for details.");
}
return ts;
}
private void fillTrainAssociation(@NotNull TrainSchedule ts, @NotNull java.sql.Date sqlDate, @NotNull String scheduleType) throws SQLException{
// LOG.
if("OVL".equals(scheduleType)){
scheduleType = "O";
}else{
scheduleType = "P";
}
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"SELECT main_uid, assoc_uid, assoc_type, location " +
"FROM ltp_assoication " +
"WHERE (main_uid=? OR assoc_uid=?) " +
"AND ? between start_date AND end_date " +
"AND assoc_days LIKE get_wd_str(?) " +
"AND stp_ind = ?"
)
){
pstmt.setString(1, ts.getTrainId());
pstmt.setString(2, ts.getTrainId());
pstmt.setDate(3, sqlDate);
pstmt.setDate(4, sqlDate);
pstmt.setString(5, scheduleType);
try(ResultSet rs = pstmt.executeQuery()){
while(rs.next()){
LOG.info("Assoc {}/{} type {}@{}", rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4));
TrainAssociationInfo trainAssociationInfo = new TrainAssociationInfo();
if(ts.getTrainId().equals(rs.getString(1))){
trainAssociationInfo.setAssociatedTrainId(rs.getString(2));
}else{
trainAssociationInfo.setAssociatedTrainId(rs.getString(1));
}
if("VV".equals(rs.getString(3))){
trainAssociationInfo.setAssociationType("Divide");
}else if("JJ".equals(rs.getString(3))){
trainAssociationInfo.setAssociationType("Join");
}else if("NP".equals(rs.getString(3))){
trainAssociationInfo.setAssociationType("Next train");
}else{
trainAssociationInfo.setAssociationType("Unknown");
}
for(TrainScheduleDetails scheduleDetails:ts.getScheduleEntries()){
if(scheduleDetails.getTiplocCode().equals(rs.getString(4))){
scheduleDetails.getAssociation().add(trainAssociationInfo);
break;
}
}
}
}
}
}
private @NotNull ReturnSet getScheduleBySUID(@NotNull String suid, @NotNull String tableType, boolean stopOnly) throws SQLException{
TrainSchedule ts = new TrainSchedule();
try(Connection conn = DatabaseManager.getInstance().getConnection()) {
//Note: Cols/Tbl name cannot be resolved because the table name is dynamically generated.
//noinspection
try (PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM " + tableType + "tp_schedule WHERE suid = ?")) {
pstmt.setString(1, suid);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
ts.setTrainId(rs.getString(2));
ts.setScheduleType(rs.getString(3));
ts.setStartDate(rs.getDate(4));
ts.setEndDate(rs.getDate(5));
ts.setDaysRun(daysRunToHumanReadableFormat(rs.getString(6)));
ts.setBankHolidayRun(rs.getString(7));
ts.setHeadcode(rs.getString(8));
if (rs.getString(9) != null) {
ts.setReservationSystemHeadcode(rs.getString(22) + rs.getString(9));
}
ts.setStatus(rs.getString(10));
ts.setCategory(TrainCategoryCache.getInstance().getTrainCategory(rs.getString(11)));
ts.setPowerType(rs.getString(13));
ts.setTimingLoad(rs.getString(14));
ts.setSpeed(rs.getString(15));
ts.setOperatingCharacters(rs.getString(16));
ts.setClassAvailable(rs.getString(17));
ts.setReservation(rs.getString(19));
ts.setCatering(rs.getString(20));
ts.setTrainOperatorCode(rs.getString(22));
ts.setTrainOperatorName(TrainOperatorCache.getInstance().getTocNameByAtocCode(rs.getString(22)));
}
}
}
if("WTT".equals(ts.getScheduleType())){
if(Character.isDigit(ts.getStatus().codePointAt(0))){
ts.setScheduleType("STP");
}
}
//Note: Cols/Tbl name cannot be resolved because the table name is dynamically generated.
//noinspection
try (PreparedStatement pstmt = conn.prepareStatement(
"SELECT suid, seq, tiploc_code, get_tiploc_name(tiploc_code), arrival, departure, pass, " +
"pub_arrival, pub_departure, platform, line, path, engineering_allowance, pathing_allowance," +
" performance_allowance, type, tiploc_to_crs(tiploc_code) FROM " + tableType + "tp_location WHERE suid = ? ORDER BY seq ASC")) {
pstmt.setString(1, suid);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
TrainScheduleDetails tsd = new TrainScheduleDetails();
tsd.setTiplocCode(rs.getString(3));
tsd.setTiplocName(rs.getString(4));
tsd.setWttArrival(rs.getString(5));
tsd.setWttDeparture(rs.getString(6));
tsd.setWttPass(rs.getString(7));
tsd.setGbttArrival(rs.getString(8));
tsd.setGbttDeparture(rs.getString(9));
tsd.setPlatform(rs.getString(10));
tsd.setLine(rs.getString(11));
tsd.setPath(rs.getString(12));
tsd.setEngineeringAllowance(rs.getString(13));
tsd.setPathingAllowance(rs.getString(14));
tsd.setPerformanceAllowance(rs.getString(15));
tsd.setCrsCode(rs.getString(17));
if(stopOnly && tsd.getWttArrival()==null && tsd.getWttDeparture()==null){
continue;
}
ts.getScheduleEntries().add(tsd);
}
}
}
}
return ts;
}
}

493
src/main/java/org/leolo/nrapi/v0/api/ScheduleSearchAPI.java

@ -0,0 +1,493 @@
package org.leolo.nrapi.v0.api;
import org.leolo.nrapi.manager.DatabaseManager;
import org.leolo.nrapi.manager.PropertyManager;
import org.leolo.nrapi.v0.cache.TrainOperatorCache;
import org.leolo.nrapi.v0.model.DisplayableError;
import org.leolo.nrapi.v0.model.ScheduleSummary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
@RequestMapping(value={"/v0/search/schedule","/search/schedule"})
@RestController
public class ScheduleSearchAPI {
private final Logger log = LoggerFactory.getLogger(ScheduleSearchAPI.class);
@RequestMapping(value = {"{location}","{location}/{date}/{time}"})
public Object search(
@PathVariable(value = "location") String location,
@PathVariable(value = "date", required = false) String strDate,
@PathVariable(value = "time", required = false) String strTime,
@RequestParam(value = "range", required = false, defaultValue = "60") int range,
@RequestParam(value = "strict", required = false, defaultValue = "false") boolean strict,
@RequestParam(value = "stopOnly", required = false, defaultValue = "false") boolean stopOnly,
@RequestParam(value = "platform", required = false, defaultValue = "") String platform,
@RequestParam(value = "previous", required = false, defaultValue = "") String previous,
@RequestParam(value = "next", required = false, defaultValue = "") String next,
HttpServletRequest request
){
Date date = new Date();
Date time = new Date();
if(range > 120 | range <= 0){
range = 60;
}
try{
if(strDate!=null)
date = new SimpleDateFormat("yyyy-MM-dd").parse(strDate);
if(strTime!=null)
time = new SimpleDateFormat("HH:mm").parse(strTime);
}catch (ParseException e){
return new DisplayableError(e);
}
log.info("Search {}, stopOnly {}", location, stopOnly);
ArrayList<ScheduleSummary> list = new ArrayList<>();
try {
ScheduleSearchQueryBuilder queryBuilder = new ScheduleSearchQueryBuilder();
queryBuilder.setSearchDate(date)
.setSearchTime(time)
.setSearchLocation(LocationType.BOTH, location)
.setStrictMode(strict)
.setStopOnly(stopOnly)
.setSearchRange(range);
if(platform!=null && platform.length()!=0){
queryBuilder.setPlatforms(platform.split(","));
}
if(previous!=null&&previous.length()>0){
queryBuilder.setPrevLocation(previous);
}
if(next!=null&&next.length()>0){
queryBuilder.setNextLocation(next);
}
list.addAll(queryBuilder.setTableGroup(TableGroup.LONG_TERM_PLANNING).execute());
list.addAll(queryBuilder.setTableGroup(TableGroup.SHORT_TERM_PLANNING).execute());
}catch(SQLException e){
return new DisplayableError(e);
}
Collections.sort(list, (v1, v2)->{
if(v1 instanceof ScheduleSummary && v2 instanceof ScheduleSummary){
ScheduleSummary s1 = (ScheduleSummary) v1;
ScheduleSummary s2 = (ScheduleSummary) v2;
return getRepTime(s1).compareTo(getRepTime(s2));
}
return 0;
});
return list;
}
private Object _searchByCRS(String crsCode, Date date, Date time, int range, HttpServletRequest request) throws SQLException{
ArrayList<ScheduleSummary> list = new ArrayList<>();
list.addAll(
new ScheduleSearchQueryBuilder()
.setSearchDate(date)
.setSearchTime(time)
.setSearchLocation(LocationType.CRS, crsCode)
.setTableGroup(TableGroup.LONG_TERM_PLANNING)
.setStrictMode(false)
.execute()
);
list.addAll(
new ScheduleSearchQueryBuilder()
.setSearchDate(date)
.setSearchTime(time)
.setSearchLocation(LocationType.CRS, crsCode)
.setTableGroup(TableGroup.SHORT_TERM_PLANNING)
.setStrictMode(false)
.execute()
);
return list;
}
private Object _searchByTIPLOC(String tiplocCode, Date date, Date time, int range, HttpServletRequest request){
ArrayList<ScheduleSummary> result = new ArrayList<>();
HashMap<String, ScheduleStatus> status = new HashMap<>();
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"select" +
" l.suid, train_uid, arrival, departure, pass, platform, " +
" get_tiploc_name(origin), origin_time, get_tiploc_name(destination), destination_time, " +
" ls.signal_id, ls.atoc_code" +
" from" +
" ltp_location l" +
" JOIN ltp_schedule ls on l.suid = ls.suid" +
" where" +
" tiploc_code IN (select tiploc_code from tiploc " +
" where tiploc_code = ? " +
" OR nalco IN (SELECT nalco FROM tiploc WHERE tiploc_code = ?)" +
" OR stanox IN (SELECT stanox FROM tiploc WHERE tiploc_code = ?)" +
" UNION SELECT tiploc_code FROM tiploc_group " +
" WHERE group_id IN (SELECT group_id FROM tiploc_group WHERE tiploc_code = ?)" +
")" +
" and ? between ls.start_date and ls.end_date" +
" and ls.days like get_wd_str(?)" +
" and (" +
" abs(time_to_sec(pass)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(departure)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(arrival)-time_to_sec(?)) < ?" +
" )" +
" order by ifnull(departure, ifnull(arrival, pass))" +
" ;"
)
){
pstmt.setString(1, tiplocCode);
pstmt.setString(2, tiplocCode);
pstmt.setString(3, tiplocCode);
pstmt.setString(4, tiplocCode);
pstmt.setDate (5, new java.sql.Date(date.getTime()));
pstmt.setDate (6, new java.sql.Date(date.getTime()));
pstmt.setTime (7, new java.sql.Time(time.getTime()));
pstmt.setInt (8, range * 60);
pstmt.setTime (9, new java.sql.Time(time.getTime()));
pstmt.setInt (10, range * 60);
pstmt.setTime (11, new java.sql.Time(time.getTime()));
pstmt.setInt (12, range * 60);
long queryStart = System.currentTimeMillis();
try(ResultSet rs = pstmt.executeQuery()){
long queryEnd = System.currentTimeMillis();
log.info("Query takes {} ms", (queryEnd - queryStart));
result.addAll(processResult(rs, status, date, request));
}
}catch(SQLException e){
log.error(e.getMessage(), e);
return new DisplayableError(e);
}
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"select" +
" l.suid, train_uid, arrival, departure, pass, platform, " +
" get_tiploc_name(origin), origin_time, get_tiploc_name(destination), destination_time, " +
" ls.signal_id, ls.atoc_code" +
" from" +
" stp_location l" +
" JOIN stp_schedule ls on l.suid = ls.suid" +
" where" +
" tiploc_code IN (select tiploc_code from tiploc " +
" where tiploc_code = ? " +
" OR nalco IN (SELECT nalco FROM tiploc WHERE tiploc_code = ?)" +
" OR stanox IN (SELECT stanox FROM tiploc WHERE tiploc_code = ?)" +
" UNION SELECT tiploc_code FROM tiploc_group " +
" WHERE group_id IN (SELECT group_id FROM tiploc_group WHERE tiploc_code = ?)" +
")"+
" and ? between ls.start_date and ls.end_date" +
" and ls.days like get_wd_str(?)" +
" and (" +
" abs(time_to_sec(pass)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(departure)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(arrival)-time_to_sec(?)) < ?" +
" )" +
" order by ifnull(departure, ifnull(arrival, pass))" +
" ;"
)
){
pstmt.setString(1, tiplocCode);
pstmt.setString(2, tiplocCode);
pstmt.setString(3, tiplocCode);
pstmt.setString(4, tiplocCode);
pstmt.setDate (5, new java.sql.Date(date.getTime()));
pstmt.setDate (6, new java.sql.Date(date.getTime()));
pstmt.setTime (7, new java.sql.Time(time.getTime()));
pstmt.setInt (8, range * 60);
pstmt.setTime (9, new java.sql.Time(time.getTime()));
pstmt.setInt (10, range * 60);
pstmt.setTime (11, new java.sql.Time(time.getTime()));
pstmt.setInt (12, range * 60);
long queryStart = System.currentTimeMillis();
try(ResultSet rs = pstmt.executeQuery()){
long queryEnd = System.currentTimeMillis();
log.info("Query takes {} ms", (queryEnd - queryStart));
result.addAll(processResult(rs, status, date, request));
}
}catch(SQLException e){
log.error(e.getMessage(), e);
return new DisplayableError(e);
}
return result;
}
private Object _searchByCRSStrict(String crsCode, Date date, Date time, int range, HttpServletRequest request){
ArrayList<ScheduleSummary> result = new ArrayList<>();
HashMap<String, ScheduleStatus> status = new HashMap<>();
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"select" +
" l.suid, train_uid, arrival, departure, pass, platform, " +
" get_tiploc_name(origin), origin_time, get_tiploc_name(destination), destination_time, " +
" ls.signal_id, ls.atoc_code" +
" from" +
" ltp_location l" +
" JOIN ltp_schedule ls on l.suid = ls.suid" +
" where" +
" tiploc_code IN (SELECT tiploc_code FROM tiploc WHERE crs = ?)" +
" and ? between ls.start_date and ls.end_date" +
" and ls.days like get_wd_str(?)" +
" and (" +
" abs(time_to_sec(pass)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(departure)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(arrival)-time_to_sec(?)) < ?" +
" )" +
" order by ifnull(departure, ifnull(arrival, pass))" +
" ;"
)
){
pstmt.setString(1, crsCode);
pstmt.setDate (2, new java.sql.Date(date.getTime()));
pstmt.setDate (3, new java.sql.Date(date.getTime()));
pstmt.setTime (4, new java.sql.Time(time.getTime()));
pstmt.setInt (5, range * 60);
pstmt.setTime (6, new java.sql.Time(time.getTime()));
pstmt.setInt (7, range * 60);
pstmt.setTime (8, new java.sql.Time(time.getTime()));
pstmt.setInt (9, range * 60);
long queryStart = System.currentTimeMillis();
try(ResultSet rs = pstmt.executeQuery()){
long queryEnd = System.currentTimeMillis();
log.info("Query takes {} ms", (queryEnd - queryStart));
result.addAll(processResult(rs, status, date, request));
}
}catch(SQLException e){
log.error(e.getMessage(), e);
return new DisplayableError(e);
}
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"select" +
" l.suid, train_uid, arrival, departure, pass, platform, " +
" get_tiploc_name(origin), origin_time, get_tiploc_name(destination), destination_time, " +
" ls.signal_id, ls.atoc_code" +
" from" +
" stp_location l" +
" JOIN stp_schedule ls on l.suid = ls.suid" +
" where" +
" tiploc_code IN (SELECT tiploc_code FROM tiploc WHERE crs = ?)" +
" and ? between ls.start_date and ls.end_date" +
" and ls.days like get_wd_str(?)" +
" and (" +
" abs(time_to_sec(pass)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(departure)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(arrival)-time_to_sec(?)) < ?" +
" )" +
" order by ifnull(departure, ifnull(arrival, pass))" +
" ;"
)
){
pstmt.setString(1, crsCode);
pstmt.setDate (2, new java.sql.Date(date.getTime()));
pstmt.setDate (3, new java.sql.Date(date.getTime()));
pstmt.setTime (4, new java.sql.Time(time.getTime()));
pstmt.setInt (5, range * 60);
pstmt.setTime (6, new java.sql.Time(time.getTime()));
pstmt.setInt (7, range * 60);
pstmt.setTime (8, new java.sql.Time(time.getTime()));
pstmt.setInt (9, range * 60);
long queryStart = System.currentTimeMillis();
try(ResultSet rs = pstmt.executeQuery()){
long queryEnd = System.currentTimeMillis();
log.info("Query takes {} ms", (queryEnd - queryStart));
result.addAll(processResult(rs, status, date, request));
}
}catch(SQLException e){
log.error(e.getMessage(), e);
return new DisplayableError(e);
}
return result;
}
private Object _searchByTIPLOCStrict(String tiplocCode, Date date, Date time, int range, HttpServletRequest request){
ArrayList<ScheduleSummary> result = new ArrayList<>();
HashMap<String, ScheduleStatus> status = new HashMap<>();
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"select" +
" l.suid, train_uid, arrival, departure, pass, platform, " +
" get_tiploc_name(origin), origin_time, get_tiploc_name(destination), destination_time, " +
" ls.signal_id, ls.atoc_code" +
" from" +
" ltp_location l" +
" JOIN ltp_schedule ls on l.suid = ls.suid" +
" where" +
" tiploc_code = ?" +
" and ? between ls.start_date and ls.end_date" +
" and ls.days like get_wd_str(?)" +
" and (" +
" abs(time_to_sec(pass)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(departure)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(arrival)-time_to_sec(?)) < ?" +
" )" +
" order by ifnull(departure, ifnull(arrival, pass))" +
" ;"
)
){
pstmt.setString(1, tiplocCode);
pstmt.setDate (2, new java.sql.Date(date.getTime()));
pstmt.setDate (3, new java.sql.Date(date.getTime()));
pstmt.setTime (4, new java.sql.Time(time.getTime()));
pstmt.setInt (5, range * 60);
pstmt.setTime (6, new java.sql.Time(time.getTime()));
pstmt.setInt (7, range * 60);
pstmt.setTime (8, new java.sql.Time(time.getTime()));
pstmt.setInt (9, range * 60);
long queryStart = System.currentTimeMillis();
try(ResultSet rs = pstmt.executeQuery()){
long queryEnd = System.currentTimeMillis();
log.info("Query takes {} ms", (queryEnd - queryStart));
result.addAll(processResult(rs, status, date, request));
}
}catch(SQLException e){
log.error(e.getMessage(), e);
return new DisplayableError(e);
}
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"select" +
" l.suid, train_uid, arrival, departure, pass, platform, " +
" get_tiploc_name(origin), origin_time, get_tiploc_name(destination), destination_time, " +
" ls.signal_id, ls.atoc_code" +
" from" +
" stp_location l" +
" JOIN stp_schedule ls on l.suid = ls.suid" +
" where" +
" tiploc_code = ?" +
" and ? between ls.start_date and ls.end_date" +
" and ls.days like get_wd_str(?)" +
" and (" +
" abs(time_to_sec(pass)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(departure)-time_to_sec(?)) < ?" +
" OR abs(time_to_sec(arrival)-time_to_sec(?)) < ?" +
" )" +
" order by ifnull(departure, ifnull(arrival, pass))" +
" ;"
)
){
pstmt.setString(1, tiplocCode);
pstmt.setDate (2, new java.sql.Date(date.getTime()));
pstmt.setDate (3, new java.sql.Date(date.getTime()));
pstmt.setTime (4, new java.sql.Time(time.getTime()));
pstmt.setInt (5, range * 60);
pstmt.setTime (6, new java.sql.Time(time.getTime()));
pstmt.setInt (7, range * 60);
pstmt.setTime (8, new java.sql.Time(time.getTime()));
pstmt.setInt (9, range * 60);
long queryStart = System.currentTimeMillis();
try(ResultSet rs = pstmt.executeQuery()){
long queryEnd = System.currentTimeMillis();
log.info("Query takes {} ms", (queryEnd - queryStart));
result.addAll(processResult(rs, status, date, request));
}
}catch(SQLException e){
log.error(e.getMessage(), e);
return new DisplayableError(e);
}
return result;
}
private Collection<? extends ScheduleSummary> processResult(ResultSet rs, HashMap<String, ScheduleStatus> status, Date date, HttpServletRequest request) throws SQLException{
ArrayList<ScheduleSummary> result = new ArrayList<>();
while(rs.next()) {
ScheduleStatus ss = null;
if (status.containsKey(rs.getString(1))) {
ss = status.get(rs.getString(1));
} else {
ss = getScheduleStatus(rs.getString(1), rs.getString(2), date);
status.put(rs.getString(1), ss);//Put the status in map for potential use later
}
if(ss==ScheduleStatus.OVERRIDDEN){
continue;
}
ScheduleSummary summary = new ScheduleSummary();
summary.setHeadCode(rs.getString(11));
summary.setArrivalTime(rs.getTime(3));
summary.setDepartureTime(rs.getTime(4));
summary.setPassingTime(rs.getTime(5));
summary.setPlatform(rs.getString(6));
summary.setOriginName(rs.getString(7));
summary.setOriginTime(rs.getTime(8));
summary.setDestinationName(rs.getString(9));
summary.setDestinationTime(rs.getTime(10));
summary.setTocCode(rs.getString(12));
summary.setTocName(TrainOperatorCache.getInstance().getTocNameByAtocCode(rs.getString(12)));
StringBuilder url = new StringBuilder();
url.append(PropertyManager.getInstance().getProperties().getProperty("generic.host"));
url.append(request.getContextPath())
.append("/v0/schedule/")
.append(rs.getString(2))
.append("/")
.append(new SimpleDateFormat("yyyy-MM-dd").format(date));
summary.setDetailUrl(url.toString());
summary.setCancelled(ss==ScheduleStatus.CANCELLED);
result.add(summary);
}
return result;
}
private ScheduleStatus getScheduleStatus(String scheduleId, String trainId, Date date) throws SQLException{
if(scheduleId.endsWith("C")){
return ScheduleStatus.CANCELLED;
}
if(scheduleId.endsWith("O")){
//Overlay record is always deem to be valid
return ScheduleStatus.VALID;
}
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"SELECT suid, sch_type FROM ltp_schedule " +
"WHERE train_uid = ? " +
"AND ? between start_date and end_date " +
"and days like get_wd_str(?) " +
"ORDER BY IF(sch_type = 'WTT', 1, 0)"
)
){
pstmt.setString(1, trainId);
pstmt.setDate(2, new java.sql.Date(date.getTime()));
pstmt.setDate(3, new java.sql.Date(date.getTime()));
try(ResultSet rs = pstmt.executeQuery()){
rs.next();
if(!rs.getString(1).equals(scheduleId)){
if("CAN".equals(rs.getString(2))){
return ScheduleStatus.CANCELLED;
}
if("OVL".equals(rs.getString(2))){
return ScheduleStatus.OVERRIDDEN;
}
}
return ScheduleStatus.VALID;
}
}
}
private enum ScheduleStatus{
VALID,
OVERRIDDEN,
CANCELLED;
}
private Date getRepTime(ScheduleSummary scheduleSummary){
if(scheduleSummary.getDepartureTime()!=null)
return scheduleSummary.getDepartureTime();
if(scheduleSummary.getArrivalTime()!=null)
return scheduleSummary.getArrivalTime();
return scheduleSummary.getPassingTime();
}
}

289
src/main/java/org/leolo/nrapi/v0/api/ScheduleSearchQueryBuilder.java

@ -0,0 +1,289 @@
package org.leolo.nrapi.v0.api;
import org.leolo.nrapi.db.SearchParameter;
import org.leolo.nrapi.manager.DatabaseManager;
import org.leolo.nrapi.manager.PropertyManager;
import org.leolo.nrapi.util.HttpReqRespUtils;
import org.leolo.nrapi.v0.cache.TrainOperatorCache;
import org.leolo.nrapi.v0.model.ScheduleSummary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
class ScheduleSearchQueryBuilder {
private Logger log = LoggerFactory.getLogger(ScheduleSearchQueryBuilder.class);
private TableGroup tableGroup = TableGroup.LONG_TERM_PLANNING;
private LocationType searchLocationType;
private String searchLocation;
private Date searchDate = new Date();
private Date searchTime = new Date();
private int searchRange = 60;
private boolean strictMode = false;
private String[] platforms;
private boolean stopOnly = false;
private String prevLocation;
private String nextLocation;
public ScheduleSearchQueryBuilder setTableGroup(TableGroup tableGroup) {
this.tableGroup = tableGroup;
return this;
}
public ScheduleSearchQueryBuilder setSearchLocation(LocationType locationType, String searchLocation) {
this.searchLocationType = searchLocationType;
this.searchLocation = searchLocation;
return this;
}
public ScheduleSearchQueryBuilder setSearchDate(Date searchDate) {
this.searchDate = searchDate;
return this;
}
public ScheduleSearchQueryBuilder setSearchTime(Date searchTime) {
this.searchTime = searchTime;
return this;
}
public ScheduleSearchQueryBuilder setSearchRange(int searchRange) {
this.searchRange = searchRange;
return this;
}
public ScheduleSearchQueryBuilder setStrictMode(boolean strictMode) {
this.strictMode = strictMode;
return this;
}
public ScheduleSearchQueryBuilder setStopOnly(boolean stopOnly) {
this.stopOnly = stopOnly;
return this;
}
public ScheduleSearchQueryBuilder setPlatforms(String... platforms){
this.platforms = platforms;
return this;
}
public ScheduleSearchQueryBuilder setPrevLocation(String prevLocation) {
this.prevLocation = prevLocation;
return this;
}
public ScheduleSearchQueryBuilder setNextLocation(String nextLocation) {
this.nextLocation = nextLocation;
return this;
}
public List<ScheduleSummary> execute() throws SQLException {
if(searchLocation==null){
throw new IllegalArgumentException("Location must be specified");
}
ArrayList<ScheduleSummary> list = new ArrayList<>();
ArrayList<SearchParameter> params = new ArrayList<>();
//Build the SQL
StringBuilder sql = new StringBuilder();
sql.append("SELECT mt.suid, train_uid, dt.arrival, dt.departure, dt.pass, dt.platform, get_tiploc_name(origin), ");
sql.append("origin_time, get_tiploc_name(destination), destination_time, mt.signal_id, mt.atoc_code ");
sql.append(" from ").append(tableGroup.getMainTable()).append(" mt ")
.append(" JOIN ").append(tableGroup.getDetailTable()).append(" dt ON mt.suid=dt.suid ");
if(prevLocation!=null&&prevLocation.length()!=0){
sql.append(" JOIN ").append(tableGroup.getDetailTable()).append(" pt ON mt.suid=pt.suid AND dt.seq > pt.seq");
}
if(nextLocation!=null&&nextLocation.length()!=0){
sql.append(" JOIN ").append(tableGroup.getDetailTable()).append(" nt ON mt.suid=nt.suid AND dt.seq < nt.seq");
}
sql.append(" where dt.tiploc_code IN (");
appendLocationSearch(sql, params, searchLocation);
sql.append(") AND (");
sql.append("(abs(time_to_sec(dt.departure)-time_to_sec(?)) <= ?)");
sql.append("OR (abs(time_to_sec(dt.arrival)-time_to_sec(?)) <= ?)");
params.add(SearchParameter.timeParameter(searchTime));
params.add(SearchParameter.numberParameter(searchRange*60));
params.add(SearchParameter.timeParameter(searchTime));
params.add(SearchParameter.numberParameter(searchRange*60));
if(!stopOnly){
sql.append("OR (abs(time_to_sec(dt.pass)-time_to_sec(?)) <= ?)");
params.add(SearchParameter.timeParameter(searchTime));
params.add(SearchParameter.numberParameter(searchRange*60));
}
sql.append(")");
sql.append(" AND ? BETWEEN mt.start_date AND mt.end_date AND mt.days like get_wd_str(?) ");
params.add(SearchParameter.dateParameter(searchDate));
params.add(SearchParameter.dateParameter(searchDate));
if(platforms!=null){
boolean hadEntry = false;
sql.append(" AND platform IN (");
for(String plat:platforms){
if(hadEntry){
sql.append(",?");
}else{
sql.append("?");
hadEntry = true;
}
params.add(SearchParameter.stringParameter(plat));
}
sql.append(")");
}
if(prevLocation!=null&&prevLocation.length()!=0){
sql.append(" AND pt.tiploc_code IN (");
appendLocationSearch(sql, params, prevLocation);
sql.append(") ");
}
if(nextLocation!=null&&nextLocation.length()!=0){
sql.append(" AND nt.tiploc_code IN (");
appendLocationSearch(sql, params, nextLocation);
sql.append(") ");
}
log.info(sql.toString());
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql.toString())
){
for(int i=0;i<params.size();){
params.get(i).setParam(pstmt, ++i);
}
try(ResultSet rs = pstmt.executeQuery()){
while(rs.next()) {
ScheduleSummary summary = new ScheduleSummary();
String scheduleId = rs.getString(1);
ScheduleStatus ss = getScheduleStatus(scheduleId, rs.getString(2), searchDate);
if(ss == ScheduleStatus.OVERRIDDEN){
continue;
}
summary.setHeadCode(rs.getString(11));
summary.setArrivalTime(rs.getTime(3));
summary.setDepartureTime(rs.getTime(4));
summary.setPassingTime(rs.getTime(5));
summary.setPlatform(rs.getString(6));
summary.setOriginName(rs.getString(7));
summary.setOriginTime(rs.getTime(8));
summary.setDestinationName(rs.getString(9));
summary.setDestinationTime(rs.getTime(10));
summary.setTocCode(rs.getString(12));
summary.setTocName(TrainOperatorCache.getInstance().getTocNameByAtocCode(rs.getString(12)));
StringBuilder url = new StringBuilder();
url.append(PropertyManager.getInstance().getProperties().getProperty("generic.host"));
url.append(HttpReqRespUtils.getContextPath())
.append("/v0/schedule/")
.append(rs.getString(2))
.append("/")
.append(new SimpleDateFormat("yyyy-MM-dd").format(searchDate));
summary.setDetailUrl(url.toString());
summary.setCancelled(ss==ScheduleStatus.CANCELLED);
list.add(summary);
}
}
}
return list;
}
private void appendLocationSearch(StringBuilder sql, ArrayList<SearchParameter> params, String searchLocation) {
if(strictMode){
if(searchLocationType== LocationType.TIPLOC){
sql.append("?");
params.add(SearchParameter.stringParameter(searchLocation));
}else if(searchLocationType==LocationType.CRS){
sql.append("SELECT tiploc_code FROM tiploc WHERE crs = ?");
params.add(SearchParameter.stringParameter(searchLocation));
}else{
sql.append("SELECT tiploc_code FROM tiploc WHERE crs = ? OR tiploc_code = ?");
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
}
}else{
if(searchLocationType==LocationType.TIPLOC){
sql.append("SELECT tiploc_code FROM tiploc WHERE tiploc_code = ? ")
.append("OR nalco IN (SELECT nalco FROM tiploc WHERE tiploc_code = ?) ")
.append("OR stanox IN (SELECT stanox FROM tiploc WHERE tiploc_code = ?) ")
.append("UNION SELECT tiploc_code FROM tiploc_group WHERE group_id IN ")
.append("(SELECT group_id FROM tiploc_group WHERE tiploc_code = ?)");
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
}else if(searchLocationType==LocationType.CRS){
sql.append("SELECT tiploc_code FROM tiploc WHERE crs = ? ")
.append("OR nalco IN (SELECT nalco FROM tiploc WHERE crs = ?) ")
.append("OR stanox IN (SELECT stanox FROM tiploc WHERE crs = ?) ")
.append("UNION SELECT tiploc_code FROM tiploc_group WHERE group_id IN ")
.append("(SELECT group_id FROM tiploc_group WHERE tiploc_code IN ")
.append("(SELECT tiploc_code FROM tiploc WHERE crs = ?))");
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
}else{
sql.append("SELECT tiploc_code FROM tiploc WHERE crs = ? OR tiploc_code = ? ")
.append("OR nalco IN (SELECT nalco FROM tiploc WHERE crs = ? OR tiploc_code = ?) ")
.append("OR stanox IN (SELECT stanox FROM tiploc WHERE crs = ? OR tiploc_code = ?) ")
.append("UNION SELECT tiploc_code FROM tiploc_group WHERE group_id IN ")
.append("(SELECT group_id FROM tiploc_group WHERE tiploc_code IN ")
.append("(SELECT tiploc_code FROM tiploc WHERE crs = ? OR tiploc_code = ?))");
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
params.add(SearchParameter.stringParameter(searchLocation));
}
}
}
private enum ScheduleStatus{
VALID,
OVERRIDDEN,
CANCELLED;
}
private ScheduleStatus getScheduleStatus(String scheduleId, String trainId, Date date) throws SQLException{
if(scheduleId.endsWith("C")){
return ScheduleStatus.CANCELLED;
}
if(scheduleId.endsWith("O")){
//Overlay record is always deem to be valid
return ScheduleStatus.VALID;
}
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement(
"SELECT suid, sch_type FROM ltp_schedule " +
"WHERE train_uid = ? " +
"AND ? between start_date and end_date " +
"and days like get_wd_str(?) " +
"ORDER BY IF(sch_type = 'WTT', 1, 0)"
)
){
pstmt.setString(1, trainId);
pstmt.setDate(2, new java.sql.Date(date.getTime()));
pstmt.setDate(3, new java.sql.Date(date.getTime()));
try(ResultSet rs = pstmt.executeQuery()){
rs.next();
if(!rs.getString(1).equals(scheduleId)){
if("CAN".equals(rs.getString(2))){
return ScheduleStatus.CANCELLED;
}
if("OVL".equals(rs.getString(2))){
return ScheduleStatus.OVERRIDDEN;
}
}
return ScheduleStatus.VALID;
}
}
}
}

29
src/main/java/org/leolo/nrapi/v0/api/TableGroup.java

@ -0,0 +1,29 @@
package org.leolo.nrapi.v0.api;
public enum TableGroup {
LONG_TERM_PLANNING("ltp_schedule", "ltp_location", "ltp_association"),
SHORT_TERM_PLANNING("stp_schedule", "stp_location", "stp_association");
private String mainTable;
private String detailTable;
private String associationTable;
public String getMainTable() {
return mainTable;
}
public String getDetailTable() {
return detailTable;
}
public String getAssociationTable() {
return associationTable;
}
TableGroup(String mainTable, String detailTable, String associationTable) {
this.mainTable = mainTable;
this.detailTable = detailTable;
this.associationTable = associationTable;
}
}

57
src/main/java/org/leolo/nrapi/v0/cache/TrainCategoryCache.java vendored

@ -0,0 +1,57 @@
package org.leolo.nrapi.v0.cache;
import org.leolo.nrapi.manager.CacheManager;
import org.leolo.nrapi.manager.CacheProvider;
import org.leolo.nrapi.manager.DatabaseManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
public class TrainCategoryCache implements CacheProvider {
private static TrainCategoryCache instance;
private HashMap<String, String> entries = new HashMap<>();
private final Logger LOG = LoggerFactory.getLogger(TrainCategoryCache.class);
@Override
public void clearCache() {
entries.clear();
loadEntries();
}
public String getTrainCategory(String categoryCode){
return entries.get(categoryCode);
}
public void loadEntries(){
try(Connection conn = DatabaseManager.getInstance().getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT category, description FROM train_category")){
while(rs.next()){
entries.put(rs.getString(1), rs.getString(2).strip());
}
}catch(SQLException e){
LOG.error(e.getMessage(), e);
}
}
public static synchronized TrainCategoryCache getInstance(){
if(instance==null){
instance = new TrainCategoryCache();
}
return instance;
}
private TrainCategoryCache(){
CacheManager.getInstance().addProvider(this);
loadEntries();
}
}

57
src/main/java/org/leolo/nrapi/v0/cache/TrainOperatorCache.java vendored

@ -0,0 +1,57 @@
package org.leolo.nrapi.v0.cache;
import org.leolo.nrapi.manager.CacheProvider;
import org.leolo.nrapi.manager.DatabaseManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
public class TrainOperatorCache implements CacheProvider {
private static TrainOperatorCache instance;
private final Logger LOG = LoggerFactory.getLogger(TrainOperatorCache.class);
public static synchronized TrainOperatorCache getInstance(){
if(instance==null){
instance = new TrainOperatorCache();
}
return instance;
}
private HashMap<String, String> atocMap = new HashMap<>();
@Override
public void clearCache() {
atocMap.clear();
}
public String getTocNameByAtocCode(String atocCode){
if(atocMap.containsKey(atocCode)){
return atocMap.get(atocCode);
}
try(
Connection conn = DatabaseManager.getInstance().getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT name FROM toc WHERE atoc_code = ?")
){
pstmt.setString(1, atocCode);
try(ResultSet rs = pstmt.executeQuery()){
if(rs.next()){
String tocName = rs.getString(1).strip();
if(!rs.next()) {
atocMap.put(atocCode, tocName);
return tocName;
}else{
atocMap.put(atocCode, "");
return "";
}
}
}
}catch (Exception e){
LOG.error(e.getMessage(), e);
}
return null;
}
}

42
src/main/java/org/leolo/nrapi/v0/model/BackupEntry.java

@ -0,0 +1,42 @@
package org.leolo.nrapi.v0.model;
import java.util.Date;
public class BackupEntry {
private String key;
private String url;
private Date createdDate;
private int fileSize;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Date getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public int getFileSize() {
return fileSize;
}
public void setFileSize(int fileSize) {
this.fileSize = fileSize;
}
}

52
src/main/java/org/leolo/nrapi/v0/model/DisplayableError.java

@ -0,0 +1,52 @@
package org.leolo.nrapi.v0.model;
import java.sql.SQLException;
import java.util.Date;
public class DisplayableError implements ReturnSet{
private String exceptionName;
private String exceptionMessage;
public String getStatus(){
return "error";
}
public Date getErrorTime(){
return new Date();
}
public String getExceptionName() {
return exceptionName;
}
public void setExceptionName(String exceptionName) {
this.exceptionName = exceptionName;
}
public String getExceptionMessage() {
return exceptionMessage;
}
public void setExceptionMessage(String exceptionMessage) {
this.exceptionMessage = exceptionMessage;
}
public DisplayableError() {
}
public DisplayableError(String exceptionName, String exceptionMessage) {
this.exceptionName = exceptionName;
this.exceptionMessage = exceptionMessage;
}
public DisplayableError(Exception e){
this.exceptionName = e.getClass().getName();
this.exceptionMessage = e.getMessage();
}
public DisplayableError(SQLException e){
this.exceptionName = "Database error";
this.exceptionMessage = "Database error, please consult the server log.";
}
}

84
src/main/java/org/leolo/nrapi/v0/model/LocationSearchResult.java

@ -0,0 +1,84 @@
package org.leolo.nrapi.v0.model;
import org.leolo.nrapi.util.StringUtil;
import java.util.Objects;
public class LocationSearchResult {
private String tiplocCode;
private String nalco;
private String stanox;
private String crsCode;
private String displayName;
private int matchScore;
private int resultGroup;
public String getTiplocCode() {
return tiplocCode;
}
public void setTiplocCode(String tiplocCode) {
this.tiplocCode = tiplocCode;
}
public String getNalco() {
return nalco;
}
public void setNalco(String nalco) {
this.nalco = nalco;
}
public String getStanox() {
return stanox;
}
public void setStanox(String stanox) {
this.stanox = stanox;
}
public String getCrsCode() {
return crsCode;
}
public void setCrsCode(String crsCode) {
this.crsCode = crsCode;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public int getMatchScore() {
return matchScore * (crsCode==null?1:4) * (stanox==null?1:4) * (tiplocCode.hashCode()==resultGroup?2:1);
}
public void setMatchScore(double matchScore) {
this.matchScore = (int)(matchScore*1000);
}
public String getResultGroup() {
return StringUtil.lpad(Integer.toHexString(resultGroup), 8 , '0');
}
public void setResultGroup(int resultGroup) {
this.resultGroup = resultGroup;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LocationSearchResult that = (LocationSearchResult) o;
return Objects.equals(tiplocCode, that.tiplocCode) && Objects.equals(nalco, that.nalco) && Objects.equals(stanox, that.stanox) && Objects.equals(crsCode, that.crsCode) && Objects.equals(displayName, that.displayName);
}
@Override
public int hashCode() {
return Objects.hash(tiplocCode, nalco, stanox, crsCode, displayName);
}
}

9
src/main/java/org/leolo/nrapi/v0/model/ReturnSet.java

@ -0,0 +1,9 @@
package org.leolo.nrapi.v0.model;
public interface ReturnSet {
public default String get_type(){
return getClass().getTypeName();
}
}

126
src/main/java/org/leolo/nrapi/v0/model/ScheduleSummary.java

@ -0,0 +1,126 @@
package org.leolo.nrapi.v0.model;
import java.util.Date;
public class ScheduleSummary {
private String headCode;
private String originName;
private String destinationName;
private Date originTime;
private Date destinationTime;
private String platform;
private Date arrivalTime;
private Date departureTime;
private Date passingTime;
private String detailUrl;
private String tocCode;
private String tocName;
private boolean cancelled = false;
public String getHeadCode() {
return headCode;
}
public void setHeadCode(String headCode) {
this.headCode = headCode;
}
public String getOriginName() {
return originName;
}
public void setOriginName(String originName) {
this.originName = originName;
}
public String getDestinationName() {
return destinationName;
}
public void setDestinationName(String destinationName) {
this.destinationName = destinationName;
}
public Date getOriginTime() {
return originTime;
}
public void setOriginTime(Date originTime) {
this.originTime = originTime;
}
public Date getDestinationTime() {
return destinationTime;
}
public void setDestinationTime(Date destinationTime) {
this.destinationTime = destinationTime;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public Date getArrivalTime() {
return arrivalTime;
}
public void setArrivalTime(Date arrivalTime) {
this.arrivalTime = arrivalTime;
}
public Date getDepartureTime() {
return departureTime;
}
public void setDepartureTime(Date departureTime) {
this.departureTime = departureTime;
}
public Date getPassingTime() {
return passingTime;
}
public void setPassingTime(Date passingTime) {
this.passingTime = passingTime;
}
public String getDetailUrl() {
return detailUrl;
}
public void setDetailUrl(String detailUrl) {
this.detailUrl = detailUrl;
}
public String getTocCode() {
return tocCode;
}
public void setTocCode(String tocCode) {
this.tocCode = tocCode;
}
public String getTocName() {
return tocName;
}
public void setTocName(String tocName) {
this.tocName = tocName;
}
public boolean isCancelled() {
return cancelled;
}
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}

23
src/main/java/org/leolo/nrapi/v0/model/TrainAssociationInfo.java

@ -0,0 +1,23 @@
package org.leolo.nrapi.v0.model;
public class TrainAssociationInfo {
private String associatedTrainId;
private String associationType;
public String getAssociatedTrainId() {
return associatedTrainId;
}
public void setAssociatedTrainId(String associatedTrainId) {
this.associatedTrainId = associatedTrainId;
}
public String getAssociationType() {
return associationType;
}
public void setAssociationType(String associationType) {
this.associationType = associationType;
}
}

184
src/main/java/org/leolo/nrapi/v0/model/TrainSchedule.java

@ -0,0 +1,184 @@
package org.leolo.nrapi.v0.model;
import java.util.ArrayList;
import java.util.Date;
public class TrainSchedule implements ReturnSet{
private String trainId;
private String scheduleType;
private Date startDate;
private Date endDate;
private String daysRun;
private String bankHolidayRun;
private String status;
private String category;
private String headcode;
private String reservationSystemHeadcode;
private String powerType;
private String timingLoad;
private String speed;
private String operatingCharacters;
private String classAvailable;
private String reservation;
private String catering;
private String trainOperatorCode;
private String trainOperatorName;
private ArrayList<TrainScheduleDetails> scheduleEntries = new ArrayList<>();
public String getTrainId() {
return trainId;
}
public String getScheduleType() {
return scheduleType;
}
public Date getStartDate() {
return startDate;
}
public Date getEndDate() {
return endDate;
}
public String getDaysRun() {
return daysRun;
}
public String getBankHolidayRun() {
return bankHolidayRun;
}
public String getStatus() {
return status;
}
public String getCategory() {
return category;
}
public String getHeadcode() {
return headcode;
}
public String getReservationSystemHeadcode() {
return reservationSystemHeadcode;
}
public String getPowerType() {
return powerType;
}
public String getTimingLoad() {
return timingLoad;
}
public String getSpeed() {
return speed;
}
public String getOperatingCharacters() {
return operatingCharacters;
}
public String getClassAvailable() {
return classAvailable;
}
public String getReservation() {
return reservation;
}
public String getCatering() {
return catering;
}
public String getTrainOperatorCode() {
return trainOperatorCode;
}
public String getTrainOperatorName() {
return trainOperatorName;
}
public ArrayList<TrainScheduleDetails> getScheduleEntries() {
return scheduleEntries;
}
public void setTrainId(String trainId) {
this.trainId = trainId;
}
public void setScheduleType(String scheduleType) {
this.scheduleType = scheduleType;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
public void setDaysRun(String daysRun) {
this.daysRun = daysRun;
}
public void setBankHolidayRun(String bankHolidayRun) {
this.bankHolidayRun = bankHolidayRun;
}
public void setStatus(String status) {
this.status = status;
}
public void setCategory(String category) {
this.category = category;
}
public void setHeadcode(String headcode) {
this.headcode = headcode;
}
public void setReservationSystemHeadcode(String reservationSystemHeadcode) {
this.reservationSystemHeadcode = reservationSystemHeadcode;
}
public void setPowerType(String powerType) {
this.powerType = powerType;
}
public void setTimingLoad(String timingLoad) {
this.timingLoad = timingLoad;
}
public void setSpeed(String speed) {
this.speed = speed;
}
public void setOperatingCharacters(String operatingCharacters) {
this.operatingCharacters = operatingCharacters;
}
public void setClassAvailable(String classAvailable) {
this.classAvailable = classAvailable;
}
public void setReservation(String reservation) {
this.reservation = reservation;
}
public void setCatering(String catering) {
this.catering = catering;
}
public void setTrainOperatorCode(String trainOperatorCode) {
this.trainOperatorCode = trainOperatorCode;
}
public void setTrainOperatorName(String trainOperatorName) {
this.trainOperatorName = trainOperatorName;
}
}

151
src/main/java/org/leolo/nrapi/v0/model/TrainScheduleDetails.java

@ -0,0 +1,151 @@
package org.leolo.nrapi.v0.model;
import java.util.ArrayList;
public class TrainScheduleDetails {
private String tiplocCode;
private String tiplocName;
public String getTiplocName() {
return tiplocName;
}
public void setTiplocName(String toplocName) {
this.tiplocName = toplocName;
}
private String instance;
private String wttArrival;
private String wttDeparture;
private String wttPass;
private String gbttArrival;
private String gbttDeparture;
private String platform;
private String line;
private String path;
private String engineeringAllowance;
private String pathingAllowance;
private String performanceAllowance;
private String crsCode;
public String getCrsCode() {
return crsCode;
}
public void setCrsCode(String crsCode) {
this.crsCode = crsCode;
}
private ArrayList<TrainAssociationInfo> association = new ArrayList<>();
public ArrayList<TrainAssociationInfo> getAssociation() {
return association;
}
public String getTiplocCode() {
return tiplocCode;
}
public void setTiplocCode(String tiplocCode) {
this.tiplocCode = tiplocCode;
}
public String getInstance() {
return instance;
}
public void setInstance(String instance) {
this.instance = instance;
}
public String getWttArrival() {
return wttArrival;
}
public void setWttArrival(String wttArrival) {
this.wttArrival = wttArrival;
}
public String getWttDeparture() {
return wttDeparture;
}
public void setWttDeparture(String wttDeparture) {
this.wttDeparture = wttDeparture;
}
public String getWttPass() {
return wttPass;
}
public void setWttPass(String wttPass) {
this.wttPass = wttPass;
}
public String getGbttArrival() {
return gbttArrival;
}
public void setGbttArrival(String gbttArrival) {
this.gbttArrival = gbttArrival;
}
public String getGbttDeparture() {
return gbttDeparture;
}
public void setGbttDeparture(String gbttDeparture) {
this.gbttDeparture = gbttDeparture;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getLine() {
return line;
}
public void setLine(String line) {
this.line = line;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getEngineeringAllowance() {
return engineeringAllowance;
}
public void setEngineeringAllowance(String engineeringAllowance) {
this.engineeringAllowance = engineeringAllowance;
}
public String getPathingAllowance() {
return pathingAllowance;
}
public void setPathingAllowance(String pathingAllowance) {
this.pathingAllowance = pathingAllowance;
}
public String getPerformanceAllowance() {
return performanceAllowance;
}
public void setPerformanceAllowance(String performanceAllowance) {
this.performanceAllowance = performanceAllowance;
}
}

34
src/main/java/org/leolo/nrapi/web/LoginAPI.java

@ -0,0 +1,34 @@
package org.leolo.nrapi.web;
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.RestController;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping(value = "web/auth")
public class LoginAPI {
@RequestMapping(value = "login", method = RequestMethod.POST)
public Object doLogin(
@RequestParam(name="username", required = true) String userName,
@RequestParam(name="password", required = true) String password,
HttpSession session
){
return new Object(){
public String getStatus(){return "failed";}
public String getMessage() {return "Not implemented";}
};
}
@RequestMapping(value = "logout")
public Object doLogin(
HttpSession session
){
session.invalidate();
return new Object(){
public String getStatus(){return "success";}
public String getMessage() {return "Successfully logged out";}
};
}
}

159
src/main/java/org/leolo/nrapi/web/TokenStore.java

@ -0,0 +1,159 @@
package org.leolo.nrapi.web;
import org.leolo.nrapi.manager.CacheProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
public class TokenStore implements CacheProvider {
private Logger log = LoggerFactory.getLogger(TokenStore.class);
private Hashtable<String, TokenStoreEntry> tokenStore = new Hashtable<>();
private static TokenStore instance;
/**
* Default validity period of a token in millisecond.
*
* The default value is 15 minutes.
*/
public static final long DEFAULT_TOKEN_TIME = 900_000;
/**
* Maximum validity period of a generated token in millisecond.
*
* The maximum value is 14 days.
*/
public static final long MAX_TOKEN_TIME = 1_209_600_000;
public static synchronized TokenStore getInstance(){
if(instance==null){
instance = new TokenStore();
}
return instance;
}
//An empty default constructor to avoid being instanced
private TokenStore(){
}
public String generateToken(long userId){
return generateToken(userId, DEFAULT_TOKEN_TIME);
}
public String generateToken(long userId, long validityPeriod){
if(validityPeriod>MAX_TOKEN_TIME){
log.warn("A token is requested for user {} for a period exceed the maximum value.", userId);
validityPeriod = MAX_TOKEN_TIME;
}
if(validityPeriod<0){
throw new RuntimeException("Illegal validity period");
}
String token;
while(true){
token = TokenUtil.generateToken();
if(!tokenStore.containsKey(token)){
break;
}
}
long currentTime = System.currentTimeMillis();
TokenStoreEntry tse = new TokenStoreEntry();
tse.generated = new Date(currentTime);
tse.expiry = new Date(currentTime + validityPeriod);
tse.userId = userId;
tokenStore.put(token, tse);
return token;
}
public boolean isTokenValid(String token){
TokenStoreEntry tokenStoreEntry = tokenStore.get(token);
if(tokenStoreEntry==null){
return false;
}
return new Date().before(tokenStoreEntry.expiry);
}
public long getUserIdForToken(String token){
TokenStoreEntry tokenStoreEntry = tokenStore.get(token);
if(tokenStoreEntry==null){
return -1;
}
if(new Date().after(tokenStoreEntry.expiry)){
return -1;
}
return tokenStoreEntry.userId;
}
public Date getTokenExpiry(String token){
TokenStoreEntry tokenStoreEntry = tokenStore.get(token);
if(tokenStoreEntry==null){
return null;
}
if(new Date().after(tokenStoreEntry.expiry)){
return null;
}
return tokenStoreEntry.expiry;
}
public void clearExpiredEntry(){
Date currentTime = new Date();
ArrayList<String> expiredTokens = new ArrayList<>();
for(String token:tokenStore.keySet()){
if(tokenStore.get(token).expiry.before(currentTime)){
expiredTokens.add(token);
}
}
for(String token:expiredTokens){
tokenStore.remove(token);
}
}
@Override
public void clearCache() {
tokenStore.clear();
}
public int getTokenCount(){
return tokenStore.size();
}
public boolean extendTokenValidityPeriod(String token, long newValidityPeriod){
TokenStoreEntry tokenStoreEntry = tokenStore.get(token);
if(tokenStoreEntry==null){
return false;
}
if(new Date().after(tokenStoreEntry.expiry)){
return false;
}
if(newValidityPeriod<0){
throw new RuntimeException("Illegal validity period");
}
if(newValidityPeriod > MAX_TOKEN_TIME){
newValidityPeriod = MAX_TOKEN_TIME;
}
Date newExpiry = new Date(System.currentTimeMillis() + newValidityPeriod);
if(newExpiry.after(tokenStoreEntry.expiry)){
tokenStoreEntry.expiry = newExpiry;
tokenStore.put(token, tokenStoreEntry);
return true;
}
return false;
}
class TokenStoreEntry{
Date generated;
Date expiry;
long userId;
}
}

20
src/main/java/org/leolo/nrapi/web/TokenUtil.java

@ -0,0 +1,20 @@
package org.leolo.nrapi.web;
import java.util.Random;
public class TokenUtil {
public static final String TOKEN_CHARACTERS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
public static final int TOKEN_LENGTH = 32;
private static Random random = new Random();
public static String generateToken(){
StringBuilder sb = new StringBuilder();
for(int i=0;i<TOKEN_LENGTH;i++){
sb.append(TOKEN_CHARACTERS.charAt(random.nextInt(TOKEN_CHARACTERS.length())));
}
return sb.toString();
}
}

41
src/main/resources/static/docs/backup_file.html

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail API</title>
<link rel="stylesheet" href="main.css">
<script src="script.js"></script>
</head>
<body>
<h1>List the backup file available</h1>
This end point allow to search for backup file.
<p>
If only start date or end date is provided, the search will be covers
90 days. If either start date nor end date is given, the search will cover the last 90 days. If both date are given,
the search will be covering the specified period. An error will be returned if the specified period is more than 90
days.
</p>
<h2>Syntax</h2>
<div class="syntax">/backup/list?[startDate={startDate}][&amp;endDate={endDate}]</div>
<h3>Parameters</h3>
<table class="param">
<tr>
<th>startDate</th>
<td>
<span style="font-weight: bold">Optional.</span> Start date of the search in <code>YYYY-MM-DD</code> format.
</td>
</tr>
<tr>
<th>endDate</th>
<td>
<span style="font-weight: bold">Optional.</span> End date of the search in <code>YYYY-MM-DD</code> format.
</td>
</tr>
</table>
<h2>Return Value</h2>
BackupEntry[]
<h2>Type References</h2>
<iframe class="retv" src="retv-BackupEntry.html" id="retv-BackupEntry" onload="resize('retv-BackupEntry')"></iframe>
</body>
</html>

25
src/main/resources/static/docs/base.html

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail API</title>
<link rel="stylesheet" href="main.css">
<script src="script.js"></script>
</head>
<body>
<h1>Search train schedule by train ID</h1>
<h2>Syntax</h2>
<div class="syntax">/schedule/{id}[/{date}]</div>
<h3>Parameters</h3>
<table class="param">
<tr>
<th>id</th>
<td>Train ID in the scheduling system</td>
</tr>
</table>
<h2>Return Value</h2>
TrainSchedule
<h2>Type References</h2>
<iframe class="retv" src="retv-TrainSchedule.html" id="retv-TrainSchedule" onload="resize('retv-TrainSchedule')"></iframe>
</body>
</html>

11
src/main/resources/static/docs/index.html

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail API</title>
</head>
<frameset cols="20%, *" frameborder="0">
<frame name="nav" src="nav.html" noresize>
<frame name="main" src="overview.html" noresize>
</frameset>
</html>

86
src/main/resources/static/docs/location_search.html

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail API</title>
<link rel="stylesheet" href="main.css">
<script src="script.js"></script>
</head>
<body>
<h1>Search location by name</h1>
<h2>Syntax</h2>
<div class="syntax">/location/search?term={term}[&maxCount={maxCount}]</div>
<h3>Parameters</h3>
<table class="param">
<tr>
<th>term</th>
<td>
The key to be searched. The following operators may be used in the search term
<table>
<tr>
<th style="width: 15%">Operator</th>
<th style="width: 85%">Description</th>
</tr>
<tr>
<td>+</td>
<td>The word is mandatory in all rows returned.</td>
</tr>
<tr>
<td>-</td>
<td>The word cannot appear in any row returned.</td>
</tr>
<tr>
<td>&lt;</td>
<td>
The word that follows has a lower relevance than other words, although rows containing
it will still match
</td>
</tr>
<tr>
<td>&gt;</td>
<td>The word that follows has a higher relevance than other words.</td>
</tr>
<tr>
<td>()</td>
<td>Used to group words into subexpressions.</td>
</tr>
<tr>
<td>~</td>
<td>
The word following contributes negatively to the relevance of the row (which is different to
the '-' operator, which specifically excludes the word, or the '&lt;' operator, which still
causes the word to contribute positively to the relevance of the row.
</td>
</tr>
<tr>
<td>*</td>
<td>The wildcard, indicating zero or more characters. It can only appear at the end of a word.</td>
</tr>
<tr>
<td>"</td>
<td>
Anything enclosed in the double quotes is taken as a whole (so you can match phrases,
for example).
</td>
</tr>
</table>
</td>
</tr>
<tr>
<th>maxCount</th>
<td>
<span style="font-weight: bold">Optional</span>
<p>The maximum number of entries to be returned. Default value 10.</p>
<p>
If the search result has more than the specified number of entries, then the entries that matches the
search key most will be returned.
</p>
</td>
</tr>
</table>
<h2>Return Value</h2>
LocationSearchResult
<h2>Type References</h2>
<iframe class="retv" src="retv-LocationSearchResult.html" id="retv-TrainSchedule" onload="resize('retv-TrainSchedule')"></iframe>
</body>
</html>

79
src/main/resources/static/docs/main.css

@ -0,0 +1,79 @@
div.syntax{
display: block;
margin-left: 50px;
padding: 20px;
border-color: grey;
border: 2px;
border-style: dashed;
background-color: lightgray;
font-family: monospace;
}
div.warn{
display: block;
margin-left: 50px;
padding: 20px;
border-color: yellow;
border: 2px;
border-style: dashed;
background-color: lightyellow;
font-family: monospace;
}
table.param{
}
table.param tr th{
font-family: monospace;
width: 15%;
}
iframe.retv{
width: 100%;
border: 0;
height: max-content;
}
table.retv{
border-collapse: collapse;
width : 100%;
}
table.param{
border-collapse: collapse;
width : 100%;
}
table.retv tr td.name{
font-family: monospace;
}
table.retv tr{
border-bottom-width: 1px;
border-bottom-color: black;
border-bottom-style: solid;
}
table.retv tr:hover{
background-color: rgb(240, 240, 240);
}
table.param tr{
border-bottom-width: 1px;
border-bottom-color: black;
border-bottom-style: solid;
border-top-width: 1px;
border-top-color: black;
border-top-style: solid;
}
table.param tr th{
border-right-width: 1px;
border-right-color: black;
border-right-style: solid;
}
table.param table{
border-collapse: collapse;
}
table.param table td,table.param table th{
border-width: 1px;
border-color: black;
border-style: solid;
}

26
src/main/resources/static/docs/nav.html

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<base target="main">
</head>
<body>
<a href="overview.html">Overview</a>
<h3>Schedule</h3>
<ul>
<li><a href="schedule.html">Search schedule by train ID</a></li>
<li><a href="schedule_base.html">Search base schedule by train ID</a></li>
<li><a href="schedule_all.html">Search all schedule by train ID</a></li>
<li><a href="schedule_search.html">Search schedule by location</a></li>
</ul>
<h3>Location</h3>
<ul>
<li><a href="location_search.html">Search location by name</a></li>
</ul>
<h3>Backup file</h3>
<ul>
<li><a href="backup_file.html">List the backup files</a></li>
</ul>
</body>
</html>

26
src/main/resources/static/docs/overview.html

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="main.css">
<script src="script.js"></script>
</head>
<body>
<p>This API allows the user to query the train schedule in Great Britian</p>
<h2>Authentication</h2>
The user can be authenticated by the IP address or <code>device_id</code> in each request made by the user.
<h2>Common Parameter</h2>
<table class="param">
<tr>
<th>device_id</th>
<td>The device ID used to identify the user</td>
</tr>
</table>
<h2>Optional Version Prefix</h2>
For all of the API call, user can append a prefix <code>/v0</code> at the front of the API call. This will ensure the
version 0 of the API will be used. When then version is missing, the default version of the API will be used. Currently,
the default version of the API is version 0.
</body>
</html>

43
src/main/resources/static/docs/retv-BackupEntry.html

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="main.css">
</head>
<body style="padding: 0">
<h3>BackupEntry</h3>
<table class="retv">
<tr>
<th style="width: 25%">Name</th>
<th style="width: 15%">Type</th>
<th style="width: 60%">Description</th>
</tr>
<tr>
<td class="name">key</td>
<td>String</td>
<td>The file name of the file</td>
</tr>
<tr>
<td class="name">url</td>
<td>String</td>
<td>
The URL to download the file. You <span style="font-weight: bold">MAY</span> be redirected to an external site
to download the actual file and the redirected URL <span style="font-weight: bold">MAY</span> may only valid for
a limited of time. However, the URL listed here <span style="font-weight: bold">MUST</span> valid for a
indefinite period of time.
</td>
</tr>
<tr>
<td class="name">createdDate</td>
<td>Date</td>
<td>The date and time which the backup file is created.</td>
</tr>
<tr>
<td class="name">fileSize</td>
<td>int</td>
<td>The expected size of the backup file</td>
</tr>
</table>
</body>
</html>

56
src/main/resources/static/docs/retv-LocationSearchResult.html

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="main.css">
</head>
<body style="padding: 0">
<h3>LocationSearchResult</h3>
<table class="retv">
<tr>
<th style="width: 25%">Name</th>
<th style="width: 15%">Type</th>
<th style="width: 60%">Description</th>
</tr>
<tr>
<td class="name">tiplocCode</td>
<td>String</td>
<td>Timing Point Location. This code is used in train schedule.</td>
</tr>
<tr>
<td class="name">nalco</td>
<td>String</td>
<td>National Location Code. A 6-digits code generally used for retail purpose.</td>
</tr>
<tr>
<td class="name">stanox</td>
<td>String</td>
<td>
Station Number, can refer to non-station locations such as sidings and junctions. STANOX codes are grouped by
geographical area - the first two digits specify the area in which the location exists.
</td>
</tr>
<tr>
<td class="name">crsCode</td>
<td>String</td>
<td>A 3-character code used for stations.</td>
</tr>
<tr>
<td class="name">displayName</td>
<td>String</td>
<td>Name of the station/timing point</td>
</tr>
<tr>
<td class="name">matchScore</td>
<td>int</td>
<td>How good are the entries match the given search criteria.</td>
</tr>
<tr>
<td class="name">resultGroup</td>
<td>String</td>
<td>Grouping of the result. The entries with same value comes from same group.</td>
</tr>
</table>
</body>
</html>

83
src/main/resources/static/docs/retv-ScheduleSummary.html

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="main.css">
</head>
<body style="padding: 0">
<h3>ScheduleSummary</h3>
<table class="retv">
<tr>
<th style="width: 25%">Name</th>
<th style="width: 15%">Type</th>
<th style="width: 60%">Description</th>
</tr>
<tr>
<td class="name">headCode</td>
<td>String</td>
<td>Headcode using to identify the train, also known as signal ID</td>
</tr>
<tr>
<td class="name">originName</td>
<td>String</td>
<td>Name of the train origin</td>
</tr>
<tr>
<td class="name">destinationName</td>
<td>String</td>
<td>Name of the train destination</td>
</tr>
<tr>
<td class="name">originTime</td>
<td>Time</td>
<td>Departure time at origin</td>
</tr>
<tr>
<td class="name">destinationTime</td>
<td>Time</td>
<td>Arrival time at destination</td>
</tr>
<tr>
<td class="name">platform</td>
<td>String</td>
<td>The platform this train calls at, or passing though</td>
</tr>
<tr>
<td class="name">arrivalTime</td>
<td>Time</td>
<td>The arrival time of this train according to WTT</td>
</tr>
<tr>
<td class="name">departureTime</td>
<td>Time</td>
<td>The departure time of this train according to WTT</td>
</tr>
<tr>
<td class="name">passingTime</td>
<td>Time</td>
<td>The passing time of this train according to WTT</td>
</tr>
<tr>
<td class="name">detailUrl</td>
<td>URL</td>
<td>The URL to the detailed information of this train</td>
</tr>
<tr>
<td class="name">tocCode</td>
<td>String</td>
<td>The ATOC code of the operator of this train</td>
</tr>
<tr>
<td class="name">tocName</td>
<td>String</td>
<td>The name of the operator of this train</td>
</tr>
<tr>
<td class="name">cancelled</td>
<td>boolean</td>
<td>Is the train cancelled</td>
</tr>
</table>
</body>
</html>

35
src/main/resources/static/docs/retv-TrainAssociation.html

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="main.css">
</head>
<body style="padding: 0">
<h3>Train Association</h3>
<table class="retv">
<tr>
<th style="width: 25%">Name</th>
<th style="width: 15%">Type</th>
<th style="width: 60%">Description</th>
</tr>
<tr>
<td class="name">associatedTrainId</td>
<td>String</td>
<td>The train ID of the train associated with this train</td>
</tr>
<tr>
<td class="name">associationType</td>
<td>String</td>
<td>
Type of the association
<table>
<tr><th>JJ</th><td>Join</td></tr>
<tr><th>VV</th><td>Divide</td></tr>
<tr><th>NP</th><td>Next</td></tr>
</table>
</td>
</tr>
</table>
</body>
</html>

193
src/main/resources/static/docs/retv-TrainSchedule.html

@ -0,0 +1,193 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="main.css">
</head>
<body style="padding: 0">
<h3>TrainSchedule</h3>
<table class="retv">
<tr>
<th style="width: 25%">Name</th>
<th style="width: 15%">Type</th>
<th style="width: 60%">Description</th>
</tr>
<tr>
<td class="name">trainId</td>
<td>String</td>
<td>The train ID in the scheduling system</td>
</tr>
<tr>
<td class="name">scheduleType</td>
<td>String</td>
<td>
The schedule type of the schedule, valid values are
<table>
<tr>
<th>WTT</th>
<td>Working Timetable, this is usually the long term planning.</td>
</tr>
<tr>
<th>STP</th>
<td>Short term planning schedules that does not include in the working timetable</td>
</tr>
<tr>
<th>OVL</th>
<td>A modification of a working timetable that only applies on specified dates only</td>
</tr>
<tr>
<th>CAN</th>
<td>The train does not run on the specified date</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="name">startDate</td>
<td>Date</td>
<td>Start date for this schedule in <code>YYYY-MM-DD</code> format</td>
</tr>
<tr>
<td class="name">endDate</td>
<td>Date</td>
<td>End date for this schedule in <code>YYYY-MM-DD</code> format</td>
</tr>
<tr>
<td class="name">daysRun</td>
<td>String</td>
<td>
Indicate on which day will this train runs.
<table>
<tr><th>M</th><td>Monday</td></tr>
<tr><th>Tu</th><td>Tuesday</td></tr>
<tr><th>W</th><td>Wednesday</td></tr>
<tr><th>Th</th><td>Thursday</td></tr>
<tr><th>F</th><td>Friday</td></tr>
<tr><th>S</th><td>Saturday</td></tr>
<tr><th>Su</th><td>Sunday</td></tr>
<tr><th>X</th><td>Except</td></tr>
<tr><th>O</th><td>Only</td></tr>
</table>
<span style="font-style: italic">e.g.</span> <code>SSuX</code> means except Saturday and Sunday,
<code>FO</code> means Friday only.
</td>
</tr>
<tr>
<td class="name">bankHolidayRun</td>
<td>String</td>
<td>
Runs on bank holiday.
<table>
<tr><th><code>null</code></th><td>Runs on bank holiday</td></tr>
<tr><th>X</th><td>Does <span style="font-weight: bold">NOT</span> runs on bank holiday</td></tr>
<tr><th>G</th><td>Does <span style="font-weight: bold">NOT</span> runs on Glasgow bank holiday</td></tr>
</table>
</td>
</tr>
<tr>
<td class="name">endDate</td>
<td>Date</td>
<td>End date for this schedule in <code>YYYY-MM-DD</code> format</td>
</tr>
<tr>
<td class="name">status</td>
<td>String</td>
<td>
Train status code
<table>
<tr><th>Permanent</th><th>STP</th><th>Description</th></tr>
<tr><td>B</td><td>5</td><td>Bus</td></tr>
<tr><td>F</td><td>1</td><td>Freight</td></tr>
<tr><td>P</td><td>1</td><td>Passenger and parcel</td></tr>
<tr><td>S</td><td>4</td><td>Ship</td></tr>
<tr><td>T</td><td>3</td><td>Trip</td></tr>
</table>
</td>
</tr>
<tr>
<td class="name">category</td>
<td>String</td>
<td>Train category</td>
</tr>
<tr>
<td class="name">headcode</td>
<td>String</td>
<td>Headcode using to identify the train, also known as signal ID</td>
</tr>
<tr>
<td class="name">reservationSystemHeadcode</td>
<td>String</td>
<td>Headcode for reservation system. The train operator code is included if there are reservation system headcode.</td>
</tr>
<tr>
<td class="name">powerType</td>
<td>String</td>
<td>Power type of the train</td>
</tr>
<tr>
<td class="name">timingLoad</td>
<td>String</td>
<td>Timing load of the train</td>
</tr>
<tr>
<td class="name">speed</td>
<td>int</td>
<td>Planned speed of the train in miles per hour</td>
</tr>
<tr>
<td class="name">operatingCharacters</td>
<td>String</td>
<td>Operating characteristics</td>
</tr>
<tr>
<td class="name">classAvailable</td>
<td>String</td>
<td>
Which class is available on this train
<table>
<tr><th><code>null</code></th><td>Both first and standard class is available</td></tr>
<tr><th>B</th><td>Both first and standard class is available</td></tr>
<tr><th>S</th><td>Only standard class is available</td></tr>
</table>
</td>
</tr>
<tr>
<td class="name">reservation</td>
<td>String</td>
<td>
Reservation requirement for this train
<table>
<tr><th>A</th><td>Reservations compulsory</td></tr>
<tr><th>E</th><td>Reservations for bicycles essential</td></tr>
<tr><th>R</th><td>Reservations recommended</td></tr>
<tr><th>S</th><td>Reservations possible from any station</td></tr>
</table>
Note: Some train's reservation are counted space only, and no actual seat are assigned.
</td>
</tr>
<tr>
<td class="name">catering</td>
<td>String</td>
<td>
Catering availability on this train
</td>
</tr>
<tr>
<td class="name">trainOperatorCode</td>
<td>String</td>
<td>ATOC code of the train operator operates this train</td>
</tr>
<tr>
<td class="name">trainOperatorName</td>
<td>String</td>
<td>Name of the train operator operates this train</td>
</tr>
<tr>
<td class="name">scheduleEntries</td>
<td>TrainScheduleDetail[]</td>
<td>Detail entries of the train schedule</td>
</tr>
</table>
</body>
</html>

98
src/main/resources/static/docs/retv-TrainScheduleDetail.html

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="main.css">
</head>
<body style="padding: 0">
<h3>TrainScheduleDetail</h3>
<table class="retv">
<tr>
<th style="width: 25%">Name</th>
<th style="width: 15%">Type</th>
<th style="width: 60%">Description</th>
</tr>
<tr>
<td class="name">tiplocCode</td>
<td>String</td>
<td>TIPLOC code of the location of this entry</td>
</tr>
<tr>
<td class="name">tiplocName</td>
<td>String</td>
<td>Name of the location of this entry</td>
</tr>
<tr>
<td class="name">instance</td>
<td>int</td>
<td>Instance number of this TIPLOC entry. <code>null</code> means the TIPLOC code only appears once in the schedule</td>
</tr>
<tr>
<td class="name">wttArrival</td>
<td>Time</td>
<td>Arrival time of this train according to the working time table</td>
</tr>
<tr>
<td class="name">wttDeparture</td>
<td>Time</td>
<td>Departure time of this train according to the working time table</td>
</tr>
<tr>
<td class="name">wttPass</td>
<td>Time</td>
<td>Passing time of this train according to the working time table</td>
</tr>
<tr>
<td class="name">gbttArrival</td>
<td>Time</td>
<td>Arrival time of this train according to the public time table</td>
</tr>
<tr>
<td class="name">gbttDeparture</td>
<td>Time</td>
<td>Departure time of this train according to the public time table</td>
</tr>
<tr>
<td class="name">platform</td>
<td>String</td>
<td>Assigned platform number for this train</td>
</tr>
<tr>
<td class="name">line</td>
<td>String</td>
<td>Departure line</td>
</tr>
<tr>
<td class="name">path</td>
<td>String</td>
<td>Arrival path</td>
</tr>
<tr>
<td class="name">engineeringAllowance</td>
<td>Time</td>
<td>Engineering allowance </td>
</tr>
<tr>
<td class="name">pathingAllowance</td>
<td>Time</td>
<td>Pathing allowance </td>
</tr>
<tr>
<td class="name">performanceAllowance</td>
<td>Time</td>
<td>Performance allowance </td>
</tr>
<tr>
<td class="name">crsCode</td>
<td>String</td>
<td>CRS code for the location</td>
</tr>
<tr>
<td class="name">association</td>
<td>TrainAssociation[]</td>
<td>List of association happens at this location</td>
</tr>
</table>
</body>
</html>

23
src/main/resources/static/docs/retv-base.html

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="main.css">
</head>
<body style="padding: 0">
<h3></h3>
<table class="retv">
<tr>
<th style="width: 25%">Name</th>
<th style="width: 15%">Type</th>
<th style="width: 60%">Description</th>
</tr>
<tr>
<td class="name"></td>
<td></td>
<td></td>
</tr>
</table>
</body>
</html>

36
src/main/resources/static/docs/schedule.html

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail API</title>
<link rel="stylesheet" href="main.css">
<script src="script.js"></script>
</head>
<body>
<h1>Search train schedule by train ID</h1>
<h2>Syntax</h2>
<div class="syntax">/schedule/{id}[/{date}]</div>
<h3>Parameters</h3>
<table class="param">
<tr>
<th>id</th>
<td>Train ID in the scheduling system</td>
</tr>
<tr>
<th>date</th>
<td>
<span style="font-weight: bold">Optional</span>
The date of the schedule to be searched, in <code>YYYY-MM-DD</code>
format.<br>
<span style="font-weight: bold">Default:</span> today
</td>
</tr>
</table>
<h2>Return Value</h2>
TrainSchedule
<h2>Type References</h2>
<iframe class="retv" src="retv-TrainSchedule.html" id="retv-TrainSchedule" onload="resize('retv-TrainSchedule')"></iframe>
<iframe class="retv" src="retv-TrainScheduleDetail.html" id="retv-TrainScheduleDetail" onload="resize('retv-TrainScheduleDetail')"></iframe>
<iframe class="retv" src="retv-TrainAssociation.html" id="retv-TrainAssociation" onload="resize('retv-TrainAssociation')"></iframe>
</body>
</html>

32
src/main/resources/static/docs/schedule_all.html

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail API</title>
<link rel="stylesheet" href="main.css">
<script src="script.js"></script>
</head>
<body>
<h1>Search base train schedule by train ID</h1>
<p>Find all schedules for the specified train ID. No train association information are included in this API call. If train association information is required,
<a href="schedule.html">Search schedule by train ID</a> should be used.</p>
<p>A working timetable(WTT), or short term planning (STP) record may be being overridden by a cancellation or overlay
records.</p>
<h2>Syntax</h2>
<div class="syntax">/schedule/{id}/all</div>
<h3>Parameters</h3>
<table class="param">
<tr>
<th>id</th>
<td>Train ID in the scheduling system</td>
</tr>
</table>
<h2>Return Value</h2>
TrainSchedule[]
<h2>Type References</h2>
<iframe class="retv" src="retv-TrainSchedule.html" id="retv-TrainSchedule" onload="resize('retv-TrainSchedule')"></iframe>
<iframe class="retv" src="retv-TrainScheduleDetail.html" id="retv-TrainScheduleDetail" onload="resize('retv-TrainScheduleDetail')"></iframe>
<iframe class="retv" src="retv-TrainAssociation.html" id="retv-TrainAssociation" onload="resize('retv-TrainAssociation')"></iframe>
</body>
</html>

39
src/main/resources/static/docs/schedule_base.html

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail API</title>
<link rel="stylesheet" href="main.css">
<script src="script.js"></script>
</head>
<body>
<h1>Search base train schedule by train ID</h1>
Find the base schedule for the specified train. Base schedule may be overridden by an overlay record, or being cancelled on
specified dates. No train association information are included in this API call. If train association information is required,
<a href="schedule.html">Search schedule by train ID</a> should be used.
<h2>Syntax</h2>
<div class="syntax">/schedule/{id}[/{date}]/wtt</div>
<h3>Parameters</h3>
<table class="param">
<tr>
<th>id</th>
<td>Train ID in the scheduling system</td>
</tr>
<tr>
<th>date</th>
<td>
<span style="font-weight: bold">Optional</span>
The date of the schedule to be searched, in <code>YYYY-MM-DD</code>
format.<br>
<span style="font-weight: bold">Default:</span> today
</td>
</tr>
</table>
<h2>Return Value</h2>
TrainSchedule
<h2>Type References</h2>
<iframe class="retv" src="retv-TrainSchedule.html" id="retv-TrainSchedule" onload="resize('retv-TrainSchedule')"></iframe>
<iframe class="retv" src="retv-TrainScheduleDetail.html" id="retv-TrainScheduleDetail" onload="resize('retv-TrainScheduleDetail')"></iframe>
<iframe class="retv" src="retv-TrainAssociation.html" id="retv-TrainAssociation" onload="resize('retv-TrainAssociation')"></iframe>
</body>
</html>

75
src/main/resources/static/docs/schedule_search.html

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail API</title>
<link rel="stylesheet" href="main.css">
<script src="script.js"></script>
</head>
<body>
<h1>Search train schedule by location</h1>
<h2>Syntax</h2>
<div class="syntax">/search/{location}[/{date}[/{time}]]?[range={range}][&amp;strict={strict}][&amp;stopOnly={stopOnly}]</div>
<h3>Parameters</h3>
<table class="param">
<tr>
<th>location</th>
<td>The location going to be searched. It can be either a TIPLOC code, or a CRS code.</td>
</tr>
<tr>
<th>date</th>
<td>
<span style="font-weight: bold">Optional, but required if time is specified.</span>
The date going to be searched, in <code>YYYY-MM-DD</code> format. Default to the current
date.
</td>
</tr>
<tr>
<th>time</th>
<td>
<span style="font-weight: bold">Optional.</span>
The time going to be searched, in <code>HH:MM</code> format. Default to the current
time.
</td>
</tr>
<tr>
<th>range</th>
<td>
<span style="font-weight: bold">Optional.</span>
Number of minutes on either side of the specified time to be included in the search. Default to
60 minutes. Valid values are between 1 and 120, default value will be included when an invalid
value is provided.
</td>
</tr>
<tr>
<th>strict</th>
<td>
<p>
<span style="font-weight: bold">Optional.</span>
Search in strict mode. Default value is <code>true</code> or <code>false</code>. Default value is
<code>false</code>
</p>
<p>
In strict mode, only the entry that matches the provided code will be included.
</p>
<p>
In relaxed mode (default), the entries in same group (having same STANOX code or NALCO code) will
be also included in the search.
</p>
</td>
</tr>
<tr>
<th>stopOnly</th>
<td>
<span style="font-weight: bold">Optional.</span>
Show only stopping trains. Valid valued are <code>true</code> or <code>false</code>. Default value is
<code>false</code>.
</td>
</tr>
</table>
<h2>Return Value</h2>
ScheduleSummary[]
<h2>Type References</h2>
<iframe class="retv" src="retv-ScheduleSummary.html" id="retv-ScheduleSummary" onload="resize('retv-ScheduleSummary')"></iframe>
</body>
</html>

4
src/main/resources/static/docs/script.js

@ -0,0 +1,4 @@
function resize(id){
var iframe = document.getElementById(id);
iframe.style.height = (iframe.contentWindow.document.body.scrollHeight + 30 )+'px';
}

12
src/main/resources/static/web/index.html

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GB Rail Data</title>
</head>
<body>
<div>
<a href="/docs/index.html">Documents</a>
</div>
</body>
</html>

23
src/test/java/org/leolo/nrapi/NrapiApplicationTests.java

@ -0,0 +1,23 @@
package org.leolo.nrapi;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.leolo.nrapi.web.TokenStore;
import org.leolo.nrapi.web.TokenUtil;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
class NrapiApplicationTests {
@Test
void contextLoads() {
}
}

104
src/test/java/org/leolo/nrapi/TokenGenerateTest.java

@ -0,0 +1,104 @@
package org.leolo.nrapi;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.leolo.nrapi.web.TokenStore;
import org.leolo.nrapi.web.TokenUtil;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class TokenGenerateTest {
@BeforeEach void clear(){
TokenStore.getInstance().clearCache();
}
@Test
void checkTokenExpiry() throws InterruptedException {
String token1 = TokenStore.getInstance().generateToken(1, 1000);
String token2 = TokenStore.getInstance().generateToken(1, 5000);
Thread.sleep(1250);
assertFalse(TokenStore.getInstance().isTokenValid(token1));
assertTrue(TokenStore.getInstance().isTokenValid(token2));
}
@Test
void checkTokenRemoval() throws InterruptedException {
String token1 = TokenStore.getInstance().generateToken(1, 1000);
String token2 = TokenStore.getInstance().generateToken(1, 5000);
assertEquals(2, TokenStore.getInstance().getTokenCount());
Thread.sleep(1250);
assertEquals(2, TokenStore.getInstance().getTokenCount());
TokenStore.getInstance().clearExpiredEntry();
assertEquals(1, TokenStore.getInstance().getTokenCount());
}
@Test void checkNonexistentToken(){
String token = TokenUtil.generateToken();
assertFalse(TokenStore.getInstance().isTokenValid(token));
}
@Test void findToken(){
final int ROUND = 10000;
String [] tokens = new String[ROUND];
for(int i=0;i<ROUND;i++){
tokens[i] = TokenStore.getInstance().generateToken(i);
}
for(int i=0;i<ROUND;i++){
assertTrue(TokenStore.getInstance().isTokenValid(tokens[i]));
}
}
@Test void tokenLength(){
assertEquals(TokenUtil.generateToken().length(), TokenUtil.TOKEN_LENGTH);
}
@Test void tokenNotEquals(){
assertNotEquals(TokenUtil.generateToken(), TokenUtil.generateToken());
}
@Test void multiTokenNotEquals(){
final int ROUND = 10000;
String [] tokens = new String[ROUND];
for(int i=0;i<ROUND;i++){
String token = TokenUtil.generateToken();
tokens[i] = token;
for(int j=0;j<i;j++){
assertNotEquals(token, tokens[j]);
}
if(i%100==0){
System.out.println("Token "+i+" is "+tokens[i]);
}
}
}
@Test void checkExpiry(){
final long TOLERANT = 100;
final long VALID_PERIOD = 2000;
TokenStore.getInstance();
final long START_TIME = System.currentTimeMillis();
String token = TokenStore.getInstance().generateToken(1, VALID_PERIOD);
long diff = TokenStore.getInstance().getTokenExpiry(token).getTime() - START_TIME - VALID_PERIOD;
System.out.print("Actual diff : "+diff);
assertTrue(diff < TOLERANT);
}
@Test void extendNonExistentToken(){
assertFalse(TokenStore.getInstance().extendTokenValidityPeriod(TokenUtil.generateToken(), 1000));
}
@Test void extendToken(){
String token1 = TokenStore.getInstance().generateToken(1, 3600000);
String token2= TokenStore.getInstance().generateToken(1, 3600000);
assertFalse(TokenStore.getInstance().extendTokenValidityPeriod(token1, 1800000));
assertTrue(TokenStore.getInstance().extendTokenValidityPeriod(token2, 7200000));
}
@Test void invalidValidity(){
assertThrows(RuntimeException.class,
()->{
TokenStore.getInstance().generateToken(1, -100);
}
);
}
}
Loading…
Cancel
Save