commit 3d80c53180b869f8a461961179bb89add6a0dd49 Author: LO Kam Tao Leo Date: Sat Jul 23 06:33:23 2022 +0100 Reimport project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35d9b7c --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -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 "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -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% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d874e04 --- /dev/null +++ b/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.0 + + + org.leolo + NRAPI + 0.0.1-SNAPSHOT + NRAPI + NRAPI + + 17 + + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-web + + + + org.mariadb.jdbc + mariadb-java-client + + + org.springframework.boot + spring-boot-starter-test + test + + + org.jetbrains + annotations + RELEASE + compile + + + + com.amazonaws + aws-java-sdk-s3 + 1.12.239 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/org/leolo/nrapi/AuthDataCache.java b/src/main/java/org/leolo/nrapi/AuthDataCache.java new file mode 100644 index 0000000..4539b5c --- /dev/null +++ b/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 ips = new HashMap<>(); + private HashMap 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; + } +} diff --git a/src/main/java/org/leolo/nrapi/AuthFilter.java b/src/main/java/org/leolo/nrapi/AuthFilter.java new file mode 100644 index 0000000..a9ce4d1 --- /dev/null +++ b/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(); + } +} diff --git a/src/main/java/org/leolo/nrapi/Constants.java b/src/main/java/org/leolo/nrapi/Constants.java new file mode 100644 index 0000000..a496b70 --- /dev/null +++ b/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"; +} diff --git a/src/main/java/org/leolo/nrapi/NrapiApplication.java b/src/main/java/org/leolo/nrapi/NrapiApplication.java new file mode 100644 index 0000000..7131fb2 --- /dev/null +++ b/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"); + } +} diff --git a/src/main/java/org/leolo/nrapi/admin/AdminAPI.java b/src/main/java/org/leolo/nrapi/admin/AdminAPI.java new file mode 100644 index 0000000..969281e --- /dev/null +++ b/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"; + } + }; + } +} diff --git a/src/main/java/org/leolo/nrapi/db/SearchParameter.java b/src/main/java/org/leolo/nrapi/db/SearchParameter.java new file mode 100644 index 0000000..9cd7f0c --- /dev/null +++ b/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())); + } + }; + } +} diff --git a/src/main/java/org/leolo/nrapi/manager/CacheManager.java b/src/main/java/org/leolo/nrapi/manager/CacheManager.java new file mode 100644 index 0000000..3c5af5e --- /dev/null +++ b/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 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(); + } + } +} diff --git a/src/main/java/org/leolo/nrapi/manager/CacheProvider.java b/src/main/java/org/leolo/nrapi/manager/CacheProvider.java new file mode 100644 index 0000000..172de95 --- /dev/null +++ b/src/main/java/org/leolo/nrapi/manager/CacheProvider.java @@ -0,0 +1,7 @@ +package org.leolo.nrapi.manager; + +public interface CacheProvider { + + public void clearCache(); + +} diff --git a/src/main/java/org/leolo/nrapi/manager/DatabaseManager.java b/src/main/java/org/leolo/nrapi/manager/DatabaseManager.java new file mode 100644 index 0000000..785b125 --- /dev/null +++ b/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; + } +} diff --git a/src/main/java/org/leolo/nrapi/manager/PropertyManager.java b/src/main/java/org/leolo/nrapi/manager/PropertyManager.java new file mode 100644 index 0000000..34724e9 --- /dev/null +++ b/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; + } +} diff --git a/src/main/java/org/leolo/nrapi/util/HttpReqRespUtils.java b/src/main/java/org/leolo/nrapi/util/HttpReqRespUtils.java new file mode 100644 index 0000000..f2e575f --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/src/main/java/org/leolo/nrapi/util/MiscUtilAPI.java b/src/main/java/org/leolo/nrapi/util/MiscUtilAPI.java new file mode 100644 index 0000000..be6725d --- /dev/null +++ b/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 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 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 headers = request.getHeaderNames(); + while(headers.hasMoreElements()){ + String header = headers.nextElement(); + ArrayList values = new ArrayList<>(); + Enumeration 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 generateDeviceId(@RequestParam(name = "count", required = false, defaultValue = "1")int count){ + ArrayList 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 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; + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/api/LocationSearchAPI.java b/src/main/java/org/leolo/nrapi/v0/api/LocationSearchAPI.java new file mode 100644 index 0000000..bac056b --- /dev/null +++ b/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 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 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 retList = new ArrayList<>(); + for(int i=0;i4){ + //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 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; + } + +} diff --git a/src/main/java/org/leolo/nrapi/v0/api/ScheduleSearchAPI.java b/src/main/java/org/leolo/nrapi/v0/api/ScheduleSearchAPI.java new file mode 100644 index 0000000..e7bd561 --- /dev/null +++ b/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 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 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 result = new ArrayList<>(); + HashMap 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 result = new ArrayList<>(); + HashMap 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 result = new ArrayList<>(); + HashMap 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 processResult(ResultSet rs, HashMap status, Date date, HttpServletRequest request) throws SQLException{ + ArrayList 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(); + } + +} diff --git a/src/main/java/org/leolo/nrapi/v0/api/ScheduleSearchQueryBuilder.java b/src/main/java/org/leolo/nrapi/v0/api/ScheduleSearchQueryBuilder.java new file mode 100644 index 0000000..5ed5324 --- /dev/null +++ b/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 execute() throws SQLException { + if(searchLocation==null){ + throw new IllegalArgumentException("Location must be specified"); + } + ArrayList list = new ArrayList<>(); + ArrayList 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, 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; + } + } + } + +} diff --git a/src/main/java/org/leolo/nrapi/v0/api/TableGroup.java b/src/main/java/org/leolo/nrapi/v0/api/TableGroup.java new file mode 100644 index 0000000..8cffa23 --- /dev/null +++ b/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; + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/cache/TrainCategoryCache.java b/src/main/java/org/leolo/nrapi/v0/cache/TrainCategoryCache.java new file mode 100644 index 0000000..8cad018 --- /dev/null +++ b/src/main/java/org/leolo/nrapi/v0/cache/TrainCategoryCache.java @@ -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 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(); + } + +} diff --git a/src/main/java/org/leolo/nrapi/v0/cache/TrainOperatorCache.java b/src/main/java/org/leolo/nrapi/v0/cache/TrainOperatorCache.java new file mode 100644 index 0000000..0e1f997 --- /dev/null +++ b/src/main/java/org/leolo/nrapi/v0/cache/TrainOperatorCache.java @@ -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 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; + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/model/BackupEntry.java b/src/main/java/org/leolo/nrapi/v0/model/BackupEntry.java new file mode 100644 index 0000000..631afc6 --- /dev/null +++ b/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; + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/model/DisplayableError.java b/src/main/java/org/leolo/nrapi/v0/model/DisplayableError.java new file mode 100644 index 0000000..2cdfe7e --- /dev/null +++ b/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."; + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/model/LocationSearchResult.java b/src/main/java/org/leolo/nrapi/v0/model/LocationSearchResult.java new file mode 100644 index 0000000..9bdcbe7 --- /dev/null +++ b/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); + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/model/ReturnSet.java b/src/main/java/org/leolo/nrapi/v0/model/ReturnSet.java new file mode 100644 index 0000000..82255df --- /dev/null +++ b/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(); + } + +} diff --git a/src/main/java/org/leolo/nrapi/v0/model/ScheduleSummary.java b/src/main/java/org/leolo/nrapi/v0/model/ScheduleSummary.java new file mode 100644 index 0000000..fb084a8 --- /dev/null +++ b/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; + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/model/TrainAssociationInfo.java b/src/main/java/org/leolo/nrapi/v0/model/TrainAssociationInfo.java new file mode 100644 index 0000000..5effb3b --- /dev/null +++ b/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; + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/model/TrainSchedule.java b/src/main/java/org/leolo/nrapi/v0/model/TrainSchedule.java new file mode 100644 index 0000000..94fde8f --- /dev/null +++ b/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 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 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; + } +} diff --git a/src/main/java/org/leolo/nrapi/v0/model/TrainScheduleDetails.java b/src/main/java/org/leolo/nrapi/v0/model/TrainScheduleDetails.java new file mode 100644 index 0000000..7bf0cb8 --- /dev/null +++ b/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 association = new ArrayList<>(); + + public ArrayList 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; + } +} diff --git a/src/main/java/org/leolo/nrapi/web/LoginAPI.java b/src/main/java/org/leolo/nrapi/web/LoginAPI.java new file mode 100644 index 0000000..e33ef3e --- /dev/null +++ b/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";} + }; + } +} diff --git a/src/main/java/org/leolo/nrapi/web/TokenStore.java b/src/main/java/org/leolo/nrapi/web/TokenStore.java new file mode 100644 index 0000000..5404111 --- /dev/null +++ b/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 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 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; + } +} diff --git a/src/main/java/org/leolo/nrapi/web/TokenUtil.java b/src/main/java/org/leolo/nrapi/web/TokenUtil.java new file mode 100644 index 0000000..1e420d5 --- /dev/null +++ b/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 + + + + GB Rail API + + + + +

List the backup file available

+This end point allow to search for backup file. + +

+ 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. +

+

Syntax

+
/backup/list?[startDate={startDate}][&endDate={endDate}]
+

Parameters

+ + + + + + + + + +
startDate + Optional. Start date of the search in YYYY-MM-DD format. +
endDate + Optional. End date of the search in YYYY-MM-DD format. +
+

Return Value

+BackupEntry[] +

Type References

+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/base.html b/src/main/resources/static/docs/base.html new file mode 100644 index 0000000..baeaf83 --- /dev/null +++ b/src/main/resources/static/docs/base.html @@ -0,0 +1,25 @@ + + + + + GB Rail API + + + + +

Search train schedule by train ID

+

Syntax

+
/schedule/{id}[/{date}]
+

Parameters

+ + + + + +
idTrain ID in the scheduling system
+

Return Value

+TrainSchedule +

Type References

+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html new file mode 100644 index 0000000..765d713 --- /dev/null +++ b/src/main/resources/static/docs/index.html @@ -0,0 +1,11 @@ + + + + + GB Rail API + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/docs/location_search.html b/src/main/resources/static/docs/location_search.html new file mode 100644 index 0000000..2a5294a --- /dev/null +++ b/src/main/resources/static/docs/location_search.html @@ -0,0 +1,86 @@ + + + + + GB Rail API + + + + +

Search location by name

+

Syntax

+
/location/search?term={term}[&maxCount={maxCount}]
+

Parameters

+ + + + + + + + + +
term + The key to be searched. The following operators may be used in the search term + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperatorDescription
+The word is mandatory in all rows returned.
-The word cannot appear in any row returned.
< + The word that follows has a lower relevance than other words, although rows containing + it will still match +
>The word that follows has a higher relevance than other words.
()Used to group words into subexpressions.
~ + The word following contributes negatively to the relevance of the row (which is different to + the '-' operator, which specifically excludes the word, or the '<' operator, which still + causes the word to contribute positively to the relevance of the row. +
*The wildcard, indicating zero or more characters. It can only appear at the end of a word.
" + Anything enclosed in the double quotes is taken as a whole (so you can match phrases, + for example). +
+
maxCount + Optional +

The maximum number of entries to be returned. Default value 10.

+

+ If the search result has more than the specified number of entries, then the entries that matches the + search key most will be returned. +

+
+

Return Value

+LocationSearchResult +

Type References

+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/main.css b/src/main/resources/static/docs/main.css new file mode 100644 index 0000000..31a7924 --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/src/main/resources/static/docs/nav.html b/src/main/resources/static/docs/nav.html new file mode 100644 index 0000000..71db3f6 --- /dev/null +++ b/src/main/resources/static/docs/nav.html @@ -0,0 +1,26 @@ + + + + + Title + + + + Overview +

Schedule

+ +

Location

+ +

Backup file

+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/overview.html b/src/main/resources/static/docs/overview.html new file mode 100644 index 0000000..af956e7 --- /dev/null +++ b/src/main/resources/static/docs/overview.html @@ -0,0 +1,26 @@ + + + + + Title + + + + +

This API allows the user to query the train schedule in Great Britian

+ +

Authentication

+The user can be authenticated by the IP address or device_id in each request made by the user. +

Common Parameter

+ + + + + +
device_idThe device ID used to identify the user
+

Optional Version Prefix

+For all of the API call, user can append a prefix /v0 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. + + \ No newline at end of file diff --git a/src/main/resources/static/docs/retv-BackupEntry.html b/src/main/resources/static/docs/retv-BackupEntry.html new file mode 100644 index 0000000..a611de2 --- /dev/null +++ b/src/main/resources/static/docs/retv-BackupEntry.html @@ -0,0 +1,43 @@ + + + + + Title + + + +

BackupEntry

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
keyStringThe file name of the file
urlString + The URL to download the file. You MAY be redirected to an external site + to download the actual file and the redirected URL MAY may only valid for + a limited of time. However, the URL listed here MUST valid for a + indefinite period of time. +
createdDateDateThe date and time which the backup file is created.
fileSizeintThe expected size of the backup file
+ + \ No newline at end of file diff --git a/src/main/resources/static/docs/retv-LocationSearchResult.html b/src/main/resources/static/docs/retv-LocationSearchResult.html new file mode 100644 index 0000000..c527d58 --- /dev/null +++ b/src/main/resources/static/docs/retv-LocationSearchResult.html @@ -0,0 +1,56 @@ + + + + + Title + + + +

LocationSearchResult

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tiplocCodeStringTiming Point Location. This code is used in train schedule.
nalcoStringNational Location Code. A 6-digits code generally used for retail purpose.
stanoxString + 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. +
crsCodeStringA 3-character code used for stations.
displayNameStringName of the station/timing point
matchScoreintHow good are the entries match the given search criteria.
resultGroupStringGrouping of the result. The entries with same value comes from same group.
+ + \ No newline at end of file diff --git a/src/main/resources/static/docs/retv-ScheduleSummary.html b/src/main/resources/static/docs/retv-ScheduleSummary.html new file mode 100644 index 0000000..c405d95 --- /dev/null +++ b/src/main/resources/static/docs/retv-ScheduleSummary.html @@ -0,0 +1,83 @@ + + + + + Title + + + +

ScheduleSummary

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
headCodeStringHeadcode using to identify the train, also known as signal ID
originNameStringName of the train origin
destinationNameStringName of the train destination
originTimeTimeDeparture time at origin
destinationTimeTimeArrival time at destination
platformStringThe platform this train calls at, or passing though
arrivalTimeTimeThe arrival time of this train according to WTT
departureTimeTimeThe departure time of this train according to WTT
passingTimeTimeThe passing time of this train according to WTT
detailUrlURLThe URL to the detailed information of this train
tocCodeStringThe ATOC code of the operator of this train
tocNameStringThe name of the operator of this train
cancelledbooleanIs the train cancelled
+ + \ No newline at end of file diff --git a/src/main/resources/static/docs/retv-TrainAssociation.html b/src/main/resources/static/docs/retv-TrainAssociation.html new file mode 100644 index 0000000..cc18337 --- /dev/null +++ b/src/main/resources/static/docs/retv-TrainAssociation.html @@ -0,0 +1,35 @@ + + + + + Title + + + +

Train Association

+ + + + + + + + + + + + + + + + +
NameTypeDescription
associatedTrainIdStringThe train ID of the train associated with this train
associationTypeString + Type of the association + + + + +
JJJoin
VVDivide
NPNext
+
+ + \ No newline at end of file diff --git a/src/main/resources/static/docs/retv-TrainSchedule.html b/src/main/resources/static/docs/retv-TrainSchedule.html new file mode 100644 index 0000000..1f6f201 --- /dev/null +++ b/src/main/resources/static/docs/retv-TrainSchedule.html @@ -0,0 +1,193 @@ + + + + + Title + + + +

TrainSchedule

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
trainIdStringThe train ID in the scheduling system
scheduleTypeString + The schedule type of the schedule, valid values are + + + + + + + + + + + + + + + + + +
WTTWorking Timetable, this is usually the long term planning.
STPShort term planning schedules that does not include in the working timetable
OVLA modification of a working timetable that only applies on specified dates only
CANThe train does not run on the specified date
+
startDateDateStart date for this schedule in YYYY-MM-DD format
endDateDateEnd date for this schedule in YYYY-MM-DD format
daysRunString + Indicate on which day will this train runs. + + + + + + + + + + +
MMonday
TuTuesday
WWednesday
ThThursday
FFriday
SSaturday
SuSunday
XExcept
OOnly
+ e.g. SSuX means except Saturday and Sunday, + FO means Friday only. +
bankHolidayRunString + Runs on bank holiday. + + + + +
nullRuns on bank holiday
XDoes NOT runs on bank holiday
GDoes NOT runs on Glasgow bank holiday
+
endDateDateEnd date for this schedule in YYYY-MM-DD format
statusString + Train status code + + + + + + + +
PermanentSTPDescription
B5Bus
F1Freight
P1Passenger and parcel
S4Ship
T3Trip
+
categoryStringTrain category
headcodeStringHeadcode using to identify the train, also known as signal ID
reservationSystemHeadcodeStringHeadcode for reservation system. The train operator code is included if there are reservation system headcode.
powerTypeStringPower type of the train
timingLoadStringTiming load of the train
speedintPlanned speed of the train in miles per hour
operatingCharactersStringOperating characteristics
classAvailableString + Which class is available on this train + + + + +
nullBoth first and standard class is available
BBoth first and standard class is available
SOnly standard class is available
+
reservationString + Reservation requirement for this train + + + + + +
AReservations compulsory
EReservations for bicycles essential
RReservations recommended
SReservations possible from any station
+ Note: Some train's reservation are counted space only, and no actual seat are assigned. +
cateringString + Catering availability on this train +
trainOperatorCodeStringATOC code of the train operator operates this train
trainOperatorNameStringName of the train operator operates this train
scheduleEntriesTrainScheduleDetail[]Detail entries of the train schedule
+ + \ No newline at end of file diff --git a/src/main/resources/static/docs/retv-TrainScheduleDetail.html b/src/main/resources/static/docs/retv-TrainScheduleDetail.html new file mode 100644 index 0000000..39fcaf7 --- /dev/null +++ b/src/main/resources/static/docs/retv-TrainScheduleDetail.html @@ -0,0 +1,98 @@ + + + + + Title + + + +

TrainScheduleDetail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tiplocCodeStringTIPLOC code of the location of this entry
tiplocNameStringName of the location of this entry
instanceintInstance number of this TIPLOC entry. null means the TIPLOC code only appears once in the schedule
wttArrivalTimeArrival time of this train according to the working time table
wttDepartureTimeDeparture time of this train according to the working time table
wttPassTimePassing time of this train according to the working time table
gbttArrivalTimeArrival time of this train according to the public time table
gbttDepartureTimeDeparture time of this train according to the public time table
platformStringAssigned platform number for this train
lineStringDeparture line
pathStringArrival path
engineeringAllowanceTimeEngineering allowance
pathingAllowanceTimePathing allowance
performanceAllowanceTimePerformance allowance
crsCodeStringCRS code for the location
associationTrainAssociation[]List of association happens at this location
+ + \ No newline at end of file diff --git a/src/main/resources/static/docs/retv-base.html b/src/main/resources/static/docs/retv-base.html new file mode 100644 index 0000000..130f7db --- /dev/null +++ b/src/main/resources/static/docs/retv-base.html @@ -0,0 +1,23 @@ + + + + + Title + + + +

+ + + + + + + + + + + +
NameTypeDescription
+ + \ No newline at end of file diff --git a/src/main/resources/static/docs/schedule.html b/src/main/resources/static/docs/schedule.html new file mode 100644 index 0000000..91baa08 --- /dev/null +++ b/src/main/resources/static/docs/schedule.html @@ -0,0 +1,36 @@ + + + + + GB Rail API + + + + +

Search train schedule by train ID

+

Syntax

+
/schedule/{id}[/{date}]
+

Parameters

+ + + + + + + + + +
idTrain ID in the scheduling system
date + Optional + The date of the schedule to be searched, in YYYY-MM-DD + format.
+ Default: today +
+

Return Value

+TrainSchedule +

Type References

+ + + + + \ No newline at end of file diff --git a/src/main/resources/static/docs/schedule_all.html b/src/main/resources/static/docs/schedule_all.html new file mode 100644 index 0000000..652f3fb --- /dev/null +++ b/src/main/resources/static/docs/schedule_all.html @@ -0,0 +1,32 @@ + + + + + GB Rail API + + + + +

Search base train schedule by train ID

+

Find all schedules for the specified train ID. No train association information are included in this API call. If train association information is required, + Search schedule by train ID should be used.

+

A working timetable(WTT), or short term planning (STP) record may be being overridden by a cancellation or overlay +records.

+ +

Syntax

+
/schedule/{id}/all
+

Parameters

+ + + + + +
idTrain ID in the scheduling system
+

Return Value

+TrainSchedule[] +

Type References

+ + + + + \ No newline at end of file diff --git a/src/main/resources/static/docs/schedule_base.html b/src/main/resources/static/docs/schedule_base.html new file mode 100644 index 0000000..a1c6c5c --- /dev/null +++ b/src/main/resources/static/docs/schedule_base.html @@ -0,0 +1,39 @@ + + + + + GB Rail API + + + + +

Search base train schedule by train ID

+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, +Search schedule by train ID should be used. +

Syntax

+
/schedule/{id}[/{date}]/wtt
+

Parameters

+ + + + + + + + + +
idTrain ID in the scheduling system
date + Optional + The date of the schedule to be searched, in YYYY-MM-DD + format.
+ Default: today +
+

Return Value

+TrainSchedule +

Type References

+ + + + + \ No newline at end of file diff --git a/src/main/resources/static/docs/schedule_search.html b/src/main/resources/static/docs/schedule_search.html new file mode 100644 index 0000000..4919faa --- /dev/null +++ b/src/main/resources/static/docs/schedule_search.html @@ -0,0 +1,75 @@ + + + + + GB Rail API + + + + +

Search train schedule by location

+

Syntax

+
/search/{location}[/{date}[/{time}]]?[range={range}][&strict={strict}][&stopOnly={stopOnly}]
+

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + +
locationThe location going to be searched. It can be either a TIPLOC code, or a CRS code.
date + Optional, but required if time is specified. + The date going to be searched, in YYYY-MM-DD format. Default to the current + date. +
time + Optional. + The time going to be searched, in HH:MM format. Default to the current + time. +
range + Optional. + 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. +
strict +

+ Optional. + Search in strict mode. Default value is true or false. Default value is + false +

+

+ In strict mode, only the entry that matches the provided code will be included. +

+

+ In relaxed mode (default), the entries in same group (having same STANOX code or NALCO code) will + be also included in the search. +

+
stopOnly + Optional. + Show only stopping trains. Valid valued are true or false. Default value is + false. +
+

Return Value

+ScheduleSummary[] +

Type References

+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/script.js b/src/main/resources/static/docs/script.js new file mode 100644 index 0000000..06dfb84 --- /dev/null +++ b/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'; +} \ No newline at end of file diff --git a/src/main/resources/static/web/index.html b/src/main/resources/static/web/index.html new file mode 100644 index 0000000..f6b22c7 --- /dev/null +++ b/src/main/resources/static/web/index.html @@ -0,0 +1,12 @@ + + + + + GB Rail Data + + +
+ Documents +
+ + \ No newline at end of file diff --git a/src/test/java/org/leolo/nrapi/NrapiApplicationTests.java b/src/test/java/org/leolo/nrapi/NrapiApplicationTests.java new file mode 100644 index 0000000..25140dc --- /dev/null +++ b/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() { + } + + + + +} diff --git a/src/test/java/org/leolo/nrapi/TokenGenerateTest.java b/src/test/java/org/leolo/nrapi/TokenGenerateTest.java new file mode 100644 index 0000000..7087d57 --- /dev/null +++ b/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{ + TokenStore.getInstance().generateToken(1, -100); + } + ); + } +}