From 489789c6de8bcdd5614104b2995cfd6d71492b7c Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Mon, 7 Aug 2023 22:12:01 +0600 Subject: [PATCH 01/29] create a new vaadin project. --- .gitignore | 21 ++ LICENSE.md | 24 ++ frontend/index.html | 23 ++ frontend/themes/weatherapp/main-layout.css | 16 + frontend/themes/weatherapp/styles.css | 1 + frontend/themes/weatherapp/theme-editor.css | 0 frontend/themes/weatherapp/theme.json | 3 + mvnw | 316 ++++++++++++++++++ mvnw.cmd | 188 +++++++++++ pom.xml | 194 +++++++++++ .../eastnetic/application/Application.java | 23 ++ .../application/views/MainLayout.java | 76 +++++ .../application/views/about/AboutView.java | 34 ++ .../views/helloworld/HelloWorldView.java | 35 ++ .../META-INF/resources/icons/icon.png | Bin 0 -> 2424 bytes .../META-INF/resources/images/empty-plant.png | Bin 0 -> 70951 bytes src/main/resources/application.properties | 10 + src/main/resources/banner.txt | 6 + .../resources/vaadin-featureflags.properties | 2 + 19 files changed, 972 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 frontend/index.html create mode 100644 frontend/themes/weatherapp/main-layout.css create mode 100644 frontend/themes/weatherapp/styles.css create mode 100644 frontend/themes/weatherapp/theme-editor.css create mode 100644 frontend/themes/weatherapp/theme.json create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/com/eastnetic/application/Application.java create mode 100644 src/main/java/com/eastnetic/application/views/MainLayout.java create mode 100644 src/main/java/com/eastnetic/application/views/about/AboutView.java create mode 100644 src/main/java/com/eastnetic/application/views/helloworld/HelloWorldView.java create mode 100644 src/main/resources/META-INF/resources/icons/icon.png create mode 100644 src/main/resources/META-INF/resources/images/empty-plant.png create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/banner.txt create mode 100644 src/main/resources/vaadin-featureflags.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..dae6077fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +/target/ +.idea/ +.vscode/ +.settings +.project +.classpath + +*.iml +.DS_Store + +# The following files are generated/updated by vaadin-maven-plugin +node_modules/ +frontend/generated/ +pnpmfile.js +vite.generated.ts + +# Browser drivers for local integration tests +drivers/ +# Error screenshots generated by TestBench for failed integration tests +error-screenshots/ +webpack.generated.js diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..cf1ab25da --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..d36e59347 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/frontend/themes/weatherapp/main-layout.css b/frontend/themes/weatherapp/main-layout.css new file mode 100644 index 000000000..313f85bf0 --- /dev/null +++ b/frontend/themes/weatherapp/main-layout.css @@ -0,0 +1,16 @@ +vaadin-scroller[slot="drawer"] { + padding: var(--lumo-space-s); +} + +[slot="drawer"]:is(header, footer) { + display: flex; + align-items: center; + gap: var(--lumo-space-s); + padding: var(--lumo-space-s) var(--lumo-space-m); + min-height: var(--lumo-size-xl); + box-sizing: border-box; +} + +[slot="drawer"]:is(header, footer):is(:empty) { + display: none; +} diff --git a/frontend/themes/weatherapp/styles.css b/frontend/themes/weatherapp/styles.css new file mode 100644 index 000000000..dcf1f1874 --- /dev/null +++ b/frontend/themes/weatherapp/styles.css @@ -0,0 +1 @@ +@import url('./main-layout.css'); \ No newline at end of file diff --git a/frontend/themes/weatherapp/theme-editor.css b/frontend/themes/weatherapp/theme-editor.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/themes/weatherapp/theme.json b/frontend/themes/weatherapp/theme.json new file mode 100644 index 000000000..0f7a81f24 --- /dev/null +++ b/frontend/themes/weatherapp/theme.json @@ -0,0 +1,3 @@ +{ + "lumoImports" : [ "typography", "color", "spacing", "badge", "utility" ] +} \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 000000000..5643201c7 --- /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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# 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 000000000..23b7079a3 --- /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 http://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 000000000..33a05e1b9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,194 @@ + + + 4.0.0 + + com.eastnetic.application + weatherapp + weatherapp + 1.0-SNAPSHOT + jar + + + 17 + 24.1.4 + 4.10.0 + + + + org.springframework.boot + spring-boot-starter-parent + 3.1.0 + + + + + Vaadin Directory + https://maven.vaadin.com/vaadin-addons + + false + + + + + + + + com.vaadin + vaadin-bom + ${vaadin.version} + pom + import + + + + + + + com.vaadin + + vaadin + + + com.vaadin + vaadin-spring-boot-starter + + + org.parttio + line-awesome + 1.1.0 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin + vaadin-testbench-junit5 + test + + + + + spring-boot:run + + + org.springframework.boot + spring-boot-maven-plugin + + -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5569 + 500 + 240 + + + + + com.vaadin + vaadin-maven-plugin + ${vaadin.version} + + + + prepare-frontend + + + + + + + + + + + production + + + + com.vaadin + vaadin-core + + + com.vaadin + vaadin-dev + + + + + + + + com.vaadin + vaadin-maven-plugin + ${vaadin.version} + + + + build-frontend + + compile + + + + + + + + + it + + + + org.springframework.boot + spring-boot-maven-plugin + + + start-spring-boot + pre-integration-test + + start + + + + stop-spring-boot + post-integration-test + + stop + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + false + true + + + + + + + + diff --git a/src/main/java/com/eastnetic/application/Application.java b/src/main/java/com/eastnetic/application/Application.java new file mode 100644 index 000000000..6e2e80184 --- /dev/null +++ b/src/main/java/com/eastnetic/application/Application.java @@ -0,0 +1,23 @@ +package com.eastnetic.application; + +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.theme.Theme; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * The entry point of the Spring Boot application. + * + * Use the @PWA annotation make the application installable on phones, tablets + * and some desktop browsers. + * + */ +@SpringBootApplication +@Theme(value = "weatherapp") +public class Application implements AppShellConfigurator { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/src/main/java/com/eastnetic/application/views/MainLayout.java b/src/main/java/com/eastnetic/application/views/MainLayout.java new file mode 100644 index 000000000..d88e49f82 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/MainLayout.java @@ -0,0 +1,76 @@ +package com.eastnetic.application.views; + +import com.eastnetic.application.views.about.AboutView; +import com.eastnetic.application.views.helloworld.HelloWorldView; +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.Footer; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Header; +import com.vaadin.flow.component.orderedlayout.Scroller; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.theme.lumo.LumoUtility; +import org.vaadin.lineawesome.LineAwesomeIcon; + +/** + * The main view is a top-level placeholder for other views. + */ +public class MainLayout extends AppLayout { + + private H2 viewTitle; + + public MainLayout() { + setPrimarySection(Section.DRAWER); + addDrawerContent(); + addHeaderContent(); + } + + private void addHeaderContent() { + DrawerToggle toggle = new DrawerToggle(); + toggle.setAriaLabel("Menu toggle"); + + viewTitle = new H2(); + viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + + addToNavbar(true, toggle, viewTitle); + } + + private void addDrawerContent() { + H1 appName = new H1("WeatherApp"); + appName.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + Header header = new Header(appName); + + Scroller scroller = new Scroller(createNavigation()); + + addToDrawer(header, scroller, createFooter()); + } + + private SideNav createNavigation() { + SideNav nav = new SideNav(); + + nav.addItem(new SideNavItem("Hello World", HelloWorldView.class, LineAwesomeIcon.GLOBE_SOLID.create())); + nav.addItem(new SideNavItem("About", AboutView.class, LineAwesomeIcon.FILE.create())); + + return nav; + } + + private Footer createFooter() { + Footer layout = new Footer(); + + return layout; + } + + @Override + protected void afterNavigation() { + super.afterNavigation(); + viewTitle.setText(getCurrentPageTitle()); + } + + private String getCurrentPageTitle() { + PageTitle title = getContent().getClass().getAnnotation(PageTitle.class); + return title == null ? "" : title.value(); + } +} diff --git a/src/main/java/com/eastnetic/application/views/about/AboutView.java b/src/main/java/com/eastnetic/application/views/about/AboutView.java new file mode 100644 index 000000000..1705c2050 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/about/AboutView.java @@ -0,0 +1,34 @@ +package com.eastnetic.application.views.about; + +import com.eastnetic.application.views.MainLayout; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Image; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.theme.lumo.LumoUtility.Margin; + +@PageTitle("About") +@Route(value = "about", layout = MainLayout.class) +public class AboutView extends VerticalLayout { + + public AboutView() { + setSpacing(false); + + Image img = new Image("images/empty-plant.png", "placeholder plant"); + img.setWidth("200px"); + add(img); + + H2 header = new H2("This place intentionally left empty"); + header.addClassNames(Margin.Top.XLARGE, Margin.Bottom.MEDIUM); + add(header); + add(new Paragraph("It’s a place where you can grow your own UI 🤗")); + + setSizeFull(); + setJustifyContentMode(JustifyContentMode.CENTER); + setDefaultHorizontalComponentAlignment(Alignment.CENTER); + getStyle().set("text-align", "center"); + } + +} diff --git a/src/main/java/com/eastnetic/application/views/helloworld/HelloWorldView.java b/src/main/java/com/eastnetic/application/views/helloworld/HelloWorldView.java new file mode 100644 index 000000000..63c0a47a3 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/helloworld/HelloWorldView.java @@ -0,0 +1,35 @@ +package com.eastnetic.application.views.helloworld; + +import com.eastnetic.application.views.MainLayout; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; + +@PageTitle("Hello World") +@Route(value = "hello", layout = MainLayout.class) +@RouteAlias(value = "", layout = MainLayout.class) +public class HelloWorldView extends HorizontalLayout { + + private TextField name; + private Button sayHello; + + public HelloWorldView() { + name = new TextField("Your name"); + sayHello = new Button("Say hello"); + sayHello.addClickListener(e -> { + Notification.show("Hello " + name.getValue()); + }); + sayHello.addClickShortcut(Key.ENTER); + + setMargin(true); + setVerticalComponentAlignment(Alignment.END, name, sayHello); + + add(name, sayHello); + } + +} diff --git a/src/main/resources/META-INF/resources/icons/icon.png b/src/main/resources/META-INF/resources/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..df2ed4bd80778f7141c825ce9275984b4a34a15c GIT binary patch literal 2424 zcmb7GYdDl!8(!}_2Xi!xLSytzg|bPKGNKGdQ9{O+Mmf~5kr)bTi+7yjDmC;)Ax){I z!*)I-hJgV-~m>2#eiJdBKFd<3_8n6Io=hB2qm7CYu9O3 z0zkOu#&z^L08O-tR0RN${3zEeSF8KK#;IF&j$SWAQbB5s2(9EKzB z^Da35@IY%rH25Kcr3n#9Ea%9Z*qN$1Wn5aY^N2E|+p)g)VbS&ec{7g|RF_7zhQ9P@ z9yJtfd|8OyXCdeydcAzqT!x*f=r2AxXPA(bI&rxlm(Na~g!_06#7yqw%;pP2qxKW0 z6ekI)$kl&X1E*PbcXllP`;=MKDEWH-L1 zJqwMdAhJ)hd_>y^#1AECemZQfh2zWviA$auf@u4!aAZaKRY zzt3EQ&m*BaGq!6w!#6!QjVp^k`l^OPLIXZE{;(bPC>>2Eal&8cQ!do}fhTbSO8alG zTtPfKWo;-jt#)^Wq*{>P_TOkrLh~PZD%woFAO$Zs#BP|Ur)%DpWk617z1QQ88-)1z zQFk^LH&jMod<)$pvYS!Cl zlvPo+lA14Ikh02`Kd3WzuPUh}_%2sR*MHp}(E`)QsX$7ijsdr}5}}T|+&tFNw^y4n`en55hinWX3eD zrUd*`52D}1#C7m44j*)A<+5{qM3hNVlTz@ z&}_OK2K!qbBa5PBOPPX=CQ(E2!EWMGVb#a2*UV*tjHks26w7Xy9k`=g)T?*UXA4b4 zT+JhAiFZeQ3cR-nU$Hz!ynEAZ_JUZS5{D^+Sf89sP>2gBU3NozlO2ymlpmlNk&tU~ z8l21~3~T4hG|t!zM%*4d8-OskJwDtT*uBi_M35oopzFgx(ns+U|Icx(ICfeJMm@NM9 z5gt}L>Y#XDe6ZO!g)yo$zx07wvZcrLBXwYH^QN^Lx3w#Xc@?jfE1P1IcfRh@>{?ug z(lY8K9SXA&C+~L8obn4DY{A9AxKZWKvZ5KBTwD;466+ydnrO8LzB7|InNWPcjV|du z+srCh)$&Fs_KEV_=}k@h);n1h6tAD(oin#PLttU6HzFK5o{qimAKNoD!#r(s>@}Pc z_Mpb_em^W2Jbpt)Wwl#yLg${Jx(bch5weEylfompA_MC;)*u$K=X5T=6w3hqQVcgr zo6PT|B!aG8d=%YcKLRywF<*Kl-5%I9<@W$rDH$H*nNkt420<2PdJ)1(&0Zxe3Q@RQ z^_y}S`Q7j%*|^OJ`?c8Zu1>JcK4WVHBxdCobAQ7bhvYkL_w=cnYT|iNwU3T?MmffhO+zP#O<}9>mfrr z(08-6*k_mJvC^@SO)FMmEOlf`-<;zl+NJCb-y8<$zs;O0t2Z=(DvqHXHd7>;vH;eu z01*!CSR~}Y6R;a9#beX=h)~SKujH0|BB0U%D=BL|i3Jq|Q>2HWdv_6hVb2L95*yDy zKu3hsrUWc&o=9Yg#kcjwP^7oE8WNX%i8c_AneJ!9Pb7IcC`TdFMz|{8xF4Ay)&UIM zruFS*cNR})X^Cp>_HeQ^ATEBZHLIW~M&vnK%QLDS& zaBx>D7&3-MBB8=Ui`pcR!_W|3^nvIihx>F6UUBRrP|}r-gL-FpG{PTpxLa}$D?5(~ z^1OUtfz^ZENGMzs094B2z12!LQcCn%U;4xLj?1FUAswpf}2X^=6!9UzsZV2rin zQW!QL5n%EVSXASvFb8AuO(ae|-f)qSO>hn(261k%9w101NRJSM$C6{VE){=anRx%@ zDu$yEV}c=xpuqj5VVhnMewR-r=oNq zGDe`pZF$g*4Ld-XSS3>>kMgrt7pMfNlT@c~R=1HtAduBB#Q3bZz#Vr*^BVl&$`eeGVHm@3l9-1XQ{j&!?^`(YTdq~qC z@SZh%)lLn!tB`-_u2O$IP*%$Ooi)ocCRY8A-w*_xH{bdCE)GXm7r422aQ|`&O8pO# C@ant( literal 0 HcmV?d00001 diff --git a/src/main/resources/META-INF/resources/images/empty-plant.png b/src/main/resources/META-INF/resources/images/empty-plant.png new file mode 100644 index 0000000000000000000000000000000000000000..9777f260a67891001eb2444f49569037a74f671d GIT binary patch literal 70951 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 z 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 Date: Tue, 8 Aug 2023 02:19:31 +0600 Subject: [PATCH 02/29] Implement OpenMeteo location API integration. -- Have used RestTemplate to make API calls and caches the location data --- pom.xml | 10 ++ .../eastnetic/application/Application.java | 7 ++ .../application/config/CacheConfig.java | 27 ++++ .../locations/entity/LocationDetails.java | 115 ++++++++++++++++++ .../OpenMeteoLocationApiResponse.java | 29 +++++ .../service/LocationProviderService.java | 12 ++ .../OpenMeteoLocationProviderServiceImpl.java | 55 +++++++++ 7 files changed, 255 insertions(+) create mode 100644 src/main/java/com/eastnetic/application/config/CacheConfig.java create mode 100644 src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java create mode 100644 src/main/java/com/eastnetic/application/locations/response/OpenMeteoLocationApiResponse.java create mode 100644 src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java create mode 100644 src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java diff --git a/pom.xml b/pom.xml index 33a05e1b9..f773b4a47 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,16 @@ vaadin-testbench-junit5 test + + org.springframework.boot + spring-boot-starter-cache + + + + com.github.ben-manes.caffeine + caffeine + 2.9.2 + diff --git a/src/main/java/com/eastnetic/application/Application.java b/src/main/java/com/eastnetic/application/Application.java index 6e2e80184..63d679a4b 100644 --- a/src/main/java/com/eastnetic/application/Application.java +++ b/src/main/java/com/eastnetic/application/Application.java @@ -4,6 +4,8 @@ import com.vaadin.flow.theme.Theme; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; /** * The entry point of the Spring Boot application. @@ -20,4 +22,9 @@ public static void main(String[] args) { SpringApplication.run(Application.class, args); } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } diff --git a/src/main/java/com/eastnetic/application/config/CacheConfig.java b/src/main/java/com/eastnetic/application/config/CacheConfig.java new file mode 100644 index 000000000..de841f84c --- /dev/null +++ b/src/main/java/com/eastnetic/application/config/CacheConfig.java @@ -0,0 +1,27 @@ +package com.eastnetic.application.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + + cacheManager.setCaffeine(Caffeine.newBuilder() + .maximumSize(100) + .expireAfterWrite(60, TimeUnit.MINUTES)); + + return cacheManager; + } +} \ No newline at end of file diff --git a/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java new file mode 100644 index 000000000..111378b01 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java @@ -0,0 +1,115 @@ +package com.eastnetic.application.locations.entity; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class LocationDetails { + + @JsonProperty("id") + private long id; + + @JsonProperty("name") + private String name; + + @JsonProperty("latitude") + private double latitude; + + @JsonProperty("longitude") + private double longitude; + + @JsonProperty("elevation") + private double elevation; + + @JsonProperty("country_code") + private String countryCode; + + @JsonProperty("timezone") + private String timezone; + + @JsonProperty("country") + private String country; + + @JsonProperty("admin1") + private String admin1; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public double getElevation() { + return elevation; + } + + public void setElevation(double elevation) { + this.elevation = elevation; + } + + public String getCountryCode() { + return countryCode; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getAdmin1() { + return admin1; + } + + public void setAdmin1(String admin1) { + this.admin1 = admin1; + } + + public String locationFullName() { + + if (getAdmin1() == null || getAdmin1().trim().isEmpty()) { + return getName() + ", " + getCountry(); + } + + return getName() + ", " + getAdmin1() + ", " + getCountry(); + } +} diff --git a/src/main/java/com/eastnetic/application/locations/response/OpenMeteoLocationApiResponse.java b/src/main/java/com/eastnetic/application/locations/response/OpenMeteoLocationApiResponse.java new file mode 100644 index 000000000..45c507f2f --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/response/OpenMeteoLocationApiResponse.java @@ -0,0 +1,29 @@ +package com.eastnetic.application.locations.response; + + +import com.eastnetic.application.locations.entity.LocationDetails; + +import java.util.List; + +public class OpenMeteoLocationApiResponse { + + private List results; + + private double generationtime_ms; + + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + public double getGenerationtime_ms() { + return generationtime_ms; + } + + public void setGenerationtime_ms(double generationtime_ms) { + this.generationtime_ms = generationtime_ms; + } +} diff --git a/src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java b/src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java new file mode 100644 index 000000000..0ad0691c0 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java @@ -0,0 +1,12 @@ +package com.eastnetic.application.locations.service; + + +import com.eastnetic.application.locations.entity.LocationDetails; + +import java.util.List; + +public interface LocationProviderService { + + List getLocationDetails(String cityName); + +} diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java new file mode 100644 index 000000000..fd7fac94f --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java @@ -0,0 +1,55 @@ +package com.eastnetic.application.locations.serviceImpl; + + +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.locations.response.OpenMeteoLocationApiResponse; +import com.eastnetic.application.locations.service.LocationProviderService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Service +@CacheConfig(cacheNames = "locationDetailsCache") +public class OpenMeteoLocationProviderServiceImpl implements LocationProviderService { + + private static final Logger LOGGER = LogManager.getLogger(OpenMeteoLocationProviderServiceImpl.class); + + private static final String API_BASE_URL = "https://geocoding-api.open-meteo.com/v1/search?name="; + + private final RestTemplate restTemplate; + + public OpenMeteoLocationProviderServiceImpl(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + @Cacheable(key = "#cityName", unless = "#result == null") + public List getLocationDetails(String cityName) { + + LOGGER.info("Fetching location details from open-meteo api: City Name={}: START", cityName); + + String apiUrl = API_BASE_URL + cityName; + + try { + OpenMeteoLocationApiResponse response = restTemplate.getForObject(apiUrl, OpenMeteoLocationApiResponse.class); + + if (response != null && response.getResults() != null && !response.getResults().isEmpty()) { + + LOGGER.info("Fetching location details from open-meteo api: City Name={}: SUCCESS", cityName); + + return response.getResults(); + } + + } catch (Exception ex) { + + LOGGER.error("Fetching location details from open-meteo api: City Name={}: Error", cityName, ex); + } + + return null; + } +} From a8547312a541f2cf25dc4e4aec282f3dc56ab591 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Tue, 8 Aug 2023 04:23:14 +0600 Subject: [PATCH 03/29] Implement OpenMeteo weather API integration for daily and hourly weather. -- Have used RestTemplate to make API calls and caches the daily weather data. --- .../weathers/entity/CurrentWeather.java | 61 +++++++++++ .../weathers/entity/DailyWeather.java | 85 +++++++++++++++ .../weathers/entity/HourlyWeather.java | 63 +++++++++++ .../weathers/entity/WeatherData.java | 39 +++++++ .../service/WeatherProviderService.java | 13 +++ .../serviceImpl/OpenMeteoWeatherAdapter.java | 100 ++++++++++++++++++ 6 files changed, 361 insertions(+) create mode 100644 src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java create mode 100644 src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java create mode 100644 src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java create mode 100644 src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java create mode 100644 src/main/java/com/eastnetic/application/weathers/service/WeatherProviderService.java create mode 100644 src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherAdapter.java diff --git a/src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java b/src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java new file mode 100644 index 000000000..4f799c813 --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java @@ -0,0 +1,61 @@ +package com.eastnetic.application.weathers.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CurrentWeather { + + @JsonProperty("temperature") + private double temperature; + + @JsonProperty("windspeed") + private double windSpeed; + + @JsonProperty("winddirection") + private double windDirection; + + @JsonProperty("is_day") + private boolean isDay; + + @JsonProperty("time") + private String time; + + public double getTemperature() { + return temperature; + } + + public void setTemperature(double temperature) { + this.temperature = temperature; + } + + public double getWindSpeed() { + return windSpeed; + } + + public void setWindSpeed(double windSpeed) { + this.windSpeed = windSpeed; + } + + public double getWindDirection() { + return windDirection; + } + + public void setWindDirection(double windDirection) { + this.windDirection = windDirection; + } + + public boolean isDay() { + return isDay; + } + + public void setDay(boolean day) { + this.isDay = day; + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } +} diff --git a/src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java b/src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java new file mode 100644 index 000000000..2310141af --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java @@ -0,0 +1,85 @@ +package com.eastnetic.application.weathers.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class DailyWeather { + + @JsonProperty("time") + private List time; + + @JsonProperty("temperature_2m_max") + private List maxTemperature; + + @JsonProperty("temperature_2m_min") + private List minTemperature; + + @JsonProperty("sunrise") + private List sunrise; + + @JsonProperty("sunset") + private List sunset; + + @JsonProperty("rain_sum") + private List rainSum; + + @JsonProperty("windspeed_10m_max") + private List maxWindSpeed; + + public List getTime() { + return time; + } + + public void setTime(List time) { + this.time = time; + } + + public List getMaxTemperature() { + return maxTemperature; + } + + public void setMaxTemperature(List maxTemperature) { + this.maxTemperature = maxTemperature; + } + + public List getMinTemperature() { + return minTemperature; + } + + public void setMinTemperature(List minTemperature) { + this.minTemperature = minTemperature; + } + + public List getSunrise() { + return sunrise; + } + + public void setSunrise(List sunrise) { + this.sunrise = sunrise; + } + + public List getSunset() { + return sunset; + } + + public void setSunset(List sunset) { + this.sunset = sunset; + } + + public List getRainSum() { + return rainSum; + } + + public void setRainSum(List rainSum) { + this.rainSum = rainSum; + } + + public List getMaxWindSpeed() { + return maxWindSpeed; + } + + public void setMaxWindSpeed(List maxWindSpeed) { + this.maxWindSpeed = maxWindSpeed; + } +} diff --git a/src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java b/src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java new file mode 100644 index 000000000..a8ee07278 --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java @@ -0,0 +1,63 @@ +package com.eastnetic.application.weathers.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class HourlyWeather { + + @JsonProperty("time") + private List time; + + @JsonProperty("temperature_2m") + private List temperature; + + @JsonProperty("rain") + private List rain; + + @JsonProperty("windspeed_10m") + private List windSpeed; + + @JsonProperty("winddirection") + private List windDirection; + + public List getTime() { + return time; + } + + public void setTime(List time) { + this.time = time; + } + + public List getTemperature() { + return temperature; + } + + public void setTemperature(List temperature) { + this.temperature = temperature; + } + + public List getRain() { + return rain; + } + + public void setRain(List rain) { + this.rain = rain; + } + + public List getWindSpeed() { + return windSpeed; + } + + public void setWindSpeed(List windSpeed) { + this.windSpeed = windSpeed; + } + + public List getWindDirection() { + return windDirection; + } + + public void setWindDirection(List windDirection) { + this.windDirection = windDirection; + } +} diff --git a/src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java b/src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java new file mode 100644 index 000000000..a671e3477 --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java @@ -0,0 +1,39 @@ +package com.eastnetic.application.weathers.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class WeatherData { + + @JsonProperty("daily") + private DailyWeather dailyWeather; + + @JsonProperty("hourly") + private HourlyWeather hourlyWeather; + + @JsonProperty("current_weather") + private CurrentWeather currentWeather; + + public DailyWeather getDailyWeather() { + return dailyWeather; + } + + public void setDailyWeather(DailyWeather dailyWeather) { + this.dailyWeather = dailyWeather; + } + + public HourlyWeather getHourlyWeather() { + return hourlyWeather; + } + + public void setHourlyWeather(HourlyWeather hourlyWeather) { + this.hourlyWeather = hourlyWeather; + } + + public CurrentWeather getCurrentWeather() { + return currentWeather; + } + + public void setCurrentWeather(CurrentWeather currentWeather) { + this.currentWeather = currentWeather; + } +} diff --git a/src/main/java/com/eastnetic/application/weathers/service/WeatherProviderService.java b/src/main/java/com/eastnetic/application/weathers/service/WeatherProviderService.java new file mode 100644 index 000000000..b3f0975ef --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/service/WeatherProviderService.java @@ -0,0 +1,13 @@ +package com.eastnetic.application.weathers.service; + +import com.eastnetic.application.weathers.entity.WeatherData; + +import java.time.LocalDate; + +public interface WeatherProviderService { + + WeatherData getDailyWeatherData(double latitude, double longitude, String timezone); + + WeatherData getHourlyWeatherDataOfADay(double latitude, double longitude, String timezone, LocalDate date); + +} diff --git a/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherAdapter.java b/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherAdapter.java new file mode 100644 index 000000000..b1d947e23 --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherAdapter.java @@ -0,0 +1,100 @@ +package com.eastnetic.application.weathers.serviceImpl; + +import com.eastnetic.application.weathers.entity.WeatherData; +import com.eastnetic.application.weathers.service.WeatherProviderService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +@Service +@CacheConfig(cacheNames = "weatherDataCache") +public class OpenMeteoWeatherAdapter implements WeatherProviderService { + + private static final Logger LOGGER = LogManager.getLogger(OpenMeteoWeatherAdapter.class); + + private static final String API_BASE_URL = "https://api.open-meteo.com/v1/forecast?"; + + private final RestTemplate restTemplate; + + @Autowired + public OpenMeteoWeatherAdapter(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + private String buildApiUrl(double latitude, double longitude, String timezone) { + + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(API_BASE_URL) + .queryParam("latitude", latitude) + .queryParam("longitude", longitude) + .queryParam("timezone", timezone) + .queryParam("current_weather", true); + + return builder.toUriString(); + } + + @Override + @Cacheable(key = "'daily-weather-' + #latitude + '-' + #longitude + '-' + #timezone", unless = "#result == null") + public WeatherData getDailyWeatherData(double latitude, double longitude, String timezone) { + + String apiUrl = buildApiUrl(latitude, longitude, timezone) + + "&daily=temperature_2m_max,temperature_2m_min,sunrise,sunset,rain_sum,windspeed_10m_max"; + + LOGGER.info("Fetching daily weather data from open-meteo api: latitude={}, longitude={}, timezone={}: START", + latitude, longitude, timezone); + + try { + + WeatherData weatherData = restTemplate.getForObject(apiUrl, WeatherData.class); + + LOGGER.info("Fetching daily weather data from open-meteo api: latitude={}, longitude={}, timezone={}: SUCCESS", + latitude, longitude, timezone); + + return weatherData; + + } catch (Exception ex) { + + LOGGER.error("Fetching daily weather data from open-meteo api: latitude={}, longitude={}, timezone={}: Error", + latitude, longitude, timezone, ex); + } + + return null; + } + + @Override + public WeatherData getHourlyWeatherDataOfADay(double latitude, double longitude, String timezone, LocalDate date) { + + String dateString = date.format(DateTimeFormatter.ISO_LOCAL_DATE); + + String apiUrl = buildApiUrl(latitude, longitude, timezone) + + "&start_date=" + dateString + "&end_date=" + dateString + + "&hourly=temperature_2m,rain,windspeed_10m,winddirection_10m"; + + LOGGER.info("Fetching hourly weather data from open-meteo api: latitude={}, longitude={}, timezone={}, date={}: START", + latitude, longitude, timezone, date); + + try { + + WeatherData weatherData = restTemplate.getForObject(apiUrl, WeatherData.class); + + LOGGER.info("Fetching hourly weather data from open-meteo api: latitude={}, longitude={}, timezone={}, date={}: SUCCESS", + latitude, longitude, timezone, date); + + return weatherData; + + } catch (Exception ex) { + + LOGGER.error("Fetching hourly weather data from open-meteo api: latitude={}, longitude={}, timezone={}, date={}: Error", + latitude, longitude, timezone, date, ex); + } + + return null; + } +} \ No newline at end of file From 6ed702bc080b4c935e2d2d68a8a950f47563a3b6 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Tue, 8 Aug 2023 04:27:17 +0600 Subject: [PATCH 04/29] Change class name OpenMeteoWeatherAdapter to OpenMeteoWeatherProviderServiceImpl. --- ...dapter.java => OpenMeteoWeatherProviderServiceImpl.java} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/main/java/com/eastnetic/application/weathers/serviceImpl/{OpenMeteoWeatherAdapter.java => OpenMeteoWeatherProviderServiceImpl.java} (94%) diff --git a/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherAdapter.java b/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherProviderServiceImpl.java similarity index 94% rename from src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherAdapter.java rename to src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherProviderServiceImpl.java index b1d947e23..7d00cf824 100644 --- a/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherAdapter.java +++ b/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherProviderServiceImpl.java @@ -16,16 +16,16 @@ @Service @CacheConfig(cacheNames = "weatherDataCache") -public class OpenMeteoWeatherAdapter implements WeatherProviderService { +public class OpenMeteoWeatherProviderServiceImpl implements WeatherProviderService { - private static final Logger LOGGER = LogManager.getLogger(OpenMeteoWeatherAdapter.class); + private static final Logger LOGGER = LogManager.getLogger(OpenMeteoWeatherProviderServiceImpl.class); private static final String API_BASE_URL = "https://api.open-meteo.com/v1/forecast?"; private final RestTemplate restTemplate; @Autowired - public OpenMeteoWeatherAdapter(RestTemplate restTemplate) { + public OpenMeteoWeatherProviderServiceImpl(RestTemplate restTemplate) { this.restTemplate = restTemplate; } From dee978286ea585482195db24bb8ca5110bfcc30a Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Wed, 9 Aug 2023 04:31:35 +0600 Subject: [PATCH 05/29] Add search location functionality and location list component. -- Implemented search functionality in LocationSearchView. -- Created LocationListComponent to display search results. -- Updated MainLayout to include new components. --- frontend/themes/weatherapp/styles.css | 9 ++- .../views/FavouriteLocationsView.java | 8 +++ .../views/LocationListComponent.java | 32 +++++++++ .../application/views/LocationSearchView.java | 72 +++++++++++++++++++ .../application/views/MainLayout.java | 71 +++++------------- .../application/views/about/AboutView.java | 34 --------- .../views/helloworld/HelloWorldView.java | 35 --------- 7 files changed, 136 insertions(+), 125 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java create mode 100644 src/main/java/com/eastnetic/application/views/LocationListComponent.java create mode 100644 src/main/java/com/eastnetic/application/views/LocationSearchView.java delete mode 100644 src/main/java/com/eastnetic/application/views/about/AboutView.java delete mode 100644 src/main/java/com/eastnetic/application/views/helloworld/HelloWorldView.java diff --git a/frontend/themes/weatherapp/styles.css b/frontend/themes/weatherapp/styles.css index dcf1f1874..ecde5ba19 100644 --- a/frontend/themes/weatherapp/styles.css +++ b/frontend/themes/weatherapp/styles.css @@ -1 +1,8 @@ -@import url('./main-layout.css'); \ No newline at end of file +@import url('./main-layout.css'); + +.blue-span { + color: #0074D9; + font-size: 25px; +} + + diff --git a/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java b/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java new file mode 100644 index 000000000..e91fa8b27 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java @@ -0,0 +1,8 @@ +package com.eastnetic.application.views; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; + +@Route("favourite-locations") +public class FavouriteLocationsView extends VerticalLayout { +} diff --git a/src/main/java/com/eastnetic/application/views/LocationListComponent.java b/src/main/java/com/eastnetic/application/views/LocationListComponent.java new file mode 100644 index 000000000..1fd842ed0 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/LocationListComponent.java @@ -0,0 +1,32 @@ +package com.eastnetic.application.views; + +import com.eastnetic.application.locations.entity.LocationDetails; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.spring.annotation.UIScope; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@UIScope +public class LocationListComponent extends VerticalLayout { + + private Grid locationGrid = new Grid<>(LocationDetails.class); + + public LocationListComponent() { + + locationGrid.setColumns("name", "country", "latitude", "longitude", "elevation", "timezone"); + locationGrid.setVisible(false); + locationGrid.setHeight("27em"); + add(locationGrid); + } + + public void showLocationList(List locationList) { + + if (locationList != null && !locationList.isEmpty()) { + locationGrid.setVisible(true); + locationGrid.setItems(locationList); + } + } +} diff --git a/src/main/java/com/eastnetic/application/views/LocationSearchView.java b/src/main/java/com/eastnetic/application/views/LocationSearchView.java new file mode 100644 index 000000000..e514571f9 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/LocationSearchView.java @@ -0,0 +1,72 @@ +package com.eastnetic.application.views; + +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.locations.service.LocationProviderService; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; +import com.vaadin.flow.spring.annotation.UIScope; +import org.springframework.stereotype.Component; + +import java.util.List; + +@UIScope +@Component +@Route(value = "locations", layout = MainLayout.class) +@RouteAlias(value = "", layout = MainLayout.class) +@PageTitle("Locations") +public class LocationSearchView extends VerticalLayout { + + private TextField searchField = new TextField(); + + private Button searchButton = new Button("Search"); + + private final LocationListComponent locationListComponent; + + private final LocationProviderService locationProviderService; + + public LocationSearchView(LocationListComponent locationListComponent, + LocationProviderService locationProviderService) { + + this.locationListComponent = locationListComponent; + this.locationProviderService = locationProviderService; + + setSizeFull(); + + add(getSearchForm(), locationListComponent); + + searchButton.addClickListener(e -> searchLocations()); + } + + private FormLayout getSearchForm() { + + searchField.setPlaceholder("Search by City Name"); + + searchButton.addClickShortcut(Key.ENTER); + + FormLayout searchForm = new FormLayout(searchField, searchButton); + searchForm.setWidth("50%"); + + return searchForm; + + } + + private void searchLocations() { + + String cityName = searchField.getValue(); + + if (cityName != null && !cityName.isEmpty()) { + + List locations = locationProviderService.getLocationDetails(cityName); + + locationListComponent.showLocationList(locations); + + } + } + +} diff --git a/src/main/java/com/eastnetic/application/views/MainLayout.java b/src/main/java/com/eastnetic/application/views/MainLayout.java index d88e49f82..b0aeecf70 100644 --- a/src/main/java/com/eastnetic/application/views/MainLayout.java +++ b/src/main/java/com/eastnetic/application/views/MainLayout.java @@ -1,76 +1,37 @@ package com.eastnetic.application.views; -import com.eastnetic.application.views.about.AboutView; -import com.eastnetic.application.views.helloworld.HelloWorldView; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.DrawerToggle; -import com.vaadin.flow.component.html.Footer; -import com.vaadin.flow.component.html.H1; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.Header; -import com.vaadin.flow.component.orderedlayout.Scroller; -import com.vaadin.flow.component.sidenav.SideNav; -import com.vaadin.flow.component.sidenav.SideNavItem; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.theme.lumo.LumoUtility; -import org.vaadin.lineawesome.LineAwesomeIcon; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.RouterLink; -/** - * The main view is a top-level placeholder for other views. - */ public class MainLayout extends AppLayout { - private H2 viewTitle; - public MainLayout() { - setPrimarySection(Section.DRAWER); - addDrawerContent(); - addHeaderContent(); - } - private void addHeaderContent() { - DrawerToggle toggle = new DrawerToggle(); - toggle.setAriaLabel("Menu toggle"); + addToNavbar(new DrawerToggle(), createTopBarSpan()); - viewTitle = new H2(); - viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); - - addToNavbar(true, toggle, viewTitle); + addToDrawer(createSidebarLayout()); } - private void addDrawerContent() { - H1 appName = new H1("WeatherApp"); - appName.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); - Header header = new Header(appName); + private Span createTopBarSpan() { - Scroller scroller = new Scroller(createNavigation()); + Span title = new Span("Weather App"); + title.addClassName("blue-span"); - addToDrawer(header, scroller, createFooter()); + return title; } - private SideNav createNavigation() { - SideNav nav = new SideNav(); - - nav.addItem(new SideNavItem("Hello World", HelloWorldView.class, LineAwesomeIcon.GLOBE_SOLID.create())); - nav.addItem(new SideNavItem("About", AboutView.class, LineAwesomeIcon.FILE.create())); + private VerticalLayout createSidebarLayout() { - return nav; - } - - private Footer createFooter() { - Footer layout = new Footer(); + VerticalLayout menuLayout = new VerticalLayout(); - return layout; - } + RouterLink homeLink = new RouterLink("Home", LocationSearchView.class); + RouterLink locationsLink = new RouterLink("Favourite Locations", FavouriteLocationsView.class); - @Override - protected void afterNavigation() { - super.afterNavigation(); - viewTitle.setText(getCurrentPageTitle()); - } + menuLayout.add(homeLink, locationsLink); - private String getCurrentPageTitle() { - PageTitle title = getContent().getClass().getAnnotation(PageTitle.class); - return title == null ? "" : title.value(); + return menuLayout; } -} +} \ No newline at end of file diff --git a/src/main/java/com/eastnetic/application/views/about/AboutView.java b/src/main/java/com/eastnetic/application/views/about/AboutView.java deleted file mode 100644 index 1705c2050..000000000 --- a/src/main/java/com/eastnetic/application/views/about/AboutView.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.eastnetic.application.views.about; - -import com.eastnetic.application.views.MainLayout; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.Image; -import com.vaadin.flow.component.html.Paragraph; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.theme.lumo.LumoUtility.Margin; - -@PageTitle("About") -@Route(value = "about", layout = MainLayout.class) -public class AboutView extends VerticalLayout { - - public AboutView() { - setSpacing(false); - - Image img = new Image("images/empty-plant.png", "placeholder plant"); - img.setWidth("200px"); - add(img); - - H2 header = new H2("This place intentionally left empty"); - header.addClassNames(Margin.Top.XLARGE, Margin.Bottom.MEDIUM); - add(header); - add(new Paragraph("It’s a place where you can grow your own UI 🤗")); - - setSizeFull(); - setJustifyContentMode(JustifyContentMode.CENTER); - setDefaultHorizontalComponentAlignment(Alignment.CENTER); - getStyle().set("text-align", "center"); - } - -} diff --git a/src/main/java/com/eastnetic/application/views/helloworld/HelloWorldView.java b/src/main/java/com/eastnetic/application/views/helloworld/HelloWorldView.java deleted file mode 100644 index 63c0a47a3..000000000 --- a/src/main/java/com/eastnetic/application/views/helloworld/HelloWorldView.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.eastnetic.application.views.helloworld; - -import com.eastnetic.application.views.MainLayout; -import com.vaadin.flow.component.Key; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouteAlias; - -@PageTitle("Hello World") -@Route(value = "hello", layout = MainLayout.class) -@RouteAlias(value = "", layout = MainLayout.class) -public class HelloWorldView extends HorizontalLayout { - - private TextField name; - private Button sayHello; - - public HelloWorldView() { - name = new TextField("Your name"); - sayHello = new Button("Say hello"); - sayHello.addClickListener(e -> { - Notification.show("Hello " + name.getValue()); - }); - sayHello.addClickShortcut(Key.ENTER); - - setMargin(true); - setVerticalComponentAlignment(Alignment.END, name, sayHello); - - add(name, sayHello); - } - -} From b9a89d82a3bad70ccb431b58fcea0a5bc2deca57 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Wed, 9 Aug 2023 06:59:01 +0600 Subject: [PATCH 06/29] Added pagination functionality to the location search results. --- .../views/LocationListComponent.java | 41 +++++++++++-- .../views/PaginationComponent.java | 61 +++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/views/PaginationComponent.java diff --git a/src/main/java/com/eastnetic/application/views/LocationListComponent.java b/src/main/java/com/eastnetic/application/views/LocationListComponent.java index 1fd842ed0..bd89f5c90 100644 --- a/src/main/java/com/eastnetic/application/views/LocationListComponent.java +++ b/src/main/java/com/eastnetic/application/views/LocationListComponent.java @@ -6,27 +6,58 @@ import com.vaadin.flow.spring.annotation.UIScope; import org.springframework.stereotype.Component; +import java.util.ArrayList; import java.util.List; @Component @UIScope -public class LocationListComponent extends VerticalLayout { +public class LocationListComponent extends VerticalLayout implements PaginationComponent.PaginationListener { + private PaginationComponent paginationComponent; + private List locationList = new ArrayList<>(); private Grid locationGrid = new Grid<>(LocationDetails.class); public LocationListComponent() { locationGrid.setColumns("name", "country", "latitude", "longitude", "elevation", "timezone"); locationGrid.setVisible(false); - locationGrid.setHeight("27em"); add(locationGrid); + + paginationComponent = new PaginationComponent(this); + add(paginationComponent); } public void showLocationList(List locationList) { - if (locationList != null && !locationList.isEmpty()) { - locationGrid.setVisible(true); - locationGrid.setItems(locationList); + this.locationList = locationList; + + onPageChange(1); + + int totalPages = (int) Math.ceil((double) locationList.size() / paginationComponent.getPageSize()); + + paginationComponent.setTotalPages(totalPages); + } + + @Override + public void onPageChange(int page) { + + if (locationList == null || locationList.isEmpty()) { + locationGrid.setVisible(false); + return; } + + int totalPages = (int) Math.ceil((double) locationList.size() / paginationComponent.getPageSize()); + int validPage = Math.min(Math.max(page, 1), totalPages); + + int fromIndex = (validPage - 1) * paginationComponent.getPageSize(); + int toIndex = Math.min(fromIndex + paginationComponent.getPageSize(), locationList.size()); + + List pageLocations = locationList.subList(fromIndex, toIndex); + + locationGrid.setItems(pageLocations); + locationGrid.setVisible(true); + + paginationComponent.setVisible(true); + paginationComponent.setCurrentPage(page); } } diff --git a/src/main/java/com/eastnetic/application/views/PaginationComponent.java b/src/main/java/com/eastnetic/application/views/PaginationComponent.java new file mode 100644 index 000000000..dce7a5ad4 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/PaginationComponent.java @@ -0,0 +1,61 @@ +package com.eastnetic.application.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; + +public class PaginationComponent extends HorizontalLayout { + + private final Span pageField = new Span(); + private final Button nextButton = new Button("Next"); + private final Button prevButton = new Button("Previous"); + + private int totalPages = 1; + private int currentPage = 1; + + private final PaginationListener paginationListener; + + public PaginationComponent(PaginationListener paginationListener) { + this.paginationListener = paginationListener; + + prevButton.addClickListener(e -> showPage(currentPage - 1)); + nextButton.addClickListener(e -> showPage(currentPage + 1)); + + add(prevButton, pageField, nextButton); + setAlignItems(FlexComponent.Alignment.CENTER); + + setVisible(false); + + updateButtons(); + } + + public void setTotalPages(int totalPages) { + this.totalPages = totalPages; + updateButtons(); + } + + public void setCurrentPage(int currentPage) { + this.currentPage = currentPage; + updateButtons(); + } + + public int getPageSize() { + return 10; + } + + public void showPage(int page) { + paginationListener.onPageChange(page); + } + + protected void updateButtons() { + prevButton.setEnabled(currentPage > 1); + nextButton.setEnabled(currentPage < totalPages); + pageField.setText("Page " + currentPage + " of " + totalPages); + } + + public interface PaginationListener { + void onPageChange(int page); + } +} + From 1fe471dc7c7101360d1cc6a10cdf360b61a50207 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Wed, 9 Aug 2023 07:08:39 +0600 Subject: [PATCH 07/29] Added count parameter in LocationProviderService to fetch location data. --- .../locations/service/LocationProviderService.java | 2 +- .../serviceImpl/OpenMeteoLocationProviderServiceImpl.java | 4 ++-- .../com/eastnetic/application/views/LocationSearchView.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java b/src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java index 0ad0691c0..4b17f4567 100644 --- a/src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java +++ b/src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java @@ -7,6 +7,6 @@ public interface LocationProviderService { - List getLocationDetails(String cityName); + List getLocationDetails(String cityName, int count); } diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java index fd7fac94f..d42dc90a3 100644 --- a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java @@ -29,11 +29,11 @@ public OpenMeteoLocationProviderServiceImpl(RestTemplate restTemplate) { @Override @Cacheable(key = "#cityName", unless = "#result == null") - public List getLocationDetails(String cityName) { + public List getLocationDetails(String cityName, int count) { LOGGER.info("Fetching location details from open-meteo api: City Name={}: START", cityName); - String apiUrl = API_BASE_URL + cityName; + String apiUrl = API_BASE_URL + cityName + "&count=" + count; try { OpenMeteoLocationApiResponse response = restTemplate.getForObject(apiUrl, OpenMeteoLocationApiResponse.class); diff --git a/src/main/java/com/eastnetic/application/views/LocationSearchView.java b/src/main/java/com/eastnetic/application/views/LocationSearchView.java index e514571f9..7c437eda6 100644 --- a/src/main/java/com/eastnetic/application/views/LocationSearchView.java +++ b/src/main/java/com/eastnetic/application/views/LocationSearchView.java @@ -62,7 +62,7 @@ private void searchLocations() { if (cityName != null && !cityName.isEmpty()) { - List locations = locationProviderService.getLocationDetails(cityName); + List locations = locationProviderService.getLocationDetails(cityName, 50); locationListComponent.showLocationList(locations); From 3bd7a77c05c321043577b1e00cf9890bd9cb3a74 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Wed, 9 Aug 2023 07:35:09 +0600 Subject: [PATCH 08/29] Added location exception handling and improved error messages. --- .../exceptions/LocationNotFoundException.java | 13 +++++++++++++ .../exceptions/LocationProviderException.java | 13 +++++++++++++ .../OpenMeteoLocationProviderServiceImpl.java | 8 ++++++-- .../application/views/LocationSearchView.java | 15 +++++++++++++-- 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/locations/exceptions/LocationNotFoundException.java create mode 100644 src/main/java/com/eastnetic/application/locations/exceptions/LocationProviderException.java diff --git a/src/main/java/com/eastnetic/application/locations/exceptions/LocationNotFoundException.java b/src/main/java/com/eastnetic/application/locations/exceptions/LocationNotFoundException.java new file mode 100644 index 000000000..909e252bf --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/exceptions/LocationNotFoundException.java @@ -0,0 +1,13 @@ +package com.eastnetic.application.locations.exceptions; + + +public class LocationNotFoundException extends RuntimeException { + + public LocationNotFoundException(String message) { + super(message); + } + + public LocationNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/eastnetic/application/locations/exceptions/LocationProviderException.java b/src/main/java/com/eastnetic/application/locations/exceptions/LocationProviderException.java new file mode 100644 index 000000000..971fd64ac --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/exceptions/LocationProviderException.java @@ -0,0 +1,13 @@ +package com.eastnetic.application.locations.exceptions; + + +public class LocationProviderException extends RuntimeException { + + public LocationProviderException(String message) { + super(message); + } + + public LocationProviderException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java index d42dc90a3..be60cf450 100644 --- a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java @@ -2,6 +2,8 @@ import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.locations.exceptions.LocationNotFoundException; +import com.eastnetic.application.locations.exceptions.LocationProviderException; import com.eastnetic.application.locations.response.OpenMeteoLocationApiResponse; import com.eastnetic.application.locations.service.LocationProviderService; import org.apache.logging.log4j.LogManager; @@ -45,11 +47,13 @@ public List getLocationDetails(String cityName, int count) { return response.getResults(); } + throw new LocationNotFoundException("Location not found for city: " + cityName); + } catch (Exception ex) { LOGGER.error("Fetching location details from open-meteo api: City Name={}: Error", cityName, ex); - } - return null; + throw new LocationProviderException("Error fetching location details from open-meteo api.", ex); + } } } diff --git a/src/main/java/com/eastnetic/application/views/LocationSearchView.java b/src/main/java/com/eastnetic/application/views/LocationSearchView.java index 7c437eda6..9a8fbc81e 100644 --- a/src/main/java/com/eastnetic/application/views/LocationSearchView.java +++ b/src/main/java/com/eastnetic/application/views/LocationSearchView.java @@ -1,10 +1,12 @@ package com.eastnetic.application.views; import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.locations.exceptions.LocationProviderException; import com.eastnetic.application.locations.service.LocationProviderService; import com.vaadin.flow.component.Key; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.PageTitle; @@ -62,10 +64,19 @@ private void searchLocations() { if (cityName != null && !cityName.isEmpty()) { - List locations = locationProviderService.getLocationDetails(cityName, 50); + try { - locationListComponent.showLocationList(locations); + List locations = locationProviderService.getLocationDetails(cityName, 50); + locationListComponent.showLocationList(locations); + + } catch (LocationProviderException ex) { + + locationListComponent.setVisible(false); + + Notification.show(ex.getMessage(), 3000, Notification.Position.MIDDLE); + + } } } From a8d4e3c1d94f3b235a66b032a6a89815c41e0b6f Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Thu, 10 Aug 2023 01:22:52 +0600 Subject: [PATCH 09/29] Implemented navigation to weather details view on location selection. -- Add separate method to configure location grid and pagination component. --- .../views/LocationListComponent.java | 22 +++++++- .../application/views/WeatherView.java | 53 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eastnetic/application/views/WeatherView.java diff --git a/src/main/java/com/eastnetic/application/views/LocationListComponent.java b/src/main/java/com/eastnetic/application/views/LocationListComponent.java index bd89f5c90..b4e23f59b 100644 --- a/src/main/java/com/eastnetic/application/views/LocationListComponent.java +++ b/src/main/java/com/eastnetic/application/views/LocationListComponent.java @@ -1,6 +1,7 @@ package com.eastnetic.application.views; import com.eastnetic.application.locations.entity.LocationDetails; +import com.vaadin.flow.component.UI; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.spring.annotation.UIScope; @@ -19,11 +20,24 @@ public class LocationListComponent extends VerticalLayout implements PaginationC public LocationListComponent() { - locationGrid.setColumns("name", "country", "latitude", "longitude", "elevation", "timezone"); + configureLocationGrid(); + configurePaginationComponent(); + } + + private void configureLocationGrid() { + locationGrid.setVisible(false); + locationGrid.getColumns().forEach(col -> col.setAutoWidth(true)); + locationGrid.setColumns("name", "country", "latitude", "longitude", "elevation", "timezone"); + locationGrid.asSingleSelect().addValueChangeListener(event -> showWeatherDate(event.getValue())); + add(locationGrid); + } + + private void configurePaginationComponent() { paginationComponent = new PaginationComponent(this); + add(paginationComponent); } @@ -38,6 +52,12 @@ public void showLocationList(List locationList) { paginationComponent.setTotalPages(totalPages); } + private void showWeatherDate(LocationDetails selectedLocation) { + + UI.getCurrent().getSession().setAttribute("selectedLocation", selectedLocation); + UI.getCurrent().navigate(WeatherView.class); + } + @Override public void onPageChange(int page) { diff --git a/src/main/java/com/eastnetic/application/views/WeatherView.java b/src/main/java/com/eastnetic/application/views/WeatherView.java new file mode 100644 index 000000000..25c468668 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -0,0 +1,53 @@ +package com.eastnetic.application.views; + +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.weathers.entity.WeatherData; +import com.eastnetic.application.weathers.service.WeatherProviderService; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.UIScope; +import org.springframework.stereotype.Component; + +@Component +@UIScope +@Route(value = "weather", layout = MainLayout.class) +@PageTitle("Weather Information") +public class WeatherView extends VerticalLayout implements BeforeEnterObserver { + + private LocationDetails location; + + private final WeatherProviderService weatherProviderService; + + public WeatherView(WeatherProviderService weatherProviderService) { + + this.weatherProviderService = weatherProviderService; + + H1 h1 = new H1("Here we will Show weather data."); + + add(h1); + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + + event.getNavigationTarget(); + location = (LocationDetails) UI.getCurrent().getSession().getAttribute("selectedLocation"); + + if (location != null) { + + updateWeatherData(); + } + } + + private void updateWeatherData() { + + WeatherData weatherData = weatherProviderService.getDailyWeatherData( + location.getLatitude(), location.getLongitude(), location.getTimezone() + ); + } +} From c629702261940acbf936acbf0feafac7aa46698c Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Thu, 10 Aug 2023 03:07:47 +0600 Subject: [PATCH 10/29] Add CurrentWeatherCard to show current weather information in weather view. --- frontend/themes/weatherapp/styles.css | 34 ++++++++++++++ pom.xml | 5 +++ .../application/views/CurrentWeatherCard.java | 44 +++++++++++++++++++ .../application/views/WeatherView.java | 16 ++++--- 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java diff --git a/frontend/themes/weatherapp/styles.css b/frontend/themes/weatherapp/styles.css index ecde5ba19..158fe8e8a 100644 --- a/frontend/themes/weatherapp/styles.css +++ b/frontend/themes/weatherapp/styles.css @@ -5,4 +5,38 @@ font-size: 25px; } +.weather-card { + width: 100%; + height: auto; + border-radius: 10px; + padding: 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: space-between; + position: relative; + color: white; +} + +.info-container { + display: flex; + flex-direction: column; /* Display items vertically */ + align-items: flex-start; /* Align items to the left */ +} + +.data-container { + width: 100%; /* Take up full width */ + display: flex; + justify-content: space-between; /* Space out the elements */ + align-items: center; /* Center vertically */ +} + +.location { + font-size: 18px; + font-weight: bold; +} +.temperature, .wind-speed, .date-time { + font-size: 14px; + flex: 1; /* Distribute available space equally */ + margin: 5px; +} diff --git a/pom.xml b/pom.xml index f773b4a47..4fe0f5a1f 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,11 @@ caffeine 2.9.2 + + com.vaadin + vaadin-icons + 3.0.1 + diff --git a/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java b/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java new file mode 100644 index 000000000..7e1148c22 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java @@ -0,0 +1,44 @@ +package com.eastnetic.application.views; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.FlexLayout; +import com.vaadin.flow.component.textfield.TextField; + +public class CurrentWeatherCard extends Div { + + public CurrentWeatherCard(double temperature, double windSpeed, String location, String dateTime) { + + addClassName("weather-card"); + + FlexLayout infoContainer = new FlexLayout(); + infoContainer.addClassName("info-container"); + + TextField locationField = new TextField("Location"); + locationField.setValue(location); + locationField.setReadOnly(true); + locationField.addClassName("location"); + + FlexLayout dataContainer = new FlexLayout(); + dataContainer.addClassName("data-container"); + + TextField temperatureField = new TextField("Temperature"); + temperatureField.setValue(temperature + " °C"); + temperatureField.setReadOnly(true); + temperatureField.addClassName("temperature"); + + TextField windSpeedField = new TextField("Wind Speed"); + windSpeedField.setValue(windSpeed + " m/s"); + windSpeedField.setReadOnly(true); + windSpeedField.addClassName("wind-speed"); + + TextField dateTimeField = new TextField("Date and Time"); + dateTimeField.setValue(dateTime); + dateTimeField.setReadOnly(true); + dateTimeField.addClassName("date-time"); + + dataContainer.add(temperatureField, windSpeedField, dateTimeField); + + infoContainer.add(locationField, dataContainer); + add(infoContainer); + } +} diff --git a/src/main/java/com/eastnetic/application/views/WeatherView.java b/src/main/java/com/eastnetic/application/views/WeatherView.java index 25c468668..c52283c24 100644 --- a/src/main/java/com/eastnetic/application/views/WeatherView.java +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -4,7 +4,6 @@ import com.eastnetic.application.weathers.entity.WeatherData; import com.eastnetic.application.weathers.service.WeatherProviderService; import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterObserver; @@ -25,11 +24,9 @@ public class WeatherView extends VerticalLayout implements BeforeEnterObserver { public WeatherView(WeatherProviderService weatherProviderService) { - this.weatherProviderService = weatherProviderService; - - H1 h1 = new H1("Here we will Show weather data."); + addClassName("weather-view"); - add(h1); + this.weatherProviderService = weatherProviderService; } @Override @@ -49,5 +46,14 @@ private void updateWeatherData() { WeatherData weatherData = weatherProviderService.getDailyWeatherData( location.getLatitude(), location.getLongitude(), location.getTimezone() ); + + CurrentWeatherCard weatherCard = new CurrentWeatherCard( + weatherData.getCurrentWeather().getTemperature(), + weatherData.getCurrentWeather().getWindSpeed(), + location.locationFullName(), + weatherData.getCurrentWeather().getTime() + ); + + add(weatherCard); } } From 7b5f9b6f144c946008b0d9be9e6c68961550aed5 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Thu, 10 Aug 2023 05:09:44 +0600 Subject: [PATCH 11/29] Update current weather card. --- frontend/themes/weatherapp/styles.css | 20 +++++++++++++++++- .../application/views/CurrentWeatherCard.java | 21 +++++++++++++------ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/frontend/themes/weatherapp/styles.css b/frontend/themes/weatherapp/styles.css index 158fe8e8a..b9b8884f4 100644 --- a/frontend/themes/weatherapp/styles.css +++ b/frontend/themes/weatherapp/styles.css @@ -31,8 +31,26 @@ } .location { - font-size: 18px; + font-size: 24px; font-weight: bold; + border: none !important; + box-shadow: none !important; + background: none !important; + padding: 0 !important; + outline: none !important; + appearance: none; +} + +.current-weather { + font-size: 24px; + font-weight: bold; + border: none !important; + box-shadow: none !important; + background: none !important; + padding: 0 !important; + outline: none !important; + appearance: none; + margin-bottom: 50px; } .temperature, .wind-speed, .date-time { diff --git a/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java b/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java index 7e1148c22..7ddd7877c 100644 --- a/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java +++ b/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java @@ -1,22 +1,31 @@ package com.eastnetic.application.views; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.html.H5; import com.vaadin.flow.component.orderedlayout.FlexLayout; import com.vaadin.flow.component.textfield.TextField; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + public class CurrentWeatherCard extends Div { public CurrentWeatherCard(double temperature, double windSpeed, String location, String dateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a"); + LocalDateTime localDateTime = LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_DATE_TIME); + addClassName("weather-card"); FlexLayout infoContainer = new FlexLayout(); infoContainer.addClassName("info-container"); - TextField locationField = new TextField("Location"); - locationField.setValue(location); - locationField.setReadOnly(true); - locationField.addClassName("location"); + H3 locationText = new H3(location); + locationText.addClassName("location"); + + H5 currentWeatherText = new H5("Current Weather"); + locationText.addClassName("current-weather"); FlexLayout dataContainer = new FlexLayout(); dataContainer.addClassName("data-container"); @@ -32,13 +41,13 @@ public CurrentWeatherCard(double temperature, double windSpeed, String location, windSpeedField.addClassName("wind-speed"); TextField dateTimeField = new TextField("Date and Time"); - dateTimeField.setValue(dateTime); + dateTimeField.setValue(localDateTime.format(formatter)); dateTimeField.setReadOnly(true); dateTimeField.addClassName("date-time"); dataContainer.add(temperatureField, windSpeedField, dateTimeField); - infoContainer.add(locationField, dataContainer); + infoContainer.add(locationText, currentWeatherText, dataContainer); add(infoContainer); } } From 16ce6765a07013262ea06a4462c8599b4b0f2869 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Thu, 10 Aug 2023 05:30:40 +0600 Subject: [PATCH 12/29] Add DailyWeatherCard to show daily weather information. --- frontend/themes/weatherapp/styles.css | 26 ++++++++ .../application/views/DailyWeatherCard.java | 60 +++++++++++++++++++ .../application/views/WeatherView.java | 20 +++++++ 3 files changed, 106 insertions(+) create mode 100644 src/main/java/com/eastnetic/application/views/DailyWeatherCard.java diff --git a/frontend/themes/weatherapp/styles.css b/frontend/themes/weatherapp/styles.css index b9b8884f4..57b25ea34 100644 --- a/frontend/themes/weatherapp/styles.css +++ b/frontend/themes/weatherapp/styles.css @@ -58,3 +58,29 @@ flex: 1; /* Distribute available space equally */ margin: 5px; } + + +.daily-weather-card { + width: 100%; + height: auto; + border-radius: 10px; + padding: 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: space-between; + position: relative; + color: black; + margin-top: 10px; +} + +.info-layout { + display: flex; + flex-direction: row; /* Display items horizontally */ + align-items: center; /* Center items vertically */ + gap: 10px; /* Add some space between items */ +} + +.max-temperature, .min-temperature, .sunrise, .sunset, .rain, max-wind-speed { + font-size: 14px; + flex: 1; /* Distribute available space equally */ +} \ No newline at end of file diff --git a/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java b/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java new file mode 100644 index 000000000..5a5ce9aad --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java @@ -0,0 +1,60 @@ +package com.eastnetic.application.views; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class DailyWeatherCard extends Div { + + public DailyWeatherCard(String day, double maxTemperature, double minTemperature, String sunrise, String sunset, double rainSum, double maxWindSpeed) { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); + LocalDateTime sunriseDateTime = LocalDateTime.parse(sunrise, DateTimeFormatter.ISO_DATE_TIME); + LocalDateTime sunsetDateTime = LocalDateTime.parse(sunset, DateTimeFormatter.ISO_DATE_TIME); + + HorizontalLayout infoLayout = new HorizontalLayout(); + infoLayout.addClassName("info-layout"); + + TextField dayField = new TextField("Day"); + dayField.setValue(day); + dayField.setReadOnly(true); + dayField.addClassName("day"); + + TextField maxTemperatureField = new TextField("Max Temperature"); + maxTemperatureField.setValue(maxTemperature + " °C"); + maxTemperatureField.setReadOnly(true); + maxTemperatureField.addClassName("max-temperature"); + + TextField minTemperatureField = new TextField("Min Temperature"); + minTemperatureField.setValue(minTemperature + " °C"); + minTemperatureField.setReadOnly(true); + minTemperatureField.addClassName("min-temperature"); + + TextField sunriseField = new TextField("Sunrise"); + sunriseField.setValue(sunriseDateTime.format(formatter)); + sunriseField.setReadOnly(true); + sunriseField.addClassName("sunrise"); + + TextField sunsetField = new TextField("Sunset"); + sunsetField.setValue(sunsetDateTime.format(formatter)); + sunsetField.setReadOnly(true); + sunsetField.addClassName("sunset"); + + TextField rainField = new TextField("Rain"); + rainField.setValue(rainSum + "mm"); + rainField.setReadOnly(true); + rainField.addClassName("rain"); + + TextField maxWindSpeedField = new TextField("Max Wind Speed"); + maxWindSpeedField.setValue(maxWindSpeed + "km/h"); + maxWindSpeedField.setReadOnly(true); + maxWindSpeedField.addClassName("max-wind-speed"); + + infoLayout.add(dayField, maxTemperatureField, minTemperatureField, sunriseField, sunsetField, rainField, maxWindSpeedField); + + add(infoLayout); + } +} diff --git a/src/main/java/com/eastnetic/application/views/WeatherView.java b/src/main/java/com/eastnetic/application/views/WeatherView.java index c52283c24..dd9f4afda 100644 --- a/src/main/java/com/eastnetic/application/views/WeatherView.java +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -1,6 +1,7 @@ package com.eastnetic.application.views; import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.weathers.entity.DailyWeather; import com.eastnetic.application.weathers.entity.WeatherData; import com.eastnetic.application.weathers.service.WeatherProviderService; import com.vaadin.flow.component.UI; @@ -55,5 +56,24 @@ private void updateWeatherData() { ); add(weatherCard); + + DailyWeather dailyWeather = weatherData.getDailyWeather(); + + int dayCount = dailyWeather.getTime().size(); + + for (int i=0; i Date: Thu, 10 Aug 2023 07:05:39 +0600 Subject: [PATCH 13/29] Add HourlyWeatherDialog to show hourly weather data of a day. --- .../application/views/DailyWeatherCard.java | 40 ++++++++++- .../views/HourlyWeatherDialog.java | 67 +++++++++++++++++++ .../application/views/WeatherView.java | 4 +- 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java diff --git a/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java b/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java index 5a5ce9aad..a3526a43d 100644 --- a/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java +++ b/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java @@ -1,6 +1,12 @@ package com.eastnetic.application.views; +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.weathers.service.WeatherProviderService; +import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.textfield.TextField; @@ -9,7 +15,15 @@ public class DailyWeatherCard extends Div { - public DailyWeatherCard(String day, double maxTemperature, double minTemperature, String sunrise, String sunset, double rainSum, double maxWindSpeed) { + public DailyWeatherCard(String day, + double maxTemperature, + double minTemperature, + String sunrise, + String sunset, + double rainSum, + double maxWindSpeed, + LocationDetails location, + WeatherProviderService weatherProviderService) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); LocalDateTime sunriseDateTime = LocalDateTime.parse(sunrise, DateTimeFormatter.ISO_DATE_TIME); @@ -56,5 +70,29 @@ public DailyWeatherCard(String day, double maxTemperature, double minTemperature infoLayout.add(dayField, maxTemperatureField, minTemperatureField, sunriseField, sunsetField, rainField, maxWindSpeedField); add(infoLayout); + + configureHourlyWeatherButton(day, location, weatherProviderService, infoLayout); + } + + private void configureHourlyWeatherButton(String day, + LocationDetails location, + WeatherProviderService weatherProviderService, + HorizontalLayout infoLayout) { + + Button showHourlyButton = new Button("Show Hourly Weather"); + showHourlyButton.setIcon(new Icon(VaadinIcon.ARROW_DOWN)); + + HorizontalLayout buttonLayout = new HorizontalLayout(); + buttonLayout.setAlignItems(FlexComponent.Alignment.CENTER); + buttonLayout.add(showHourlyButton); + + showHourlyButton.addClickListener(event -> showHourlyWeatherDialog(day, location, weatherProviderService)); + + add(infoLayout, buttonLayout); + } + + private void showHourlyWeatherDialog(String selectedDay, LocationDetails location, WeatherProviderService weatherProviderService) { + HourlyWeatherDialog dialog = new HourlyWeatherDialog(selectedDay, location, weatherProviderService); + dialog.open(); } } diff --git a/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java b/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java new file mode 100644 index 000000000..7eaff52c3 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java @@ -0,0 +1,67 @@ +package com.eastnetic.application.views; + +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.weathers.entity.WeatherData; +import com.eastnetic.application.weathers.service.WeatherProviderService; +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class HourlyWeatherDialog extends Dialog { + + private final WeatherProviderService weatherProviderService; + + public HourlyWeatherDialog(String day, + LocationDetails location, + WeatherProviderService weatherProviderService) { + + this.weatherProviderService = weatherProviderService; + + setCloseOnEsc(true); + setCloseOnOutsideClick(true); + + WeatherData weatherData = getHourlyWeatherData(day, location); + + setHourlyData(weatherData); + } + + private WeatherData getHourlyWeatherData(String day, LocationDetails locationDetails) { + + LocalDate date = LocalDate.parse(day, DateTimeFormatter.ISO_DATE); + + return weatherProviderService.getHourlyWeatherDataOfADay( + locationDetails.getLatitude(), locationDetails.getLongitude(), locationDetails.getTimezone(), date + ); + + } + + private void setHourlyData(WeatherData weatherData) { + + VerticalLayout hourlyWeatherLayout = new VerticalLayout(); + hourlyWeatherLayout.setSpacing(true); + + int dataSize = weatherData.getHourlyWeather().getTime().size(); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); + + for (int i=0; i Date: Thu, 10 Aug 2023 19:59:11 +0600 Subject: [PATCH 14/29] Update current weather card view. --- .../application/views/CurrentWeatherCard.java | 55 +++++++++---------- .../application/views/WeatherView.java | 22 ++++---- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java b/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java index 7ddd7877c..d431fc526 100644 --- a/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java +++ b/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java @@ -1,53 +1,50 @@ package com.eastnetic.application.views; +import com.eastnetic.application.weathers.entity.CurrentWeather; import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.H5; -import com.vaadin.flow.component.orderedlayout.FlexLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.spring.annotation.UIScope; +import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +@Component +@UIScope public class CurrentWeatherCard extends Div { - public CurrentWeatherCard(double temperature, double windSpeed, String location, String dateTime) { + private final H5 currentWeatherText = new H5("Current Weather"); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a"); - LocalDateTime localDateTime = LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_DATE_TIME); + private final TextField windSpeedField = new TextField("Wind Speed"); - addClassName("weather-card"); + private final TextField temperatureField = new TextField("Temperature"); - FlexLayout infoContainer = new FlexLayout(); - infoContainer.addClassName("info-container"); + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a"); - H3 locationText = new H3(location); - locationText.addClassName("location"); + public CurrentWeatherCard() { - H5 currentWeatherText = new H5("Current Weather"); - locationText.addClassName("current-weather"); + windSpeedField.setReadOnly(true); + temperatureField.setReadOnly(true); - FlexLayout dataContainer = new FlexLayout(); - dataContainer.addClassName("data-container"); + HorizontalLayout dataContainer = new HorizontalLayout(); + dataContainer.add(temperatureField, windSpeedField); - TextField temperatureField = new TextField("Temperature"); - temperatureField.setValue(temperature + " °C"); - temperatureField.setReadOnly(true); - temperatureField.addClassName("temperature"); + VerticalLayout infoContainer = new VerticalLayout(); + addClassName("info-container"); + infoContainer.add(currentWeatherText, dataContainer); - TextField windSpeedField = new TextField("Wind Speed"); - windSpeedField.setValue(windSpeed + " m/s"); - windSpeedField.setReadOnly(true); - windSpeedField.addClassName("wind-speed"); + add(infoContainer); + } - TextField dateTimeField = new TextField("Date and Time"); - dateTimeField.setValue(localDateTime.format(formatter)); - dateTimeField.setReadOnly(true); - dateTimeField.addClassName("date-time"); + public void showCurrentWeather(CurrentWeather currentWeather) { - dataContainer.add(temperatureField, windSpeedField, dateTimeField); + LocalDateTime localDateTime = LocalDateTime.parse(currentWeather.getTime(), DateTimeFormatter.ISO_DATE_TIME); - infoContainer.add(locationText, currentWeatherText, dataContainer); - add(infoContainer); + windSpeedField.setValue(currentWeather.getWindSpeed() + " m/s"); + temperatureField.setValue(currentWeather.getTemperature() + " °C"); + currentWeatherText.setText("Current Weather at " + localDateTime.format(formatter)); } } diff --git a/src/main/java/com/eastnetic/application/views/WeatherView.java b/src/main/java/com/eastnetic/application/views/WeatherView.java index 976a09dbe..001eee8d1 100644 --- a/src/main/java/com/eastnetic/application/views/WeatherView.java +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -5,6 +5,7 @@ import com.eastnetic.application.weathers.entity.WeatherData; import com.eastnetic.application.weathers.service.WeatherProviderService; import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterObserver; @@ -21,13 +22,19 @@ public class WeatherView extends VerticalLayout implements BeforeEnterObserver { private LocationDetails location; - private final WeatherProviderService weatherProviderService; + private final H3 locationName = new H3(); + + private final CurrentWeatherCard currentWeatherCard; - public WeatherView(WeatherProviderService weatherProviderService) { + private final WeatherProviderService weatherProviderService; - addClassName("weather-view"); + public WeatherView(CurrentWeatherCard currentWeatherCard, + WeatherProviderService weatherProviderService) { + this.currentWeatherCard = currentWeatherCard; this.weatherProviderService = weatherProviderService; + + add(locationName, currentWeatherCard); } @Override @@ -48,14 +55,9 @@ private void updateWeatherData() { location.getLatitude(), location.getLongitude(), location.getTimezone() ); - CurrentWeatherCard weatherCard = new CurrentWeatherCard( - weatherData.getCurrentWeather().getTemperature(), - weatherData.getCurrentWeather().getWindSpeed(), - location.locationFullName(), - weatherData.getCurrentWeather().getTime() - ); + locationName.setText(location.locationFullName()); - add(weatherCard); + currentWeatherCard.showCurrentWeather(weatherData.getCurrentWeather()); DailyWeather dailyWeather = weatherData.getDailyWeather(); From a8a0a036a2258b4f24a3b11516b3c81d64a1d3f4 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Thu, 10 Aug 2023 20:03:55 +0600 Subject: [PATCH 15/29] Add addItemClickListener instead of asSingleSelect().addValueChangeListener for location grid. --- .../com/eastnetic/application/views/LocationListComponent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eastnetic/application/views/LocationListComponent.java b/src/main/java/com/eastnetic/application/views/LocationListComponent.java index b4e23f59b..fc8b0c506 100644 --- a/src/main/java/com/eastnetic/application/views/LocationListComponent.java +++ b/src/main/java/com/eastnetic/application/views/LocationListComponent.java @@ -29,7 +29,7 @@ private void configureLocationGrid() { locationGrid.setVisible(false); locationGrid.getColumns().forEach(col -> col.setAutoWidth(true)); locationGrid.setColumns("name", "country", "latitude", "longitude", "elevation", "timezone"); - locationGrid.asSingleSelect().addValueChangeListener(event -> showWeatherDate(event.getValue())); + locationGrid.addItemClickListener(event -> showWeatherDate(event.getItem())); add(locationGrid); } From 67e4e3cacacaebdbf6e2594ee92536aaf3324495 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Thu, 10 Aug 2023 21:14:00 +0600 Subject: [PATCH 16/29] Update hourly weather view. --make grid list view. --- .../views/HourlyWeatherDialog.java | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java b/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java index 7eaff52c3..bc65f63a2 100644 --- a/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java +++ b/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java @@ -1,16 +1,17 @@ package com.eastnetic.application.views; import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.weathers.entity.HourlyWeather; import com.eastnetic.application.weathers.entity.WeatherData; import com.eastnetic.application.weathers.service.WeatherProviderService; -import com.vaadin.flow.component.Text; import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.grid.Grid; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; public class HourlyWeatherDialog extends Dialog { @@ -22,12 +23,13 @@ public HourlyWeatherDialog(String day, this.weatherProviderService = weatherProviderService; + setWidth("50%"); setCloseOnEsc(true); setCloseOnOutsideClick(true); WeatherData weatherData = getHourlyWeatherData(day, location); - setHourlyData(weatherData); + setHourlyData(weatherData.getHourlyWeather()); } private WeatherData getHourlyWeatherData(String day, LocationDetails locationDetails) { @@ -40,28 +42,80 @@ private WeatherData getHourlyWeatherData(String day, LocationDetails locationDet } - private void setHourlyData(WeatherData weatherData) { + private void setHourlyData(HourlyWeather hourlyWeather) { - VerticalLayout hourlyWeatherLayout = new VerticalLayout(); - hourlyWeatherLayout.setSpacing(true); - - int dataSize = weatherData.getHourlyWeather().getTime().size(); + List hourlyWeatherGridDataList = new ArrayList<>(); + int dataSize = hourlyWeather.getTime().size(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); for (int i=0; i hourlyWeatherGrid = new Grid<>(HourlyWeatherGridData.class); + hourlyWeatherGrid.setItems(hourlyWeatherGridDataList); + + add(hourlyWeatherGrid); + } + + public static class HourlyWeatherGridData { + + public String time; + + private String rain; + + private String windSpeed; + + private String temperature; + + public HourlyWeatherGridData(String time, String rain, String windSpeed, String temperature) { + this.time = time; + this.rain = rain; + this.windSpeed = windSpeed; + this.temperature = temperature; + } + + public String getTime() { + return time; } - add(hourlyWeatherLayout); + public void setTime(String time) { + this.time = time; + } + + public String getRain() { + return rain; + } + + public void setRain(String rain) { + this.rain = rain; + } + + public String getWindSpeed() { + return windSpeed; + } + + public void setWindSpeed(String windSpeed) { + this.windSpeed = windSpeed; + } + + public String getTemperature() { + return temperature; + } + + public void setTemperature(String temperature) { + this.temperature = temperature; + } } } From 38d2accc75470491c2e4f9b7eaffd4eb38d6196d Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Thu, 10 Aug 2023 23:53:53 +0600 Subject: [PATCH 17/29] Add exception handling for weather view. --- .../views/HourlyWeatherDialog.java | 55 +++++++++++------ .../application/views/WeatherView.java | 60 ++++++++++++------- .../exceptions/WeatherDataException.java | 13 ++++ .../OpenMeteoWeatherProviderServiceImpl.java | 17 ++++-- 4 files changed, 104 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/weathers/exceptions/WeatherDataException.java diff --git a/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java b/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java index bc65f63a2..393d0f213 100644 --- a/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java +++ b/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java @@ -3,9 +3,13 @@ import com.eastnetic.application.locations.entity.LocationDetails; import com.eastnetic.application.weathers.entity.HourlyWeather; import com.eastnetic.application.weathers.entity.WeatherData; +import com.eastnetic.application.weathers.exceptions.WeatherDataException; import com.eastnetic.application.weathers.service.WeatherProviderService; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.notification.Notification; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.time.LocalDate; import java.time.LocalDateTime; @@ -15,6 +19,8 @@ public class HourlyWeatherDialog extends Dialog { + private static final Logger LOGGER = LogManager.getLogger(HourlyWeatherDialog.class); + private final WeatherProviderService weatherProviderService; public HourlyWeatherDialog(String day, @@ -44,29 +50,44 @@ private WeatherData getHourlyWeatherData(String day, LocationDetails locationDet private void setHourlyData(HourlyWeather hourlyWeather) { - List hourlyWeatherGridDataList = new ArrayList<>(); + try { - int dataSize = hourlyWeather.getTime().size(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); + List hourlyWeatherGridDataList = new ArrayList<>(); - for (int i=0; i hourlyWeatherGrid = new Grid<>(HourlyWeatherGridData.class); + hourlyWeatherGrid.setItems(hourlyWeatherGridDataList); + + add(hourlyWeatherGrid); - Grid hourlyWeatherGrid = new Grid<>(HourlyWeatherGridData.class); - hourlyWeatherGrid.setItems(hourlyWeatherGridDataList); + } catch (WeatherDataException ex) { - add(hourlyWeatherGrid); + LOGGER.error("Error setting hourly data: {}", ex.getMessage(), ex); + + Notification.show(ex.getMessage(), 3000, Notification.Position.MIDDLE); + + } catch (Exception ex) { + + LOGGER.error("Error setting hourly data: {}", ex.getMessage(), ex); + + Notification.show("Error loading hourly weather data. Please try again later.", 3000, Notification.Position.MIDDLE); + } } public static class HourlyWeatherGridData { diff --git a/src/main/java/com/eastnetic/application/views/WeatherView.java b/src/main/java/com/eastnetic/application/views/WeatherView.java index 001eee8d1..abff8d81d 100644 --- a/src/main/java/com/eastnetic/application/views/WeatherView.java +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -3,15 +3,19 @@ import com.eastnetic.application.locations.entity.LocationDetails; import com.eastnetic.application.weathers.entity.DailyWeather; import com.eastnetic.application.weathers.entity.WeatherData; +import com.eastnetic.application.weathers.exceptions.WeatherDataException; import com.eastnetic.application.weathers.service.WeatherProviderService; import com.vaadin.flow.component.UI; import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.spring.annotation.UIScope; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; @Component @@ -20,6 +24,8 @@ @PageTitle("Weather Information") public class WeatherView extends VerticalLayout implements BeforeEnterObserver { + private static final Logger LOGGER = LogManager.getLogger(WeatherView.class); + private LocationDetails location; private final H3 locationName = new H3(); @@ -51,33 +57,47 @@ public void beforeEnter(BeforeEnterEvent event) { private void updateWeatherData() { - WeatherData weatherData = weatherProviderService.getDailyWeatherData( - location.getLatitude(), location.getLongitude(), location.getTimezone() - ); + try { + WeatherData weatherData = weatherProviderService.getDailyWeatherData( + location.getLatitude(), location.getLongitude(), location.getTimezone() + ); - locationName.setText(location.locationFullName()); + locationName.setText(location.locationFullName()); - currentWeatherCard.showCurrentWeather(weatherData.getCurrentWeather()); + currentWeatherCard.showCurrentWeather(weatherData.getCurrentWeather()); - DailyWeather dailyWeather = weatherData.getDailyWeather(); + DailyWeather dailyWeather = weatherData.getDailyWeather(); - int dayCount = dailyWeather.getTime().size(); + int dayCount = dailyWeather.getTime().size(); - for (int i=0; i Date: Thu, 10 Aug 2023 23:59:10 +0600 Subject: [PATCH 18/29] Update location exception handling. --- .../locations/exceptions/LocationDataException.java | 13 +++++++++++++ .../exceptions/LocationNotFoundException.java | 13 ------------- .../exceptions/LocationProviderException.java | 13 ------------- .../OpenMeteoLocationProviderServiceImpl.java | 7 +++---- .../application/views/LocationSearchView.java | 13 +++++++++++-- 5 files changed, 27 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/locations/exceptions/LocationDataException.java delete mode 100644 src/main/java/com/eastnetic/application/locations/exceptions/LocationNotFoundException.java delete mode 100644 src/main/java/com/eastnetic/application/locations/exceptions/LocationProviderException.java diff --git a/src/main/java/com/eastnetic/application/locations/exceptions/LocationDataException.java b/src/main/java/com/eastnetic/application/locations/exceptions/LocationDataException.java new file mode 100644 index 000000000..3f62e4d4f --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/exceptions/LocationDataException.java @@ -0,0 +1,13 @@ +package com.eastnetic.application.locations.exceptions; + + +public class LocationDataException extends RuntimeException { + + public LocationDataException(String message) { + super(message); + } + + public LocationDataException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/eastnetic/application/locations/exceptions/LocationNotFoundException.java b/src/main/java/com/eastnetic/application/locations/exceptions/LocationNotFoundException.java deleted file mode 100644 index 909e252bf..000000000 --- a/src/main/java/com/eastnetic/application/locations/exceptions/LocationNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.eastnetic.application.locations.exceptions; - - -public class LocationNotFoundException extends RuntimeException { - - public LocationNotFoundException(String message) { - super(message); - } - - public LocationNotFoundException(String message, Throwable cause) { - super(message, cause); - } -} \ No newline at end of file diff --git a/src/main/java/com/eastnetic/application/locations/exceptions/LocationProviderException.java b/src/main/java/com/eastnetic/application/locations/exceptions/LocationProviderException.java deleted file mode 100644 index 971fd64ac..000000000 --- a/src/main/java/com/eastnetic/application/locations/exceptions/LocationProviderException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.eastnetic.application.locations.exceptions; - - -public class LocationProviderException extends RuntimeException { - - public LocationProviderException(String message) { - super(message); - } - - public LocationProviderException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java index be60cf450..6061ab482 100644 --- a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java @@ -2,8 +2,7 @@ import com.eastnetic.application.locations.entity.LocationDetails; -import com.eastnetic.application.locations.exceptions.LocationNotFoundException; -import com.eastnetic.application.locations.exceptions.LocationProviderException; +import com.eastnetic.application.locations.exceptions.LocationDataException; import com.eastnetic.application.locations.response.OpenMeteoLocationApiResponse; import com.eastnetic.application.locations.service.LocationProviderService; import org.apache.logging.log4j.LogManager; @@ -47,13 +46,13 @@ public List getLocationDetails(String cityName, int count) { return response.getResults(); } - throw new LocationNotFoundException("Location not found for city: " + cityName); + throw new LocationDataException("Location not found for city: " + cityName); } catch (Exception ex) { LOGGER.error("Fetching location details from open-meteo api: City Name={}: Error", cityName, ex); - throw new LocationProviderException("Error fetching location details from open-meteo api.", ex); + throw new LocationDataException("Error fetching location details from open-meteo api.", ex); } } } diff --git a/src/main/java/com/eastnetic/application/views/LocationSearchView.java b/src/main/java/com/eastnetic/application/views/LocationSearchView.java index 9a8fbc81e..698e82b4d 100644 --- a/src/main/java/com/eastnetic/application/views/LocationSearchView.java +++ b/src/main/java/com/eastnetic/application/views/LocationSearchView.java @@ -1,7 +1,7 @@ package com.eastnetic.application.views; import com.eastnetic.application.locations.entity.LocationDetails; -import com.eastnetic.application.locations.exceptions.LocationProviderException; +import com.eastnetic.application.locations.exceptions.LocationDataException; import com.eastnetic.application.locations.service.LocationProviderService; import com.vaadin.flow.component.Key; import com.vaadin.flow.component.button.Button; @@ -13,6 +13,8 @@ import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteAlias; import com.vaadin.flow.spring.annotation.UIScope; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; import java.util.List; @@ -24,6 +26,8 @@ @PageTitle("Locations") public class LocationSearchView extends VerticalLayout { + private static final Logger LOGGER = LogManager.getLogger(WeatherView.class); + private TextField searchField = new TextField(); private Button searchButton = new Button("Search"); @@ -70,12 +74,17 @@ private void searchLocations() { locationListComponent.showLocationList(locations); - } catch (LocationProviderException ex) { + } catch (LocationDataException ex) { locationListComponent.setVisible(false); Notification.show(ex.getMessage(), 3000, Notification.Position.MIDDLE); + } catch (Exception ex) { + + LOGGER.error("Error fetching location data: {}", ex.getMessage(), ex); + + Notification.show("Error fetching location data. Please try again later.", 3000, Notification.Position.MIDDLE); } } } From 6047a3b13945dc818c35f124763cc61beff5886a Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Fri, 11 Aug 2023 02:45:03 +0600 Subject: [PATCH 19/29] Add favorite icon to WeatherView to mark or unmark a location as favorite. --- .../application/views/FavoriteIconButton.java | 48 +++++++++++++++++++ .../application/views/WeatherView.java | 7 ++- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eastnetic/application/views/FavoriteIconButton.java diff --git a/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java b/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java new file mode 100644 index 000000000..96e880e7e --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java @@ -0,0 +1,48 @@ +package com.eastnetic.application.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.icon.VaadinIcon; + +public class FavoriteIconButton extends Button { + + private boolean isFavorite = false; + + public FavoriteIconButton() { + setIcon(VaadinIcon.STAR.create()); + addClickListener(event -> toggleFavorite()); + updateFavoriteIconStyle(); + } + + private void toggleFavorite() { + + isFavorite = !isFavorite; + + if (isFavorite()) { + deleteFavorite(); + } else { + markFavorite(); + } + updateFavoriteIconStyle(); + } + + public boolean isFavorite() { + return isFavorite; + } + + public void markFavorite() { + + } + + public void deleteFavorite() { + + } + + private void updateFavoriteIconStyle() { + + boolean isFavorite = isFavorite(); + + getStyle().set("padding", "5px"); + getStyle().set("border", "2px solid black"); + getStyle().set("color", isFavorite ? "orange" : "black"); + } +} \ No newline at end of file diff --git a/src/main/java/com/eastnetic/application/views/WeatherView.java b/src/main/java/com/eastnetic/application/views/WeatherView.java index abff8d81d..038c1d429 100644 --- a/src/main/java/com/eastnetic/application/views/WeatherView.java +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -8,6 +8,7 @@ import com.vaadin.flow.component.UI; import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterObserver; @@ -40,7 +41,11 @@ public WeatherView(CurrentWeatherCard currentWeatherCard, this.currentWeatherCard = currentWeatherCard; this.weatherProviderService = weatherProviderService; - add(locationName, currentWeatherCard); + HorizontalLayout markLocation = new HorizontalLayout(); + FavoriteIconButton favoriteIcon = new FavoriteIconButton(); + markLocation.add(locationName, favoriteIcon); + + add(markLocation, currentWeatherCard); } @Override From fd56bcd540e9219a2c1f24e56ccb88b873167b6d Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Sat, 12 Aug 2023 18:48:27 +0600 Subject: [PATCH 20/29] Implement user login and registration view. --- pom.xml | 4 + .../application/config/SecurityConfig.java | 35 ++++++ .../application/views/LocationSearchView.java | 6 +- .../eastnetic/application/views/MainView.java | 106 ++++++++++++++++++ .../application/views/WeatherView.java | 2 + 5 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/config/SecurityConfig.java create mode 100644 src/main/java/com/eastnetic/application/views/MainView.java diff --git a/pom.xml b/pom.xml index 4fe0f5a1f..ecab1c57f 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,10 @@ vaadin-icons 3.0.1 + + org.springframework.boot + spring-boot-starter-security + diff --git a/src/main/java/com/eastnetic/application/config/SecurityConfig.java b/src/main/java/com/eastnetic/application/config/SecurityConfig.java new file mode 100644 index 000000000..838f24d3d --- /dev/null +++ b/src/main/java/com/eastnetic/application/config/SecurityConfig.java @@ -0,0 +1,35 @@ +package com.eastnetic.application.config; + +import com.eastnetic.application.views.MainView; +import com.vaadin.flow.spring.security.VaadinWebSecurity; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@EnableWebSecurity +@Configuration +public class SecurityConfig extends VaadinWebSecurity { + + @Override + protected void configure(HttpSecurity http) throws Exception { + + super.configure(http); + setLoginView(http, MainView.class); + } + + @Override + public void configure(WebSecurity web) throws Exception { + super.configure(web); + + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/eastnetic/application/views/LocationSearchView.java b/src/main/java/com/eastnetic/application/views/LocationSearchView.java index 698e82b4d..d1df3223c 100644 --- a/src/main/java/com/eastnetic/application/views/LocationSearchView.java +++ b/src/main/java/com/eastnetic/application/views/LocationSearchView.java @@ -11,7 +11,7 @@ import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouteAlias; +import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.spring.annotation.UIScope; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -19,14 +19,14 @@ import java.util.List; +@AnonymousAllowed @UIScope @Component @Route(value = "locations", layout = MainLayout.class) -@RouteAlias(value = "", layout = MainLayout.class) @PageTitle("Locations") public class LocationSearchView extends VerticalLayout { - private static final Logger LOGGER = LogManager.getLogger(WeatherView.class); + private static final Logger LOGGER = LogManager.getLogger(LocationSearchView.class); private TextField searchField = new TextField(); diff --git a/src/main/java/com/eastnetic/application/views/MainView.java b/src/main/java/com/eastnetic/application/views/MainView.java new file mode 100644 index 000000000..fc16b8060 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/MainView.java @@ -0,0 +1,106 @@ +package com.eastnetic.application.views; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.tabs.Tab; +import com.vaadin.flow.component.tabs.Tabs; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; +import com.vaadin.flow.server.VaadinSession; + +@Route("login") +@RouteAlias(value = "", layout = MainLayout.class) +@PageTitle("Login") +public class MainView extends VerticalLayout { + + private final TextField loginUsername = new TextField("Username"); + private final PasswordField loginPassword = new PasswordField("Password"); + + private final TextField regUsername = new TextField("Username"); + private final PasswordField regPassword = new PasswordField("Password"); + + Tabs tabs = new Tabs(); + Tab loginTab = new Tab("Login"); + Tab registrationTab = new Tab("Registration"); + + public MainView() { + + setAlignItems(Alignment.CENTER); + setJustifyContentMode(JustifyContentMode.CENTER); + + HorizontalLayout loginLayout = new HorizontalLayout(configureLoginForm()); + HorizontalLayout registrationLayout = new HorizontalLayout(configureRegistrationForm()); + + tabs.add(loginTab, registrationTab); + + tabs.addSelectedChangeListener(event -> { + + if (event.getSelectedTab() == loginTab) { + removeAll(); + add(tabs, loginLayout); + + } else if (event.getSelectedTab() == registrationTab) { + removeAll(); + add(tabs, registrationLayout); + } + }); + + add(tabs, loginLayout); + } + + private FormLayout configureLoginForm() { + + Button loginButton = new Button("Login", event -> login()); + + return new FormLayout(loginUsername, loginPassword, loginButton); + } + + private FormLayout configureRegistrationForm() { + + Button registerButton = new Button("Register", event -> register()); + + return new FormLayout(regUsername, regPassword, registerButton); + } + + private void register() { + + String username = regUsername.getValue(); + String password = regPassword.getValue(); + + if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { + return; + } + + tabs.setSelectedTab(loginTab); + + Notification.show("Registration success.", 1000, Notification.Position.TOP_CENTER); + } + + private void login() { + + String username = loginUsername.getValue(); + String password = loginPassword.getValue(); + + boolean loginSuccessful = true; //Todo call login service. + + if (loginSuccessful) { + + VaadinSession.getCurrent().setAttribute("username", username); + + Notification.show("Login successful.", 1000, Notification.Position.TOP_CENTER); + + UI.getCurrent().navigate(LocationSearchView.class); + + } else { + Notification.show("Login failed: Invalid credentials.", 3000, Notification.Position.TOP_CENTER); + } + } +} + diff --git a/src/main/java/com/eastnetic/application/views/WeatherView.java b/src/main/java/com/eastnetic/application/views/WeatherView.java index 038c1d429..417e23a65 100644 --- a/src/main/java/com/eastnetic/application/views/WeatherView.java +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -14,11 +14,13 @@ import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.spring.annotation.UIScope; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; +@AnonymousAllowed @Component @UIScope @Route(value = "weather", layout = MainLayout.class) From 5e1282cc68b6f32c8562bdb023bca342c1227ecb Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Sat, 12 Aug 2023 20:06:49 +0600 Subject: [PATCH 21/29] Implement user login and registration service. --Add user service to register user. --Add AuthService to check authentication. --Add exception handling. --- pom.xml | 9 +++ .../authentication/AuthService.java | 58 ++++++++++++++++ .../application/users/dto/UserDto.java | 39 +++++++++++ .../application/users/entity/User.java | 49 ++++++++++++++ .../users/exceptions/UserException.java | 12 ++++ .../users/repository/UserRepository.java | 11 ++++ .../users/service/UserService.java | 11 ++++ .../users/serviceImpl/UserServiceImpl.java | 59 +++++++++++++++++ .../eastnetic/application/views/MainView.java | 66 ++++++++++++++++--- src/main/resources/application.properties | 11 +++- 10 files changed, 314 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/authentication/AuthService.java create mode 100644 src/main/java/com/eastnetic/application/users/dto/UserDto.java create mode 100644 src/main/java/com/eastnetic/application/users/entity/User.java create mode 100644 src/main/java/com/eastnetic/application/users/exceptions/UserException.java create mode 100644 src/main/java/com/eastnetic/application/users/repository/UserRepository.java create mode 100644 src/main/java/com/eastnetic/application/users/service/UserService.java create mode 100644 src/main/java/com/eastnetic/application/users/serviceImpl/UserServiceImpl.java diff --git a/pom.xml b/pom.xml index ecab1c57f..582200674 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,15 @@ org.springframework.boot spring-boot-starter-security + + org.postgresql + postgresql + 42.2.27 + + + org.springframework.boot + spring-boot-starter-data-jpa + diff --git a/src/main/java/com/eastnetic/application/authentication/AuthService.java b/src/main/java/com/eastnetic/application/authentication/AuthService.java new file mode 100644 index 000000000..8ea0afc92 --- /dev/null +++ b/src/main/java/com/eastnetic/application/authentication/AuthService.java @@ -0,0 +1,58 @@ +package com.eastnetic.application.authentication; + +import com.eastnetic.application.users.entity.User; +import com.eastnetic.application.users.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + +@Service +public class AuthService implements UserDetailsService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Autowired + public AuthService(UserRepository userRepository, + PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + User user = userRepository.findByUsername(username); + + if (user == null) { + throw new UsernameNotFoundException("Username not found."); + } + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), user.getPassword(), new ArrayList<>() + ); + } + + public boolean isAuthenticatedUser(String username, String password) { + + UserDetails userDetails = loadUserByUsername(username); + + if (passwordEncoder.matches(password, userDetails.getPassword())) { + + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()) + ); + + return true; + } + + return false; + } +} diff --git a/src/main/java/com/eastnetic/application/users/dto/UserDto.java b/src/main/java/com/eastnetic/application/users/dto/UserDto.java new file mode 100644 index 000000000..d301e5237 --- /dev/null +++ b/src/main/java/com/eastnetic/application/users/dto/UserDto.java @@ -0,0 +1,39 @@ +package com.eastnetic.application.users.dto; + +public class UserDto { + + private Long id; + + private String username; + + private String password; + + public UserDto(String username, String password) { + this.username = username; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/eastnetic/application/users/entity/User.java b/src/main/java/com/eastnetic/application/users/entity/User.java new file mode 100644 index 000000000..115029b7f --- /dev/null +++ b/src/main/java/com/eastnetic/application/users/entity/User.java @@ -0,0 +1,49 @@ +package com.eastnetic.application.users.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name="users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "username", columnDefinition = "VARCHAR(100)", nullable=false, unique = true) + private String username; + + @Column(name = "password", columnDefinition = "VARCHAR(1000)", nullable=false) + private String password; + + public User() {} + + public User(String username, String password) { + this.username = username; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/eastnetic/application/users/exceptions/UserException.java b/src/main/java/com/eastnetic/application/users/exceptions/UserException.java new file mode 100644 index 000000000..a5bde8491 --- /dev/null +++ b/src/main/java/com/eastnetic/application/users/exceptions/UserException.java @@ -0,0 +1,12 @@ +package com.eastnetic.application.users.exceptions; + +public class UserException extends RuntimeException { + + public UserException(String message) { + super(message); + } + + public UserException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/eastnetic/application/users/repository/UserRepository.java b/src/main/java/com/eastnetic/application/users/repository/UserRepository.java new file mode 100644 index 000000000..03b92b62d --- /dev/null +++ b/src/main/java/com/eastnetic/application/users/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.eastnetic.application.users.repository; + +import com.eastnetic.application.users.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + + User findByUsername(String username); +} diff --git a/src/main/java/com/eastnetic/application/users/service/UserService.java b/src/main/java/com/eastnetic/application/users/service/UserService.java new file mode 100644 index 000000000..6fdacae54 --- /dev/null +++ b/src/main/java/com/eastnetic/application/users/service/UserService.java @@ -0,0 +1,11 @@ +package com.eastnetic.application.users.service; + +import com.eastnetic.application.users.dto.UserDto; +import com.eastnetic.application.users.entity.User; + +public interface UserService { + + void registerUser(UserDto userDto) throws Exception; + + User findUserByUsername(String email); +} diff --git a/src/main/java/com/eastnetic/application/users/serviceImpl/UserServiceImpl.java b/src/main/java/com/eastnetic/application/users/serviceImpl/UserServiceImpl.java new file mode 100644 index 000000000..c682cb227 --- /dev/null +++ b/src/main/java/com/eastnetic/application/users/serviceImpl/UserServiceImpl.java @@ -0,0 +1,59 @@ +package com.eastnetic.application.users.serviceImpl; + +import com.eastnetic.application.users.dto.UserDto; +import com.eastnetic.application.users.entity.User; +import com.eastnetic.application.users.exceptions.UserException; +import com.eastnetic.application.users.repository.UserRepository; +import com.eastnetic.application.users.service.UserService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +public class UserServiceImpl implements UserService { + + private static final Logger LOGGER = LogManager.getLogger(UserServiceImpl.class); + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) { + + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public void registerUser(UserDto userDto) throws Exception { + + User user = userRepository.findByUsername(userDto.getUsername()); + + if (user != null) { + throw new UserException("This username already registered."); + } + + user = new User( + userDto.getUsername(), + passwordEncoder.encode(userDto.getPassword()) + ); + + try { + + userRepository.save(user); + + LOGGER.info("Registration success."); + + } catch (Exception e) { + + LOGGER.error("Registration error : ", e); + + throw e; + } + } + + @Override + public User findUserByUsername(String username) { + return userRepository.findByUsername(username); + } +} diff --git a/src/main/java/com/eastnetic/application/views/MainView.java b/src/main/java/com/eastnetic/application/views/MainView.java index fc16b8060..e68ce7425 100644 --- a/src/main/java/com/eastnetic/application/views/MainView.java +++ b/src/main/java/com/eastnetic/application/views/MainView.java @@ -1,5 +1,9 @@ package com.eastnetic.application.views; +import com.eastnetic.application.authentication.AuthService; +import com.eastnetic.application.users.dto.UserDto; +import com.eastnetic.application.users.exceptions.UserException; +import com.eastnetic.application.users.service.UserService; import com.vaadin.flow.component.UI; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.formlayout.FormLayout; @@ -14,12 +18,20 @@ import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteAlias; import com.vaadin.flow.server.VaadinSession; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.security.core.userdetails.UsernameNotFoundException; @Route("login") @RouteAlias(value = "", layout = MainLayout.class) @PageTitle("Login") public class MainView extends VerticalLayout { + private static final Logger LOGGER = LogManager.getLogger(MainView.class); + + private final AuthService authService; + private final UserService userService; + private final TextField loginUsername = new TextField("Username"); private final PasswordField loginPassword = new PasswordField("Password"); @@ -30,7 +42,10 @@ public class MainView extends VerticalLayout { Tab loginTab = new Tab("Login"); Tab registrationTab = new Tab("Registration"); - public MainView() { + public MainView(AuthService authService, UserService userService) { + + this.authService = authService; + this.userService = userService; setAlignItems(Alignment.CENTER); setJustifyContentMode(JustifyContentMode.CENTER); @@ -78,9 +93,27 @@ private void register() { return; } - tabs.setSelectedTab(loginTab); + try { + + UserDto userDto = new UserDto(username, password); + + userService.registerUser(userDto); + tabs.setSelectedTab(loginTab); + + Notification.show("Registration success.", 1000, Notification.Position.TOP_CENTER); + + } catch (UserException e) { + + LOGGER.error("User registration failed: Username={}", username, e); + + Notification.show("Registration failed." + e.getMessage(), 3000, Notification.Position.TOP_CENTER); + + } catch (Exception e) { - Notification.show("Registration success.", 1000, Notification.Position.TOP_CENTER); + LOGGER.error("User registration failed: Username={}", username, e); + + Notification.show("Registration failed. Please try again later.", 3000, Notification.Position.TOP_CENTER); + } } private void login() { @@ -88,18 +121,31 @@ private void login() { String username = loginUsername.getValue(); String password = loginPassword.getValue(); - boolean loginSuccessful = true; //Todo call login service. + try { + + boolean loginSuccessful = authService.isAuthenticatedUser(username, password); + + if (loginSuccessful) { + + VaadinSession.getCurrent().setAttribute("username", username); + + Notification.show("Login successful.", 1000, Notification.Position.TOP_CENTER); + + UI.getCurrent().navigate(LocationSearchView.class); + + } else { + Notification.show("Login failed: Invalid credentials.", 3000, Notification.Position.TOP_CENTER); + } - if (loginSuccessful) { + } catch (UsernameNotFoundException e) { - VaadinSession.getCurrent().setAttribute("username", username); + Notification.show("Login failed: " + e.getMessage(), 3000, Notification.Position.TOP_CENTER); - Notification.show("Login successful.", 1000, Notification.Position.TOP_CENTER); + } catch (Exception e) { - UI.getCurrent().navigate(LocationSearchView.class); + LOGGER.error("User login failed: Username={}", username, e); - } else { - Notification.show("Login failed: Invalid credentials.", 3000, Notification.Position.TOP_CENTER); + Notification.show("Login failed. Please try again later.", 3000, Notification.Position.TOP_CENTER); } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 09c7b12b3..91a83e29c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,4 +7,13 @@ vaadin.launch-browser=true # To improve the performance during development. # For more information https://vaadin.com/docs/flow/spring/tutorial-spring-configuration.html#special-configuration-parameters vaadin.whitelisted-packages = com.vaadin,org.vaadin,dev.hilla,com.example.application -spring.jpa.defer-datasource-initialization = true +#spring.jpa.defer-datasource-initialization = true + +spring.jpa.hibernate.ddl-auto=update +logging.level.org.springframework.data.jpa=DEBUG + +spring.datasource.url=jdbc:postgresql://localhost:5432/weather_app +spring.datasource.username=weather_app +spring.datasource.password=weather_app +spring.datasource.driver-class-name=org.postgresql.Driver + From d29e6b5efd5f68d228eafbd0bcf170df39ff7e04 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Sat, 12 Aug 2023 22:11:56 +0600 Subject: [PATCH 22/29] Add reference source in location details. --change id to reference id. --set source from open-meteo service provider. --- .../locations/entity/LocationDetails.java | 20 ++++++++++++++----- .../OpenMeteoLocationProviderServiceImpl.java | 7 ++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java index 111378b01..32625880f 100644 --- a/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java +++ b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java @@ -6,7 +6,7 @@ public class LocationDetails { @JsonProperty("id") - private long id; + private String referenceId; @JsonProperty("name") private String name; @@ -32,12 +32,14 @@ public class LocationDetails { @JsonProperty("admin1") private String admin1; - public long getId() { - return id; + private String referenceSource; + + public String getReferenceId() { + return referenceId; } - public void setId(long id) { - this.id = id; + public void setReferenceId(String referenceId) { + this.referenceId = referenceId; } public String getName() { @@ -104,6 +106,14 @@ public void setAdmin1(String admin1) { this.admin1 = admin1; } + public String getReferenceSource() { + return referenceSource; + } + + public void setReferenceSource(String referenceSource) { + this.referenceSource = referenceSource; + } + public String locationFullName() { if (getAdmin1() == null || getAdmin1().trim().isEmpty()) { diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java index 6061ab482..a3966629d 100644 --- a/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java @@ -13,6 +13,7 @@ import org.springframework.web.client.RestTemplate; import java.util.List; +import java.util.stream.Collectors; @Service @CacheConfig(cacheNames = "locationDetailsCache") @@ -20,6 +21,8 @@ public class OpenMeteoLocationProviderServiceImpl implements LocationProviderSer private static final Logger LOGGER = LogManager.getLogger(OpenMeteoLocationProviderServiceImpl.class); + private static final String SOURCE_NAME = "open-meteo"; + private static final String API_BASE_URL = "https://geocoding-api.open-meteo.com/v1/search?name="; private final RestTemplate restTemplate; @@ -43,7 +46,9 @@ public List getLocationDetails(String cityName, int count) { LOGGER.info("Fetching location details from open-meteo api: City Name={}: SUCCESS", cityName); - return response.getResults(); + return response.getResults().stream() + .peek(locationDetail -> locationDetail.setReferenceSource(SOURCE_NAME)) + .collect(Collectors.toList()); } throw new LocationDataException("Location not found for city: " + cityName); From cd07d13be268927317382d98be12275149982bbc Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Sat, 12 Aug 2023 22:24:02 +0600 Subject: [PATCH 23/29] Implement locations crud operations. --add location entity. --- .../locations/entity/Location.java | 155 ++++++++++++++++++ .../repository/LocationRepository.java | 12 ++ .../locations/service/LocationService.java | 15 ++ .../serviceImpl/LocationServiceImpl.java | 99 +++++++++++ 4 files changed, 281 insertions(+) create mode 100644 src/main/java/com/eastnetic/application/locations/entity/Location.java create mode 100644 src/main/java/com/eastnetic/application/locations/repository/LocationRepository.java create mode 100644 src/main/java/com/eastnetic/application/locations/service/LocationService.java create mode 100644 src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java diff --git a/src/main/java/com/eastnetic/application/locations/entity/Location.java b/src/main/java/com/eastnetic/application/locations/entity/Location.java new file mode 100644 index 000000000..a81eaac42 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/entity/Location.java @@ -0,0 +1,155 @@ +package com.eastnetic.application.locations.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "locations", uniqueConstraints = @UniqueConstraint(columnNames={"reference_id", "reference_source"})) +public class Location { + + @Id + @GeneratedValue + private long id; + + @Column(name = "reference_id", columnDefinition = "VARCHAR(100)") + private String referenceId; + + @Column(name = "reference_source", columnDefinition = "VARCHAR(100)") + private String referenceSource; + + @Column(name = "name", columnDefinition = "VARCHAR(255)", nullable = false) + private String name; + + @Column(name = "latitude", nullable = false) + private double latitude; + + @Column(name = "longitude", nullable = false) + private double longitude; + + @Column(name = "elevation", nullable = false) + private double elevation; + + @Column(name = "country_code", nullable = false) + private String countryCode; + + @Column(name = "timezone", columnDefinition = "VARCHAR(100)", nullable = false) + private String timezone; + + @Column(name = "country", columnDefinition = "VARCHAR(100)", nullable = false) + private String country; + + @Column(name = "admin_1", columnDefinition = "VARCHAR(255)", nullable = false) + private String admin1; + + public Location() {} + + public Location(String referenceId, + String referenceSource, + String name, + double latitude, + double longitude, + double elevation, + String countryCode, + String timezone, + String country, + String admin1) { + + this.referenceId = referenceId; + this.referenceSource = referenceSource; + this.name = name; + this.latitude = latitude; + this.longitude = longitude; + this.elevation = elevation; + this.countryCode = countryCode; + this.timezone = timezone; + this.country = country; + this.admin1 = admin1; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getReferenceId() { + return referenceId; + } + + public void setReferenceId(String referenceId) { + this.referenceId = referenceId; + } + + public String getReferenceSource() { + return referenceSource; + } + + public void setReferenceSource(String referenceSource) { + this.referenceSource = referenceSource; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public double getElevation() { + return elevation; + } + + public void setElevation(double elevation) { + this.elevation = elevation; + } + + public String getCountryCode() { + return countryCode; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getAdmin1() { + return admin1; + } + + public void setAdmin1(String admin1) { + this.admin1 = admin1; + } +} diff --git a/src/main/java/com/eastnetic/application/locations/repository/LocationRepository.java b/src/main/java/com/eastnetic/application/locations/repository/LocationRepository.java new file mode 100644 index 000000000..98b7ea84e --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/repository/LocationRepository.java @@ -0,0 +1,12 @@ +package com.eastnetic.application.locations.repository; + +import com.eastnetic.application.locations.entity.Location; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LocationRepository extends JpaRepository { + + Location findByReferenceIdAndReferenceSource(String id, String source); + +} diff --git a/src/main/java/com/eastnetic/application/locations/service/LocationService.java b/src/main/java/com/eastnetic/application/locations/service/LocationService.java new file mode 100644 index 000000000..d3425813e --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/service/LocationService.java @@ -0,0 +1,15 @@ +package com.eastnetic.application.locations.service; + +import com.eastnetic.application.locations.entity.Location; +import com.eastnetic.application.locations.entity.LocationDetails; + +public interface LocationService { + + Location getLocationById(long id); + + void addLocation(LocationDetails locationDetails); + + void deleteLocation(LocationDetails locationDetails); + + Location getLocationByReferenceIdAndSource(String id, String source); +} diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java new file mode 100644 index 000000000..d3b1b9c41 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java @@ -0,0 +1,99 @@ +package com.eastnetic.application.locations.serviceImpl; + +import com.eastnetic.application.locations.entity.Location; +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.locations.repository.LocationRepository; +import com.eastnetic.application.locations.service.LocationService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Service; + +@Service +public class LocationServiceImpl implements LocationService { + + private static final Logger LOGGER = LogManager.getLogger(LocationServiceImpl.class); + + private final LocationRepository locationRepository; + + public LocationServiceImpl(LocationRepository locationRepository) { + this.locationRepository = locationRepository; + } + + @Override + public Location getLocationById(long id) { + return locationRepository.findById(id).orElse(null); + } + + @Override + public void addLocation(LocationDetails locationDetails) { + + Location location = getLocationByReferenceIdAndSource( + locationDetails.getReferenceId(), + locationDetails.getReferenceSource() + ); + + if (location != null) { + return; + } + + location = convertDtoToEntity(locationDetails); + + try { + + locationRepository.save(location); + + } catch (Exception e) { + + LOGGER.error("Saving location failed. Reference ID={}, Reference Source={}", + locationDetails.getReferenceId(), locationDetails.getReferenceSource(), e + ); + + throw e; + } + } + + @Override + public void deleteLocation(LocationDetails locationDetails) { + + Location location = getLocationByReferenceIdAndSource( + locationDetails.getReferenceId(), + locationDetails.getReferenceSource() + ); + + if (location == null) { + return; + } + + try { + + locationRepository.delete(location); + + } catch (Exception e) { + + LOGGER.error("Delete location failed. Reference ID={}, Reference Source={}", + locationDetails.getReferenceId(), locationDetails.getReferenceSource(), e + ); + } + } + + @Override + public Location getLocationByReferenceIdAndSource(String id, String source) { + return locationRepository.findByReferenceIdAndReferenceSource(id, source); + } + + private Location convertDtoToEntity(LocationDetails locationDetails) { + + return new Location( + locationDetails.getReferenceId(), + locationDetails.getName(), + locationDetails.getName(), + locationDetails.getLatitude(), + locationDetails.getLongitude(), + locationDetails.getElevation(), + locationDetails.getCountryCode(), + locationDetails.getTimezone(), + locationDetails.getCountry(), + locationDetails.getAdmin1() + ); + } +} From 135f8ac954d2f8599ec492faefdd478fce47e9c3 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Sun, 13 Aug 2023 00:03:38 +0600 Subject: [PATCH 24/29] Update add and delete locations method signature. --- .../locations/service/LocationService.java | 4 ++-- .../serviceImpl/LocationServiceImpl.java | 23 +++++++------------ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/eastnetic/application/locations/service/LocationService.java b/src/main/java/com/eastnetic/application/locations/service/LocationService.java index d3425813e..31c8aac66 100644 --- a/src/main/java/com/eastnetic/application/locations/service/LocationService.java +++ b/src/main/java/com/eastnetic/application/locations/service/LocationService.java @@ -7,9 +7,9 @@ public interface LocationService { Location getLocationById(long id); - void addLocation(LocationDetails locationDetails); + Location addLocation(LocationDetails locationDetails); - void deleteLocation(LocationDetails locationDetails); + void deleteLocation(Location location); Location getLocationByReferenceIdAndSource(String id, String source); } diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java index d3b1b9c41..e900a4fcc 100644 --- a/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java @@ -25,7 +25,7 @@ public Location getLocationById(long id) { } @Override - public void addLocation(LocationDetails locationDetails) { + public Location addLocation(LocationDetails locationDetails) { Location location = getLocationByReferenceIdAndSource( locationDetails.getReferenceId(), @@ -33,14 +33,16 @@ public void addLocation(LocationDetails locationDetails) { ); if (location != null) { - return; + return null; } location = convertDtoToEntity(locationDetails); try { - locationRepository.save(location); + location = locationRepository.save(location); + + return location; } catch (Exception e) { @@ -53,16 +55,7 @@ public void addLocation(LocationDetails locationDetails) { } @Override - public void deleteLocation(LocationDetails locationDetails) { - - Location location = getLocationByReferenceIdAndSource( - locationDetails.getReferenceId(), - locationDetails.getReferenceSource() - ); - - if (location == null) { - return; - } + public void deleteLocation(Location location) { try { @@ -71,7 +64,7 @@ public void deleteLocation(LocationDetails locationDetails) { } catch (Exception e) { LOGGER.error("Delete location failed. Reference ID={}, Reference Source={}", - locationDetails.getReferenceId(), locationDetails.getReferenceSource(), e + location.getReferenceId(), location.getReferenceSource(), e ); } } @@ -85,7 +78,7 @@ private Location convertDtoToEntity(LocationDetails locationDetails) { return new Location( locationDetails.getReferenceId(), - locationDetails.getName(), + locationDetails.getReferenceSource(), locationDetails.getName(), locationDetails.getLatitude(), locationDetails.getLongitude(), From a6fa34da466438da4d8e83e6769e74afd86151ac Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Sun, 13 Aug 2023 00:16:45 +0600 Subject: [PATCH 25/29] Implement favourite locations crud operations. --Set favourite icon while entering weather view. --- .../locations/entity/FavouriteLocation.java | 52 ++++++ .../FavouriteLocationException.java | 12 ++ .../FavouriteLocationRepository.java | 15 ++ .../service/FavouriteLocationService.java | 16 ++ .../FavouriteLocationServiceImpl.java | 171 ++++++++++++++++++ .../application/views/FavoriteIconButton.java | 74 +++++++- .../application/views/WeatherView.java | 11 +- 7 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java create mode 100644 src/main/java/com/eastnetic/application/locations/exceptions/FavouriteLocationException.java create mode 100644 src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java create mode 100644 src/main/java/com/eastnetic/application/locations/service/FavouriteLocationService.java create mode 100644 src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java diff --git a/src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java b/src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java new file mode 100644 index 000000000..518febf4d --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java @@ -0,0 +1,52 @@ +package com.eastnetic.application.locations.entity; + +import com.eastnetic.application.users.entity.User; +import jakarta.persistence.*; + +@Entity +@Table(name = "favourite_locations") +public class FavouriteLocation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) + @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false) + private User user; + + @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) + @JoinColumn(name = "location_id", referencedColumnName = "id", nullable = false) + private Location location; + + public FavouriteLocation() {} + + public FavouriteLocation(User user, Location location) { + this.user = user; + this.location = location; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } +} diff --git a/src/main/java/com/eastnetic/application/locations/exceptions/FavouriteLocationException.java b/src/main/java/com/eastnetic/application/locations/exceptions/FavouriteLocationException.java new file mode 100644 index 000000000..159b8d4a0 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/exceptions/FavouriteLocationException.java @@ -0,0 +1,12 @@ +package com.eastnetic.application.locations.exceptions; + +public class FavouriteLocationException extends RuntimeException { + + public FavouriteLocationException(String message) { + super(message); + } + + public FavouriteLocationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java b/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java new file mode 100644 index 000000000..3388ddb6b --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java @@ -0,0 +1,15 @@ +package com.eastnetic.application.locations.repository; + +import com.eastnetic.application.locations.entity.FavouriteLocation; +import com.eastnetic.application.locations.entity.Location; +import com.eastnetic.application.users.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FavouriteLocationRepository extends JpaRepository { + + FavouriteLocation findFirstByLocation(Location location); + + FavouriteLocation findByUserAndLocation(User user, Location location); +} diff --git a/src/main/java/com/eastnetic/application/locations/service/FavouriteLocationService.java b/src/main/java/com/eastnetic/application/locations/service/FavouriteLocationService.java new file mode 100644 index 000000000..95fba2736 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/service/FavouriteLocationService.java @@ -0,0 +1,16 @@ +package com.eastnetic.application.locations.service; + +import com.eastnetic.application.locations.entity.LocationDetails; + +import java.util.List; + +public interface FavouriteLocationService { + + void addFavouriteLocation(String username, LocationDetails locationDetails); + + void deleteFavouriteLocation(String username, LocationDetails locationDetails); + + List getFavouriteLocations(String username); + + boolean isFavouriteLocation(String username, LocationDetails locationDetails); +} diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java new file mode 100644 index 000000000..913d57873 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java @@ -0,0 +1,171 @@ +package com.eastnetic.application.locations.serviceImpl; + +import com.eastnetic.application.locations.entity.FavouriteLocation; +import com.eastnetic.application.locations.entity.Location; +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.locations.exceptions.FavouriteLocationException; +import com.eastnetic.application.locations.repository.FavouriteLocationRepository; +import com.eastnetic.application.locations.service.FavouriteLocationService; +import com.eastnetic.application.locations.service.LocationService; +import com.eastnetic.application.users.entity.User; +import com.eastnetic.application.users.exceptions.UserException; +import com.eastnetic.application.users.service.UserService; +import jakarta.transaction.Transactional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class FavouriteLocationServiceImpl implements FavouriteLocationService { + + private static final Logger LOGGER = LogManager.getLogger(FavouriteLocationServiceImpl.class); + + private final UserService userService; + + private final LocationService locationService; + + private final FavouriteLocationRepository favouriteLocationRepository; + + public FavouriteLocationServiceImpl(UserService userService, + LocationService locationService, + FavouriteLocationRepository favouriteLocationRepository) { + this.userService = userService; + this.locationService = locationService; + this.favouriteLocationRepository = favouriteLocationRepository; + } + + @Transactional + @Override + public void addFavouriteLocation(String username, LocationDetails locationDetails) { + + LOGGER.info("Add favourite location: username={}, Reference ID={}, Reference Source={}: START", + username, locationDetails.getReferenceId(), locationDetails.getReferenceSource() + ); + + User user = userService.findUserByUsername(username); + + if (user == null) { + throw new FavouriteLocationException("User not found."); + } + + Location location = locationService.getLocationByReferenceIdAndSource( + locationDetails.getReferenceId(), locationDetails.getReferenceSource() + ); + + if (location == null) { + location = locationService.addLocation(locationDetails); + } + + FavouriteLocation favouriteLocation = favouriteLocationRepository.findByUserAndLocation(user, location); + + if (favouriteLocation != null) { + throw new FavouriteLocationException("This location already marked."); + } + + favouriteLocation = new FavouriteLocation(user, location); + + try { + + favouriteLocationRepository.save(favouriteLocation); + + LOGGER.info("Add favourite location: username={}, Reference ID={}, Reference Source={}: SUCCESS", + username, locationDetails.getReferenceId(), locationDetails.getReferenceSource() + ); + + } catch (Exception e) { + + LOGGER.error("Add favourite location: username={}, Reference ID={}, Reference Source={}: FAILED", + username, locationDetails.getReferenceId(), locationDetails.getReferenceSource(), e + ); + + throw new FavouriteLocationException("Marked location error. Please try again later."); + } + } + + @Override + public void deleteFavouriteLocation(String username, LocationDetails locationDetails) { + + LOGGER.info("Delete favourite location: username={}, Reference ID={}, Reference Source={}: START", + username, locationDetails.getReferenceId(), locationDetails.getReferenceSource() + ); + + User user = userService.findUserByUsername(username); + + if (user == null) { + throw new FavouriteLocationException("User not found."); + } + + Location location = locationService.getLocationByReferenceIdAndSource( + locationDetails.getReferenceId(), locationDetails.getReferenceSource() + ); + + FavouriteLocation favouriteLocation = favouriteLocationRepository.findByUserAndLocation(user, location); + + if (favouriteLocation == null) { + throw new FavouriteLocationException("This location is not marked."); + } + + try { + + favouriteLocationRepository.delete(favouriteLocation); + + deleteLocationIfNotInUsed(location); + + LOGGER.info("Delete favourite location: username={}, Reference ID={}, Reference Source={}: SUCCESS", + username, locationDetails.getReferenceId(), locationDetails.getReferenceSource() + ); + + } catch (Exception e) { + + LOGGER.error("Delete favourite location: username={}, Reference ID={}, Reference Source={}: FAILED", + username, locationDetails.getReferenceId(), locationDetails.getReferenceSource(), e + ); + + throw new FavouriteLocationException("Delete marked location error. Please try again later."); + } + } + + @Override + public List getFavouriteLocations(String username) { + return null; + } + + @Override + public boolean isFavouriteLocation(String username, LocationDetails locationDetails) { + + FavouriteLocation favouriteLocation = getFavouriteLocation(username, locationDetails); + + return favouriteLocation != null; + } + + private FavouriteLocation getFavouriteLocation(String username, LocationDetails locationDetails) { + + User user = userService.findUserByUsername(username); + + if (user == null) { + throw new UserException("User not found."); + } + + Location location = locationService.getLocationByReferenceIdAndSource( + locationDetails.getReferenceId(), locationDetails.getReferenceSource() + ); + + if (location == null) { + return null; + } + + return favouriteLocationRepository.findByUserAndLocation(user, location); + } + + private void deleteLocationIfNotInUsed(Location location) { + + FavouriteLocation favouriteLocation = favouriteLocationRepository.findFirstByLocation(location); + + if (favouriteLocation == null) { + + locationService.deleteLocation(location); + } + } +} diff --git a/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java b/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java index 96e880e7e..198eebf15 100644 --- a/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java +++ b/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java @@ -1,40 +1,102 @@ package com.eastnetic.application.views; +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.locations.exceptions.FavouriteLocationException; +import com.eastnetic.application.locations.service.FavouriteLocationService; +import com.vaadin.flow.component.UI; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.notification.Notification; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; +@Component public class FavoriteIconButton extends Button { + private static final Logger LOGGER = LogManager.getLogger(FavoriteIconButton.class); + + private String username; + + private LocationDetails location; + private boolean isFavorite = false; - public FavoriteIconButton() { + private final FavouriteLocationService favouriteLocationService; + + public FavoriteIconButton(FavouriteLocationService favouriteLocationService) { + + this.favouriteLocationService = favouriteLocationService; + setIcon(VaadinIcon.STAR.create()); addClickListener(event -> toggleFavorite()); + } + + public void setFavourite() { + + username = (String) UI.getCurrent().getSession().getAttribute("username"); + location = (LocationDetails) UI.getCurrent().getSession().getAttribute("selectedLocation"); + + isFavorite = favouriteLocationService.isFavouriteLocation(username, location); + updateFavoriteIconStyle(); } private void toggleFavorite() { - isFavorite = !isFavorite; - if (isFavorite()) { deleteFavorite(); } else { markFavorite(); } + + isFavorite = !isFavorite; + updateFavoriteIconStyle(); } - public boolean isFavorite() { + private boolean isFavorite() { return isFavorite; } - public void markFavorite() { + private void markFavorite() { + try { + + favouriteLocationService.addFavouriteLocation(username, location); + + } catch (FavouriteLocationException ex) { + + LOGGER.error("Error marking location: {}", ex.getMessage(), ex); + + Notification.show("Error marking locations. " + ex.getMessage(), 3000, Notification.Position.TOP_CENTER); + + } catch (Exception ex) { + + LOGGER.error("Error marking locations: {}", ex.getMessage(), ex); + + Notification.show("Error marking locations. Please try again later.", 3000, Notification.Position.TOP_CENTER); + } } - public void deleteFavorite() { + private void deleteFavorite() { + try { + + favouriteLocationService.deleteFavouriteLocation(username, location); + + } catch (FavouriteLocationException ex) { + + LOGGER.error("Error removing marked locations: {}", ex.getMessage(), ex); + + Notification.show("Error removing marked locations. " + ex.getMessage(), 3000, Notification.Position.TOP_CENTER); + + } catch (Exception ex) { + + LOGGER.error("Error removing marked locations: {}", ex.getMessage(), ex); + + Notification.show("Error removing marked locations. Please try again later.", 3000, Notification.Position.TOP_CENTER); + } } private void updateFavoriteIconStyle() { diff --git a/src/main/java/com/eastnetic/application/views/WeatherView.java b/src/main/java/com/eastnetic/application/views/WeatherView.java index 417e23a65..2b8b6b671 100644 --- a/src/main/java/com/eastnetic/application/views/WeatherView.java +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -33,19 +33,22 @@ public class WeatherView extends VerticalLayout implements BeforeEnterObserver { private final H3 locationName = new H3(); + private final FavoriteIconButton favoriteIconButton; + private final CurrentWeatherCard currentWeatherCard; private final WeatherProviderService weatherProviderService; - public WeatherView(CurrentWeatherCard currentWeatherCard, + public WeatherView(FavoriteIconButton favoriteIconButton, + CurrentWeatherCard currentWeatherCard, WeatherProviderService weatherProviderService) { + this.favoriteIconButton = favoriteIconButton; this.currentWeatherCard = currentWeatherCard; this.weatherProviderService = weatherProviderService; HorizontalLayout markLocation = new HorizontalLayout(); - FavoriteIconButton favoriteIcon = new FavoriteIconButton(); - markLocation.add(locationName, favoriteIcon); + markLocation.add(locationName, favoriteIconButton); add(markLocation, currentWeatherCard); } @@ -59,6 +62,8 @@ public void beforeEnter(BeforeEnterEvent event) { if (location != null) { updateWeatherData(); + + favoriteIconButton.setFavourite(); } } From 0d0ecfb10ebe8ad5d67ae8ab68d29a684f1abffe Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Sun, 13 Aug 2023 01:50:34 +0600 Subject: [PATCH 26/29] Show favourite locations list in FavouriteLocationsView. --Make Location as eager loading in FavouriteLocation. --- .../locations/entity/FavouriteLocation.java | 4 +- .../locations/entity/LocationDetails.java | 23 ++++++ .../FavouriteLocationRepository.java | 4 + .../FavouriteLocationServiceImpl.java | 37 ++++++++- .../views/FavouriteLocationsView.java | 77 ++++++++++++++++++- 5 files changed, 140 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java b/src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java index 518febf4d..5e701867a 100644 --- a/src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java +++ b/src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java @@ -11,11 +11,11 @@ public class FavouriteLocation { @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; - @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) + @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false) private User user; - @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) + @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) @JoinColumn(name = "location_id", referencedColumnName = "id", nullable = false) private Location location; diff --git a/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java index 32625880f..9f8602137 100644 --- a/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java +++ b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java @@ -34,6 +34,29 @@ public class LocationDetails { private String referenceSource; + public LocationDetails(String name, + double latitude, + double longitude, + double elevation, + String countryCode, + String timezone, + String country, + String admin1, + String referenceId, + String referenceSource) { + + this.name = name; + this.latitude = latitude; + this.longitude = longitude; + this.elevation = elevation; + this.countryCode = countryCode; + this.timezone = timezone; + this.country = country; + this.admin1 = admin1; + this.referenceId = referenceId; + this.referenceSource = referenceSource; + } + public String getReferenceId() { return referenceId; } diff --git a/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java b/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java index 3388ddb6b..2a2efe542 100644 --- a/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java +++ b/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java @@ -6,10 +6,14 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface FavouriteLocationRepository extends JpaRepository { FavouriteLocation findFirstByLocation(Location location); + List findAllByUser(User user); + FavouriteLocation findByUserAndLocation(User user, Location location); } diff --git a/src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java index 913d57873..0145f6fe5 100644 --- a/src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.stream.Collectors; @Service public class FavouriteLocationServiceImpl implements FavouriteLocationService { @@ -129,7 +130,25 @@ public void deleteFavouriteLocation(String username, LocationDetails locationDet @Override public List getFavouriteLocations(String username) { - return null; + + LOGGER.info("Get favourite locations: username={}: START", username); + + User user = userService.findUserByUsername(username); + + if (user == null) { + throw new FavouriteLocationException("User not found."); + } + + List favouriteLocations = favouriteLocationRepository.findAllByUser(user); + + List locationDetails = favouriteLocations.stream() + .map(FavouriteLocation::getLocation) + .map(this::convertEntityToDto) + .collect(Collectors.toList()); + + LOGGER.info("Get favourite locations: username={}: SUCCESS", username); + + return locationDetails; } @Override @@ -168,4 +187,20 @@ private void deleteLocationIfNotInUsed(Location location) { locationService.deleteLocation(location); } } + + private LocationDetails convertEntityToDto(Location locationDetails) { + + return new LocationDetails( + locationDetails.getName(), + locationDetails.getLatitude(), + locationDetails.getLongitude(), + locationDetails.getElevation(), + locationDetails.getCountryCode(), + locationDetails.getTimezone(), + locationDetails.getCountry(), + locationDetails.getAdmin1(), + locationDetails.getReferenceId(), + locationDetails.getReferenceSource() + ); + } } diff --git a/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java b/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java index e91fa8b27..652328076 100644 --- a/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java +++ b/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java @@ -1,8 +1,81 @@ package com.eastnetic.application.views; +import com.eastnetic.application.locations.entity.LocationDetails; +import com.eastnetic.application.locations.exceptions.FavouriteLocationException; +import com.eastnetic.application.locations.service.FavouriteLocationService; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; -@Route("favourite-locations") -public class FavouriteLocationsView extends VerticalLayout { +import java.util.List; + +@AnonymousAllowed +@Route(value = "favourite-locations", layout = MainLayout.class) +@PageTitle("Favourite Locations") +public class FavouriteLocationsView extends VerticalLayout implements BeforeEnterObserver { + + private static final Logger LOGGER = LogManager.getLogger(FavouriteLocationsView.class); + + private List favouriteLocations; + + private final LocationListComponent locationListComponent; + + private final FavouriteLocationService favouriteLocationService; + + public FavouriteLocationsView(LocationListComponent locationListComponent, + FavouriteLocationService favouriteLocationService) { + + this.locationListComponent = locationListComponent; + this.favouriteLocationService = favouriteLocationService; + + setSizeFull(); + + add(locationListComponent); + } + + @Override + public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { + + String username = (String) UI.getCurrent().getSession().getAttribute("username"); + + List favouriteLocationList = getFavouriteLocations(username); + + if (favouriteLocationList == null || favouriteLocationList.isEmpty()) { + Notification.show("No Favourite locations found.", 3000, Notification.Position.MIDDLE); + return; + } + + locationListComponent.showLocationList(favouriteLocationList); + } + + private List getFavouriteLocations(String username) { + + try { + + return favouriteLocationService.getFavouriteLocations(username); + + } catch (FavouriteLocationException ex) { + + locationListComponent.setVisible(false); + + Notification.show("Error fetching Favourite locations. " + ex.getMessage(), 3000, Notification.Position.MIDDLE); + + } catch (Exception ex) { + + locationListComponent.setVisible(false); + + LOGGER.error("Error fetching Favourite locations: {}", ex.getMessage(), ex); + + Notification.show("Error fetching Favourite locations. Please try again later.", 3000, Notification.Position.MIDDLE); + } + + return null; + } } From c9e77fd2d0deb971b926e29a9fddbb7e6a2571f2 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Mon, 14 Aug 2023 21:34:02 +0600 Subject: [PATCH 27/29] Add constructor for LocationDetails, CurrentWeather, DailyWeather, HourlyWeather and WeatherData. --- .../eastnetic/application/locations/entity/LocationDetails.java | 2 ++ .../eastnetic/application/weathers/entity/CurrentWeather.java | 2 ++ .../com/eastnetic/application/weathers/entity/DailyWeather.java | 2 ++ .../eastnetic/application/weathers/entity/HourlyWeather.java | 2 ++ .../com/eastnetic/application/weathers/entity/WeatherData.java | 2 ++ 5 files changed, 10 insertions(+) diff --git a/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java index 9f8602137..847c1b05b 100644 --- a/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java +++ b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java @@ -34,6 +34,8 @@ public class LocationDetails { private String referenceSource; + public LocationDetails() {} + public LocationDetails(String name, double latitude, double longitude, diff --git a/src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java b/src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java index 4f799c813..699b10aac 100644 --- a/src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java +++ b/src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java @@ -19,6 +19,8 @@ public class CurrentWeather { @JsonProperty("time") private String time; + public CurrentWeather() {} + public double getTemperature() { return temperature; } diff --git a/src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java b/src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java index 2310141af..176633103 100644 --- a/src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java +++ b/src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java @@ -27,6 +27,8 @@ public class DailyWeather { @JsonProperty("windspeed_10m_max") private List maxWindSpeed; + public DailyWeather() {} + public List getTime() { return time; } diff --git a/src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java b/src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java index a8ee07278..35c0dcea5 100644 --- a/src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java +++ b/src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java @@ -21,6 +21,8 @@ public class HourlyWeather { @JsonProperty("winddirection") private List windDirection; + public HourlyWeather() {} + public List getTime() { return time; } diff --git a/src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java b/src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java index a671e3477..cfb81d95c 100644 --- a/src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java +++ b/src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java @@ -13,6 +13,8 @@ public class WeatherData { @JsonProperty("current_weather") private CurrentWeather currentWeather; + public WeatherData() {} + public DailyWeather getDailyWeather() { return dailyWeather; } From af5863f02f6d62fa1248d906b1ef284902137fa4 Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Tue, 15 Aug 2023 01:50:43 +0600 Subject: [PATCH 28/29] Add docker compose file to dockerized the weather app. --Add servlet initializer. --Make the packaging as war. --Add .env file for environment variables. --- .env | 9 +++++ Dockerfile | 35 +++++++++++++++++++ docker-compose.yml | 18 ++++++++++ pom.xml | 2 +- .../application/ServletInitializer.java | 12 +++++++ 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 .env create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/main/java/com/eastnetic/application/ServletInitializer.java diff --git a/.env b/.env new file mode 100644 index 000000000..87a42ef88 --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +SPRING_DATASOURCE_URL=jdbc:postgresql://database:5432/weatherdb +SPRING_DATASOURCE_USERNAME=weatheruser +SPRING_DATASOURCE_PASSWORD=weatherpassword + +POSTGRES_DB=weatherdb +POSTGRES_USER=weatheruser +POSTGRES_PASSWORD=weatherpassword + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..ebc992e93 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Use a Maven base image for building +FROM maven:3.8.3-openjdk-17 AS build + +# Set the working directory inside the container +WORKDIR /app + +# Copy the Maven project files for dependency resolution +COPY pom.xml . + +# Download the project dependencies +RUN mvn dependency:go-offline + +# Copy the application source code +COPY src ./src +COPY frontend ./frontend + +# Build the application +RUN mvn clean package -Pproduction + +# Use a Tomcat base image for deployment +FROM tomcat:10.0 + +# Remove the default ROOT application +RUN rm -rf /usr/local/tomcat/webapps/ROOT + +# Copy the JAR file (compiled application) into the container +COPY --from=build /app/target/weatherapp-1.0-SNAPSHOT.war /usr/local/tomcat/webapps/ROOT.war + +# Expose the port your application will run on +EXPOSE 8080 + +# Command to run the application +CMD ["catalina.sh", "run"] + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..631490915 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' + +services: + weather-app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + env_file: + - .env + depends_on: + - database + + database: + image: postgres:12.15 + env_file: + - .env diff --git a/pom.xml b/pom.xml index 582200674..6d491ed16 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ weatherapp weatherapp 1.0-SNAPSHOT - jar + war 17 diff --git a/src/main/java/com/eastnetic/application/ServletInitializer.java b/src/main/java/com/eastnetic/application/ServletInitializer.java new file mode 100644 index 000000000..c5c94c3f0 --- /dev/null +++ b/src/main/java/com/eastnetic/application/ServletInitializer.java @@ -0,0 +1,12 @@ +package com.eastnetic.application; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(Application.class); + } +} From 90518f2505e72765efadd36e69c42045adee6e3a Mon Sep 17 00:00:00 2001 From: Tanmoy Ghosh Date: Tue, 15 Aug 2023 02:03:22 +0600 Subject: [PATCH 29/29] Update README with Docker setup and project details. --- README.MD | 64 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/README.MD b/README.MD index 8f100a89b..7d86458f0 100644 --- a/README.MD +++ b/README.MD @@ -1,29 +1,39 @@ # Weather Application -This is the job interview task for software developer position - -## What to do -* Fork the repository -* After work is done, make a pull request and notify me by email - -## Task description -You need to use the Weather API provided here https://open-meteo.com/en to make the weather application. -1. Call the API to get the locations by city name. Make the paging available with 10 items per page and a filter to filter by location name. -2. When clicked on the location it should show the weather forecast for the location with the temperature, surface wind and rain hourly. The interface should let you first see the daily forecast and when clicked it then should show the forecast hourly for the day you selected. -3. If you UI contains a chart it is a bonus. The interface should be convenient for user and rely on best usability and design practises -4. Only logged on users should be able to see the weather forecasts. -5. User should be able to mark the location favourite and have the favourite location list for quick access. - -All the other specific requirements are up to you - -## Technical requirements -* Use Vaadin (https://vaadin.com/) framework for the frontend. For chart you may use some different framework -* For backend use Java EE -* Use any database (Postgres, Oracle, etc.) -* Make a Maven project - -## Main points -* Structure your code -* Use best practises -* Use naming conventions -* Show understanding of software development concepts +Welcome to the Weather App! This application provides weather information based on location details. + +## Getting Started + +Follow these instructions to set up and run the Weather App using Docker. + +### Prerequisites + +- [Docker](https://www.docker.com/get-started) + +### Running the App + +1. Clone this repository to your local machine: + + ```bash + git clone https://github.com/your-username/weather-app.git + +2. Navigate to the project directory: + ```bash + cd weather-app + +3. Run the following command to start the application using Docker Compose: + ```bash + docker-compose up + +4. Access the application in your browser: + Open your browser and go to http://localhost:8080 + + +## Note + +* This application was developed with a focus on backend functionality, and the UI may not have an extensive design or visual appeal. + +* The application leverages Vaadin for the user interface, and the UI design might be minimalistic due to the developer's backend-focused expertise. + +## Authors +* Tanmoy Ghosh \ No newline at end of file