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/.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/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/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/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 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/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..57b25ea34 --- /dev/null +++ b/frontend/themes/weatherapp/styles.css @@ -0,0 +1,86 @@ +@import url('./main-layout.css'); + +.blue-span { + color: #0074D9; + 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: 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 { + font-size: 14px; + 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/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..6d491ed16 --- /dev/null +++ b/pom.xml @@ -0,0 +1,222 @@ + + + 4.0.0 + + com.eastnetic.application + weatherapp + weatherapp + 1.0-SNAPSHOT + war + + + 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 + + + org.springframework.boot + spring-boot-starter-cache + + + + com.github.ben-manes.caffeine + caffeine + 2.9.2 + + + com.vaadin + vaadin-icons + 3.0.1 + + + org.springframework.boot + spring-boot-starter-security + + + org.postgresql + postgresql + 42.2.27 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + 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..63d679a4b --- /dev/null +++ b/src/main/java/com/eastnetic/application/Application.java @@ -0,0 +1,30 @@ +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; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + +/** + * 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); + } + + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} 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); + } +} 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/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/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/locations/entity/FavouriteLocation.java b/src/main/java/com/eastnetic/application/locations/entity/FavouriteLocation.java new file mode 100644 index 000000000..5e701867a --- /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.LAZY) + @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false) + private User user; + + @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) + @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/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/entity/LocationDetails.java b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java new file mode 100644 index 000000000..847c1b05b --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/entity/LocationDetails.java @@ -0,0 +1,150 @@ +package com.eastnetic.application.locations.entity; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class LocationDetails { + + @JsonProperty("id") + private String referenceId; + + @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; + + private String referenceSource; + + public LocationDetails() {} + + 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; + } + + public void setReferenceId(String referenceId) { + this.referenceId = referenceId; + } + + 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 getReferenceSource() { + return referenceSource; + } + + public void setReferenceSource(String referenceSource) { + this.referenceSource = referenceSource; + } + + 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/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/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/repository/FavouriteLocationRepository.java b/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java new file mode 100644 index 000000000..2a2efe542 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/repository/FavouriteLocationRepository.java @@ -0,0 +1,19 @@ +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; + +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/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/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/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/service/LocationProviderService.java b/src/main/java/com/eastnetic/application/locations/service/LocationProviderService.java new file mode 100644 index 000000000..4b17f4567 --- /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, int count); + +} 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..31c8aac66 --- /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); + + Location addLocation(LocationDetails locationDetails); + + void deleteLocation(Location location); + + Location getLocationByReferenceIdAndSource(String id, String source); +} 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..0145f6fe5 --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/FavouriteLocationServiceImpl.java @@ -0,0 +1,206 @@ +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; +import java.util.stream.Collectors; + +@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) { + + 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 + 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); + } + } + + 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/locations/serviceImpl/LocationServiceImpl.java b/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java new file mode 100644 index 000000000..e900a4fcc --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/LocationServiceImpl.java @@ -0,0 +1,92 @@ +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 Location addLocation(LocationDetails locationDetails) { + + Location location = getLocationByReferenceIdAndSource( + locationDetails.getReferenceId(), + locationDetails.getReferenceSource() + ); + + if (location != null) { + return null; + } + + location = convertDtoToEntity(locationDetails); + + try { + + location = locationRepository.save(location); + + return location; + + } catch (Exception e) { + + LOGGER.error("Saving location failed. Reference ID={}, Reference Source={}", + locationDetails.getReferenceId(), locationDetails.getReferenceSource(), e + ); + + throw e; + } + } + + @Override + public void deleteLocation(Location location) { + + try { + + locationRepository.delete(location); + + } catch (Exception e) { + + LOGGER.error("Delete location failed. Reference ID={}, Reference Source={}", + location.getReferenceId(), location.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.getReferenceSource(), + locationDetails.getName(), + locationDetails.getLatitude(), + locationDetails.getLongitude(), + locationDetails.getElevation(), + locationDetails.getCountryCode(), + locationDetails.getTimezone(), + locationDetails.getCountry(), + locationDetails.getAdmin1() + ); + } +} 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..a3966629d --- /dev/null +++ b/src/main/java/com/eastnetic/application/locations/serviceImpl/OpenMeteoLocationProviderServiceImpl.java @@ -0,0 +1,63 @@ +package com.eastnetic.application.locations.serviceImpl; + + +import com.eastnetic.application.locations.entity.LocationDetails; +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; +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; +import java.util.stream.Collectors; + +@Service +@CacheConfig(cacheNames = "locationDetailsCache") +public class OpenMeteoLocationProviderServiceImpl implements LocationProviderService { + + 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; + + public OpenMeteoLocationProviderServiceImpl(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + @Cacheable(key = "#cityName", unless = "#result == null") + 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 + "&count=" + count; + + 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().stream() + .peek(locationDetail -> locationDetail.setReferenceSource(SOURCE_NAME)) + .collect(Collectors.toList()); + } + + 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 LocationDataException("Error fetching location details from open-meteo api.", ex); + } + } +} 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/CurrentWeatherCard.java b/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java new file mode 100644 index 000000000..d431fc526 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/CurrentWeatherCard.java @@ -0,0 +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.H5; +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 { + + private final H5 currentWeatherText = new H5("Current Weather"); + + private final TextField windSpeedField = new TextField("Wind Speed"); + + private final TextField temperatureField = new TextField("Temperature"); + + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a"); + + public CurrentWeatherCard() { + + windSpeedField.setReadOnly(true); + temperatureField.setReadOnly(true); + + HorizontalLayout dataContainer = new HorizontalLayout(); + dataContainer.add(temperatureField, windSpeedField); + + VerticalLayout infoContainer = new VerticalLayout(); + addClassName("info-container"); + infoContainer.add(currentWeatherText, dataContainer); + + add(infoContainer); + } + + public void showCurrentWeather(CurrentWeather currentWeather) { + + LocalDateTime localDateTime = LocalDateTime.parse(currentWeather.getTime(), DateTimeFormatter.ISO_DATE_TIME); + + 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/DailyWeatherCard.java b/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java new file mode 100644 index 000000000..a3526a43d --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/DailyWeatherCard.java @@ -0,0 +1,98 @@ +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; + +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, + LocationDetails location, + WeatherProviderService weatherProviderService) { + + 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); + + 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/FavoriteIconButton.java b/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java new file mode 100644 index 000000000..198eebf15 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/FavoriteIconButton.java @@ -0,0 +1,110 @@ +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; + + 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() { + + if (isFavorite()) { + deleteFavorite(); + } else { + markFavorite(); + } + + isFavorite = !isFavorite; + + updateFavoriteIconStyle(); + } + + private boolean isFavorite() { + return isFavorite; + } + + 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); + } + } + + 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() { + + 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/FavouriteLocationsView.java b/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java new file mode 100644 index 000000000..652328076 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/FavouriteLocationsView.java @@ -0,0 +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; + +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; + } +} 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..393d0f213 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/HourlyWeatherDialog.java @@ -0,0 +1,142 @@ +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.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; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +public class HourlyWeatherDialog extends Dialog { + + private static final Logger LOGGER = LogManager.getLogger(HourlyWeatherDialog.class); + + private final WeatherProviderService weatherProviderService; + + public HourlyWeatherDialog(String day, + LocationDetails location, + WeatherProviderService weatherProviderService) { + + this.weatherProviderService = weatherProviderService; + + setWidth("50%"); + setCloseOnEsc(true); + setCloseOnOutsideClick(true); + + WeatherData weatherData = getHourlyWeatherData(day, location); + + setHourlyData(weatherData.getHourlyWeather()); + } + + 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(HourlyWeather hourlyWeather) { + + try { + + List hourlyWeatherGridDataList = new ArrayList<>(); + + int dataSize = hourlyWeather.getTime().size(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); + + for (int i = 0; i < dataSize; i++) { + + String date = hourlyWeather.getTime().get(i); + LocalDateTime dateTime = LocalDateTime.parse(date, DateTimeFormatter.ISO_DATE_TIME); + + hourlyWeatherGridDataList.add( + new HourlyWeatherGridData( + dateTime.format(formatter), + hourlyWeather.getRain().get(i) + " mm", + hourlyWeather.getWindSpeed().get(i) + " km/h", + hourlyWeather.getTemperature().get(i) + " °C" + )); + } + + Grid hourlyWeatherGrid = new Grid<>(HourlyWeatherGridData.class); + hourlyWeatherGrid.setItems(hourlyWeatherGridDataList); + + add(hourlyWeatherGrid); + + } catch (WeatherDataException ex) { + + 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 { + + 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; + } + + 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; + } + } +} 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..fc8b0c506 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/LocationListComponent.java @@ -0,0 +1,83 @@ +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; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@UIScope +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() { + + configureLocationGrid(); + configurePaginationComponent(); + } + + private void configureLocationGrid() { + + locationGrid.setVisible(false); + locationGrid.getColumns().forEach(col -> col.setAutoWidth(true)); + locationGrid.setColumns("name", "country", "latitude", "longitude", "elevation", "timezone"); + locationGrid.addItemClickListener(event -> showWeatherDate(event.getItem())); + + add(locationGrid); + } + + private void configurePaginationComponent() { + + paginationComponent = new PaginationComponent(this); + + add(paginationComponent); + } + + public void showLocationList(List locationList) { + + this.locationList = locationList; + + onPageChange(1); + + int totalPages = (int) Math.ceil((double) locationList.size() / paginationComponent.getPageSize()); + + 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) { + + 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/LocationSearchView.java b/src/main/java/com/eastnetic/application/views/LocationSearchView.java new file mode 100644 index 000000000..d1df3223c --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/LocationSearchView.java @@ -0,0 +1,92 @@ +package com.eastnetic.application.views; + +import com.eastnetic.application.locations.entity.LocationDetails; +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; +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; +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; + +import java.util.List; + +@AnonymousAllowed +@UIScope +@Component +@Route(value = "locations", layout = MainLayout.class) +@PageTitle("Locations") +public class LocationSearchView extends VerticalLayout { + + private static final Logger LOGGER = LogManager.getLogger(LocationSearchView.class); + + 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()) { + + try { + + List locations = locationProviderService.getLocationDetails(cityName, 50); + + locationListComponent.showLocationList(locations); + + } 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); + } + } + } + +} 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..b0aeecf70 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/MainLayout.java @@ -0,0 +1,37 @@ +package com.eastnetic.application.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.RouterLink; + +public class MainLayout extends AppLayout { + + public MainLayout() { + + addToNavbar(new DrawerToggle(), createTopBarSpan()); + + addToDrawer(createSidebarLayout()); + } + + private Span createTopBarSpan() { + + Span title = new Span("Weather App"); + title.addClassName("blue-span"); + + return title; + } + + private VerticalLayout createSidebarLayout() { + + VerticalLayout menuLayout = new VerticalLayout(); + + RouterLink homeLink = new RouterLink("Home", LocationSearchView.class); + RouterLink locationsLink = new RouterLink("Favourite Locations", FavouriteLocationsView.class); + + menuLayout.add(homeLink, locationsLink); + + return menuLayout; + } +} \ No newline at end of file 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..e68ce7425 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/MainView.java @@ -0,0 +1,152 @@ +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; +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; +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"); + + 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(AuthService authService, UserService userService) { + + this.authService = authService; + this.userService = userService; + + 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; + } + + 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) { + + LOGGER.error("User registration failed: Username={}", username, e); + + Notification.show("Registration failed. Please try again later.", 3000, Notification.Position.TOP_CENTER); + } + } + + private void login() { + + String username = loginUsername.getValue(); + String password = loginPassword.getValue(); + + 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); + } + + } catch (UsernameNotFoundException e) { + + Notification.show("Login failed: " + e.getMessage(), 3000, Notification.Position.TOP_CENTER); + + } catch (Exception e) { + + LOGGER.error("User login failed: Username={}", username, e); + + Notification.show("Login failed. Please try again later.", 3000, Notification.Position.TOP_CENTER); + } + } +} + 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); + } +} + 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..2b8b6b671 --- /dev/null +++ b/src/main/java/com/eastnetic/application/views/WeatherView.java @@ -0,0 +1,115 @@ +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.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.HorizontalLayout; +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 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) +@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(); + + private final FavoriteIconButton favoriteIconButton; + + private final CurrentWeatherCard currentWeatherCard; + + private final WeatherProviderService weatherProviderService; + + public WeatherView(FavoriteIconButton favoriteIconButton, + CurrentWeatherCard currentWeatherCard, + WeatherProviderService weatherProviderService) { + + this.favoriteIconButton = favoriteIconButton; + this.currentWeatherCard = currentWeatherCard; + this.weatherProviderService = weatherProviderService; + + HorizontalLayout markLocation = new HorizontalLayout(); + markLocation.add(locationName, favoriteIconButton); + + add(markLocation, currentWeatherCard); + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + + event.getNavigationTarget(); + location = (LocationDetails) UI.getCurrent().getSession().getAttribute("selectedLocation"); + + if (location != null) { + + updateWeatherData(); + + favoriteIconButton.setFavourite(); + } + } + + private void updateWeatherData() { + + try { + WeatherData weatherData = weatherProviderService.getDailyWeatherData( + location.getLatitude(), location.getLongitude(), location.getTimezone() + ); + + locationName.setText(location.locationFullName()); + + currentWeatherCard.showCurrentWeather(weatherData.getCurrentWeather()); + + DailyWeather dailyWeather = weatherData.getDailyWeather(); + + int dayCount = dailyWeather.getTime().size(); + + for (int i = 0; i < dayCount; i++) { + + DailyWeatherCard dailyWeatherCard = new DailyWeatherCard( + dailyWeather.getTime().get(i), + dailyWeather.getMaxTemperature().get(i), + dailyWeather.getMinTemperature().get(i), + dailyWeather.getSunrise().get(i), + dailyWeather.getSunset().get(i), + dailyWeather.getRainSum().get(i), + dailyWeather.getMaxWindSpeed().get(i), + location, + weatherProviderService + ); + + add(dailyWeatherCard); + } + + } catch (WeatherDataException ex) { + + LOGGER.error("Error updating weather data: {}", ex.getMessage(), ex); + + Notification.show(ex.getMessage(), 3000, Notification.Position.MIDDLE); + + } catch (Exception ex) { + + LOGGER.error("Error updating weather data: {}", ex.getMessage(), ex); + + Notification.show("Error fetching weather data. Please try again later.", 3000, Notification.Position.MIDDLE); + } + } +} 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..699b10aac --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/entity/CurrentWeather.java @@ -0,0 +1,63 @@ +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 CurrentWeather() {} + + 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..176633103 --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/entity/DailyWeather.java @@ -0,0 +1,87 @@ +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 DailyWeather() {} + + 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..35c0dcea5 --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/entity/HourlyWeather.java @@ -0,0 +1,65 @@ +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 HourlyWeather() {} + + 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..cfb81d95c --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/entity/WeatherData.java @@ -0,0 +1,41 @@ +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 WeatherData() {} + + 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/exceptions/WeatherDataException.java b/src/main/java/com/eastnetic/application/weathers/exceptions/WeatherDataException.java new file mode 100644 index 000000000..7a6f1b32b --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/exceptions/WeatherDataException.java @@ -0,0 +1,13 @@ +package com.eastnetic.application.weathers.exceptions; + + +public class WeatherDataException extends RuntimeException { + + public WeatherDataException(String message) { + super(message); + } + + public WeatherDataException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file 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/OpenMeteoWeatherProviderServiceImpl.java b/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherProviderServiceImpl.java new file mode 100644 index 000000000..9e0fd66a6 --- /dev/null +++ b/src/main/java/com/eastnetic/application/weathers/serviceImpl/OpenMeteoWeatherProviderServiceImpl.java @@ -0,0 +1,109 @@ +package com.eastnetic.application.weathers.serviceImpl; + +import com.eastnetic.application.weathers.entity.WeatherData; +import com.eastnetic.application.weathers.exceptions.WeatherDataException; +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 OpenMeteoWeatherProviderServiceImpl implements WeatherProviderService { + + 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 OpenMeteoWeatherProviderServiceImpl(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); + + if (weatherData == null) { + throw new WeatherDataException("Daily weather data not found from open-meteo api."); + } + + 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); + + throw new WeatherDataException("Error fetching daily weather from open-meteo api.", ex); + } + } + + @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); + + if (weatherData == null) { + throw new WeatherDataException("Hourly weather data not found for date ." + dateString); + } + + 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); + + throw new WeatherDataException("Error fetching daily weather from open-meteo api.", ex); + } + } +} \ No newline at end of file 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 000000000..df2ed4bd8 Binary files /dev/null and b/src/main/resources/META-INF/resources/icons/icon.png differ 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 000000000..9777f260a Binary files /dev/null and b/src/main/resources/META-INF/resources/images/empty-plant.png differ diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 000000000..91a83e29c --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,19 @@ +server.port=${PORT:8080} +logging.level.org.atmosphere = warn +spring.mustache.check-template-location = false + +# Launch the default browser when starting the application in development mode +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.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 + diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 000000000..2e08c8bea --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,6 @@ + _____ _ _ + |_ _|__ ___| |_ / \ _ __ _ __ + | |/ _ \/ __| __| / _ \ | '_ \| '_ \ + | | __/\__ \ |_ / ___ \| |_) | |_) | + |_|\___||___/\__/_/ \_\ .__/| .__/ + |_| |_| diff --git a/src/main/resources/vaadin-featureflags.properties b/src/main/resources/vaadin-featureflags.properties new file mode 100644 index 000000000..86325c698 --- /dev/null +++ b/src/main/resources/vaadin-featureflags.properties @@ -0,0 +1,2 @@ +# SideNav component (Production ready but tweaks to at least the internal DOM will still take place) +com.vaadin.experimental.sideNavComponent=true