diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..1da9e3ef7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +/target/ +.idea/ +.settings +.project +.classpath +/src/main/webapp/frontend-es*/ + +*.iml +.DS_Store + +# The following files are generated/updated by vaadin-maven-plugin +node_modules/ +pnpmfile.js + +# Browser drivers for local integration tests +drivers.xml +drivers/ +# Error screenshots generated by TestBench for failed integration tests +error-screenshots/ +# Generated theme files that should not be stored +frontend/generated/ + +webpack.generated.js + diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 000000000..63f9f5acc --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,6 @@ +tasks: + - init: mvn install -Pproduction + command: mvn package tomee:run +ports: + - port: 8080 + onOpen: open-preview diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 000000000..eed2b641d --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is + * provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl + * property to use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download + * url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a + // custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..2cc7d4a55 Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..ffdc10e59 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..3607cc58a --- /dev/null +++ b/LICENSE @@ -0,0 +1,210 @@ +All parts, except the contents of the documentation module, are licenced +under Apache License v2.0. See the license text below. + +The documentation is licensed under Creative Commons CC-BY-ND 2.0 +(http://creativecommons.org/licenses/by-nd/2.0/legalcode). + + +----------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed 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. \ No newline at end of file diff --git a/README.MD b/README.MD index 8f100a89b..885228323 100644 --- a/README.MD +++ b/README.MD @@ -1,29 +1,95 @@ -# 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 +# WeatherApp + +WeatherApp is a user-friendly web application designed to provide detailed weather forecasts for various locations. +It contains all the necessary configuration and some placeholder files to get you started. + +Vaadin 24.0 is based on Jakarta EE 10.0 and requires Java 17. + +## Running the Application + +You can also import the project to your IDE of choice as you would with any +Maven project. Read more on [how to import Vaadin projects to different +IDEs](https://vaadin.com/docs/latest/flow/guide/step-by-step/importing) (Eclipse, IntelliJ IDEA, NetBeans, and VS Code). + +Run application using +``` +mvn tomee:run +``` +``` +mvn wildfly:run +``` + +Open [http://localhost:8080/WeatherApp](http://localhost:8080/WeatherApp) in browser. + +If you want to run your app locally in the production mode, run using +``` +mvn clean package wildfly:run -Pproduction +``` +### Database Configuration +Postgresql 42.2.23 is used for the application. + +To configure database go the `DatabaseManager.java` file (see the project structure to locate) to set up the necessary changes. +Additionally a script is provided with the project placed in the resoureces folder. +Run the script to generate necessary tables & data. +``` +List of users for the application +- username: john_doe, password: password123 +- username: jane_smith, password: pass456 +- username: admin, password: admin123 +``` +#### Deployment + +The project is a standard Java/Jakarta EE application, so you can deploy it as you see best, via IDE or using Maven plugins. Wildfly and TomEE plugins are pre-configured for easy testing. Wildfly plugin is used for integration tests. Currently only Wildfly properly supports Java 17. + +The application can be deployed on the [Apache TomEE](http://tomee.apache.org/) server via the `tomee-maven-plugin`, which supports hot deployment of code changes (via the `reloadOnUpdate` setting). +This means that you can make changes to the code in your IDE while the server is running, recompile, and have the server automatically pick up the changes and redeploy them. +This setting is enabled by default in this project. + +##### Project structure + +- This setup uses [Vertical Layout](https://vaadin.com/components/vaadin-vertical-layout). +- `views` package in `src/main/java` contains the server-side Java views of your application. +- `views` folder in `frontend/` contains the client-side JavaScript views of your application. +- `themes` folder in `frontend/` contains the custom CSS styles. +``` +- WeatherApp + │ pom.xml + │ README.md + └───src + └───main + └───java + └───com + └───vaadin + └───example + ├───api + │ WeatherApiClient.java + │ + ├───config + │ DatabaseManager.java + │ + ├───model + │ Location.java + │ Users.java + │ + ├───repository + │ LocationRepository.java + │ UserRepository.java + │ + ├───service + │ LocationService.java + │ UserService.java + │ + ├───util + │ Tools.java + │ + └───view + │ FavoriteLocations.java + │ LoginView.java + MainView.java + + ``` +##### Business Logic +- any user can search Location (name, lat, long) by city name +- filter option is available +- only registered users can see weather forecast by clicking on location +- registered users also can hourly details weather info by clicking on specific date diff --git a/frontend/chart-component.js b/frontend/chart-component.js new file mode 100644 index 000000000..a2058cf02 --- /dev/null +++ b/frontend/chart-component.js @@ -0,0 +1,56 @@ +// chart-component.js + +import Chart from 'chart.js/auto'; + +window.initChart = function (element) { + if (!element.chartInstance) { + element.chartInstance = new Chart(element, { + type: 'line', + data: { + labels: [], + datasets: [ + { + label: 'Temperature (°C)', + data: [], + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + }, + { + label: 'Wind Speed (km/h)', + data: [], + borderColor: 'rgb(54, 162, 235)', + backgroundColor: 'rgba(54, 162, 235, 0.2)', + }, + { + label: 'Rainfall (mm)', + data: [], + borderColor: 'rgb(75, 192, 192)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + } + ] + }, + options: { + scales: { + x: { + type: 'category', + labels: [], + } + } + } + }); + } +}; + +window.renderChart = function (element, jsonData) { + const data = JSON.parse(jsonData); + const labels = data.time; + const datasets = data.datasets; + + if (element.chartInstance) { + element.chartInstance.data.labels = labels; + element.chartInstance.data.datasets.forEach((dataset, index) => { + dataset.data = datasets[index]; + }); + element.chartInstance.update(); + } +}; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..a5cdd4018 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/frontend/src/README b/frontend/src/README new file mode 100644 index 000000000..ab4a6d447 --- /dev/null +++ b/frontend/src/README @@ -0,0 +1 @@ +Place your Vaadin Designer or hand written templates in this folder. diff --git a/frontend/themes/my-theme/styles.css b/frontend/themes/my-theme/styles.css new file mode 100644 index 000000000..1de9e2a08 --- /dev/null +++ b/frontend/themes/my-theme/styles.css @@ -0,0 +1,57 @@ +/* + CSS styling examples for the Vaadin app. + + This is the style entrypoint for the theme and together with css in ./components/ included + automatically into the theme. + + Visit https://vaadin.com/docs-beta/latest/theming/application-theme/ for more information. +*/ + +/* Example: CSS class name to center align the content . */ +.centered-content { + margin: 0 auto; + max-width: 250px; +} + +/* Example: the style is applied only to the textfields which have the `bordered` class name. */ +vaadin-text-field.bordered::part(input-field) { + box-shadow: inset 0 0 0 1px var(--lumo-contrast-30pct); + background-color: var(--lumo-base-color); +} +.padding-margin{ + padding-top: 5px !important; + padding-bottom: 5px !important; +} +.padding spacing{ + padding-top: 5px !important; + padding-bottom: 5px !important; +} +.cursor-pointer { + cursor: pointer; +} +.close-button { + padding: 0; + margin-left: -50px; + color: #A50021; +} +.fav-container { + padding: 2px; + margin: 2px; + border-radius: 5px; +} +.fav-container-legend { + font-weight: normal; + margin-bottom: 0px; +} +.padding-margin{ + padding: 25px; +} +.no-top-margin{ + margin-top: 0; + padding-top: 0; +} +.text-align-right{ + text-align: right; +} + + diff --git a/frontend/themes/my-theme/theme.json b/frontend/themes/my-theme/theme.json new file mode 100644 index 000000000..b007ffdfc --- /dev/null +++ b/frontend/themes/my-theme/theme.json @@ -0,0 +1,3 @@ +{ + "lumoImports" : [ "typography", "color", "spacing", "badge", "utility" ] +} diff --git a/mvnw b/mvnw new file mode 100644 index 000000000..41c0f0c23 --- /dev/null +++ b/mvnw @@ -0,0 +1,310 @@ +#!/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 /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="`which 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/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$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 \ + -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..86115719e --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@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 "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\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/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "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%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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 "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\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% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..111e985b8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,243 @@ + + + 4.0.0 + com.example + weatherapp + WeatherApp + 1.0-SNAPSHOT + war + + + 17 + 17 + UTF-8 + UTF-8 + false + + + + 24.3.12 + + + + + + com.vaadin + vaadin-bom + ${vaadin.version} + pom + import + + + + + + + jakarta.platform + jakarta.jakartaee-web-api + 10.0.0 + provided + + + org.postgresql + postgresql + 42.2.23 + + + com.zaxxer + HikariCP + 5.0.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.13.1 + + + + org.slf4j + slf4j-simple + + + + com.vaadin + vaadin-core + 23.0.0 + + + + com.vaadin + vaadin-cdi + + + com.vaadin + vaadin-testbench + test + + + + io.github.bonigarcia + webdrivermanager + 5.2.1 + test + + + + + + package wildfly:run + + + + org.apache.tomee.maven + tomee-maven-plugin + + 9.0.0 + + webprofile + ROOT + + + .class + + + true + + true + - + + + + + + org.wildfly.plugins + wildfly-maven-plugin + 4.0.0.Final + + 27.0.1.Final + + + + + org.apache.maven.plugins + maven-war-plugin + 3.3.2 + + + + + com.vaadin + vaadin-maven-plugin + ${vaadin.version} + + + + prepare-frontend + + + + + + + + + + + production + + + + com.vaadin + vaadin-core + + + com.vaadin + vaadin-dev + + + + + + + + com.vaadin + vaadin-maven-plugin + + + + build-frontend + + + + + + + + + + it + + + + org.wildfly.plugins + wildfly-maven-plugin + + + start + pre-integration-test + + start + deploy + + + + stop + post-integration-test + + shutdown + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.2 + + + + integration-test + verify + + + + + false + true + + + + + + + + + vaadin-directory + https://maven.vaadin.com/vaadin-directory + + + vaadin-addons + https://maven.vaadin.com/vaadin-addons + + + + diff --git a/src/main/bundles/README.md b/src/main/bundles/README.md new file mode 100644 index 000000000..581b703fa --- /dev/null +++ b/src/main/bundles/README.md @@ -0,0 +1,32 @@ +This directory is automatically generated by Vaadin and contains the pre-compiled +frontend files/resources for your project (frontend development bundle). + +It should be added to Version Control System and committed, so that other developers +do not have to compile it again. + +Frontend development bundle is automatically updated when needed: +- an npm/pnpm package is added with @NpmPackage or directly into package.json +- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript +- Vaadin add-on with front-end customizations is added +- Custom theme imports/assets added into 'theme.json' file +- Exported web component is added. + +If your project development needs a hot deployment of the frontend changes, +you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions): +- set `vaadin.frontend.hotdeploy=true` in `application.properties` +- configure `vaadin-maven-plugin`: +``` + + true + +``` +- configure `jetty-maven-plugin`: +``` + + + true + + +``` + +Read more [about Vaadin development mode](https://vaadin.com/docs/next/configuration/development-mode/#pre-compiled-front-end-bundle-for-faster-start-up). \ No newline at end of file diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle new file mode 100644 index 000000000..34385e850 Binary files /dev/null and b/src/main/bundles/dev.bundle differ diff --git a/src/main/java/org/vaadin/example/AppShell.java b/src/main/java/org/vaadin/example/AppShell.java new file mode 100644 index 000000000..749442c62 --- /dev/null +++ b/src/main/java/org/vaadin/example/AppShell.java @@ -0,0 +1,14 @@ +package org.vaadin.example; + +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.server.PWA; +import com.vaadin.flow.theme.Theme; + +/** + * Use the @PWA annotation make the application installable on phones, tablets + * and some desktop browsers. + */ +@PWA(name = "Project Base for Vaadin", shortName = "Project Base") +@Theme("my-theme") +public class AppShell implements AppShellConfigurator { +} diff --git a/src/main/java/org/vaadin/example/MainView.java b/src/main/java/org/vaadin/example/MainView.java new file mode 100644 index 000000000..83a3dddb2 --- /dev/null +++ b/src/main/java/org/vaadin/example/MainView.java @@ -0,0 +1,353 @@ +package org.vaadin.example; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.grid.HeaderRow; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +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.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinSession; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.json.JsonObject; +import org.vaadin.example.service.LocationService; +import org.vaadin.example.service.UserService; +import org.vaadin.example.util.Tools; +import org.vaadin.example.view.FavoriteLocationsView; +import org.vaadin.example.view.LoginView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +@Route("/WeatherApp") +@PageTitle("WeatherApp") +@CdiComponent +public class MainView extends VerticalLayout { + @Inject + private UserService userService; + @Inject + private LocationService locationService; + + private HorizontalLayout buttonLayout; + private TextField cityNameField; + private TextField filterField; + private Grid grid; + private Button searchButton; + private Button resetButton; + private Button firstButton; + private Button nextButton; + private Button previousButton; + private Button lastButton; + private Span paginationLabel; + private FavoriteLocationsView favLocView; + + private List allLocations = new ArrayList<>(); + private List filteredLocations = new ArrayList<>(); + + private int currentPage = 0; + private static final int PAGE_SIZE = 10; + + private static final Logger LOGGER = Logger.getLogger(MainView.class.getName()); + + @PostConstruct + public void init() { + initAllComponent(); + + favLocView = new FavoriteLocationsView(locationService, userService); + add(favLocView); + + //start - search panel + HorizontalLayout searchLayout = new HorizontalLayout(); + searchLayout.setWidthFull(); + searchLayout.add(cityNameField, searchButton, resetButton); + searchLayout.setAlignItems(Alignment.BASELINE); + + Span spacer = new Span(); + spacer.setWidth("100%"); + searchLayout.add(spacer); + searchLayout.add(filterField); + searchLayout.addClassName("padding-margin"); + //end - search panel + + HorizontalLayout paginationLayout = new HorizontalLayout(firstButton, previousButton, paginationLabel, nextButton, lastButton); + paginationLayout.setWidthFull(); + paginationLayout.setAlignItems(FlexComponent.Alignment.CENTER); + paginationLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + + //start - main grid pagination + HorizontalLayout gridLayout = new HorizontalLayout(grid); + gridLayout.setWidthFull(); + gridLayout.addClassNames("padding-margin", "no-top-margin"); + //end - main grid pagination + + //add all panels to main layout + add(searchLayout, gridLayout, paginationLayout); + } + + private void initAllComponent(){ + configureHeader(); + configureCityNameField(); + configureFilterField(); + configureResetButton(); + configurePagination(); + + configureSearchButton(); + configureMainGrid(); + } + + private void configureHeader() { + setSizeFull(); + setPadding(false); + setSpacing(false); + H3 title = new H3("Weather App"); + title.getStyle().set("margin", "0"); + + Button loginButton = new Button("Login", e -> { + UI.getCurrent().navigate(LoginView.class); + }); + loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + HorizontalLayout header = new HorizontalLayout(title); + header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + header.setWidth("100%"); + header.setSpacing(true); + header.getStyle().set("padding", "var(--lumo-space-m)") + .set("box-shadow", "0 4px 8px rgba(0, 0, 0, 0.1)"); + + HorizontalLayout menu; + if (userService.hasSession()) { + Span userLabel = new Span("Welcome, " + userService.getLoggedUserName()); + Button logoutButton = new Button("Logout", e -> { + VaadinSession.getCurrent().close(); + VaadinSession.getCurrent().getSession().invalidate(); + UI.getCurrent().navigate(LoginView.class); + }); + logoutButton.addThemeVariants(ButtonVariant.LUMO_ERROR); + menu = new HorizontalLayout(userLabel, logoutButton); + } else { + menu = new HorizontalLayout(loginButton); + } + menu.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + menu.setSpacing(true); + + HorizontalLayout headerLayout = new HorizontalLayout(header, menu); + headerLayout.setWidth("100%"); + headerLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); + headerLayout.setAlignItems(FlexComponent.Alignment.CENTER); + + add(headerLayout); + } + + private void configureMainGrid() { + grid = new Grid<>(JsonObject.class, false); + if(userService.hasSession()){ + grid.setMinHeight("420px"); + }else{ + grid.setMinHeight("500px"); + } + grid.setWidthFull(); + grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, + GridVariant.LUMO_NO_BORDER, GridVariant.LUMO_ROW_STRIPES); + Grid.Column nameColumn = grid.addColumn(location -> location.getString("name")).setHeader("Name"); + Grid.Column admin1Column = grid.addColumn(location -> location.containsKey("admin1") ? location.getString("admin1") : "").setHeader("Admin1"); + Grid.Column admin2Column = grid.addColumn(location -> location.containsKey("admin2") ? location.getString("admin2") : "").setHeader("Admin2"); + Grid.Column admin3Column = grid.addColumn(location -> location.containsKey("admin3") ? location.getString("admin3") : "").setHeader("Admin3"); + Grid.Column latitudeColumn = grid.addColumn(location -> location.getJsonNumber("latitude").toString()).setHeader("Latitude"); + Grid.Column longitudeColumn = grid.addColumn(location -> location.getJsonNumber("longitude").toString()).setHeader("Longitude"); + + HeaderRow headerRow = grid.prependHeaderRow(); + HeaderRow.HeaderCell locationHeader = headerRow.join(nameColumn, admin1Column, admin2Column, admin3Column); + locationHeader.setText("Location"); + + HeaderRow.HeaderCell coordinatesHeader = headerRow.join(latitudeColumn, longitudeColumn); + coordinatesHeader.setText("Coordinates"); + + //Weather forecast only applicable for logged-in users + if(userService.hasSession()){ + + // Add "Favorite" button column with star icon + Grid.Column favoriteColumn = grid.addComponentColumn(item -> { + Button favoriteButton = new Button(new Icon(VaadinIcon.STAR_O)); + favoriteButton.addClassName("cursor-pointer"); + + favoriteButton.addClickListener(event -> { + // Handle favorite button click + JsonObject location = item; + Map result = locationService.saveLocationToFavorites(location); + boolean isSuccess = Boolean.parseBoolean(result.get("isSuccess").toString()); + if (isSuccess) { + Notification.show(result.get("message").toString(), 3000, Notification.Position.MIDDLE); + // Recreate and populate the bordered container + favLocView.updateFavoriteLocations(); + } else { + Notification.show(result.get("message").toString(), 3000, Notification.Position.MIDDLE); + } + + }); + return favoriteButton; + }).setHeader("Add to Favorite"); + + // Add item click listener to the grid + grid.addItemClickListener(event -> { + // Check if the clicked component is the favorite button + if (event.getColumn().equals(favoriteColumn)) { + return; + } + + // If not a favorite button click, proceed with fetching weather forecast + JsonObject location = event.getItem(); + onClickMainGridData(location); + }); + } + grid.addClassName("padding-margin"); + + } + + private void onClickMainGridData(JsonObject location){ + String locName = locationService.getLocationString(location); + double latitude = location.getJsonNumber("latitude").doubleValue(); + double longitude = location.getJsonNumber("longitude").doubleValue(); + List dailyData = locationService.getWeatherForecastApiData(Tools.WEATHER_FORECAST_TYPE_DAILY, latitude, longitude, ""); + locationService.displayDailyForecast(dailyData, latitude, longitude, locName); + } + + private void configureSearchButton() { + searchButton = new Button("Search", e -> { + filterField.setValue(""); + if (cityNameField.isEmpty()) { + Notification.show("City name is required", 3000, Notification.Position.MIDDLE); + } else { + allLocations = locationService.getLocationsByCityName(cityNameField.getValue(), 100); + filteredLocations = new ArrayList<>(allLocations); + if (allLocations.isEmpty()) { + Notification.show("No data found for the provided city name", 3000, Notification.Position.MIDDLE); + } else { + currentPage = 0; + updateMainGrid(); + } + } + }); + searchButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + searchButton.addClassName("cursor-pointer"); + } + + private void configureCityNameField() { + cityNameField = new TextField("Search Location by City Name"); + cityNameField.addThemeName("bordered"); + cityNameField.setWidth("70%"); + cityNameField.setRequired(true); + cityNameField.setClearButtonVisible(true); + } + + private void configureFilterField() { + filterField = new TextField("Filter by Location"); + filterField.setWidth("70%"); + filterField.setClearButtonVisible(true); + filterField.addValueChangeListener(event -> { + currentPage = 0; + updateMainGrid(); + }); + } + + private void configureResetButton() { + resetButton = new Button("Reset"); + resetButton.addClassName("cursor-pointer"); + resetButton.addClickListener(event -> { + grid.setItems(Collections.emptyList()); + allLocations = new ArrayList<>(); + filteredLocations = new ArrayList<>(); + cityNameField.setValue(""); + filterField.setValue(""); + currentPage = 0; + updateMainGrid(); + }); + } + + private void configurePagination() { + paginationLabel = new Span(); + + firstButton = new Button("First", e -> { + if (currentPage > 0) { + currentPage = 0; + updateMainGrid(); + } + }); + + previousButton = new Button("Previous", e -> { + if (currentPage > 0) { + currentPage--; + updateMainGrid(); + } + }); + + nextButton = new Button("Next", e -> { + if ((currentPage + 1) * PAGE_SIZE < filteredLocations.size()) { + currentPage++; + updateMainGrid(); + } + }); + + lastButton = new Button("Last", e -> { + int lastPage = (filteredLocations.size() - 1) / PAGE_SIZE; + if (currentPage < lastPage) { + currentPage = lastPage; + updateMainGrid(); + } + }); + + updatePaginationLabel(filteredLocations.size()); + } + + private void updatePaginationLabel(int totalItems) { + int startItem = totalItems == 0 ? 0 : currentPage * PAGE_SIZE + 1; + int endItem = Math.min(startItem + PAGE_SIZE - 1, totalItems); + paginationLabel.setText(String.format("%d-%d of %d", startItem, endItem, totalItems)); + } + + private void updateMainGrid() { + String filterText = filterField.getValue().trim().toLowerCase(); + filteredLocations.clear(); + + for (JsonObject location : allLocations) { + String name = location.getString("name").toLowerCase(); + String admin1 = location.containsKey("admin1") ? location.getString("admin1").toLowerCase() : ""; + String admin2 = location.containsKey("admin2") ? location.getString("admin2").toLowerCase() : ""; + String admin3 = location.containsKey("admin3") ? location.getString("admin3").toLowerCase() : ""; + + if (name.contains(filterText) + || admin1.contains(filterText) + || admin2.contains(filterText) + || admin3.contains(filterText)) { + filteredLocations.add(location); + } + } + + int start = currentPage * PAGE_SIZE; + int end = Math.min(start + PAGE_SIZE, filteredLocations.size()); + + if (start >= filteredLocations.size() && currentPage > 0) { + currentPage = Math.max(0, (filteredLocations.size() - 1) / PAGE_SIZE); + start = currentPage * PAGE_SIZE; + end = Math.min(start + PAGE_SIZE, filteredLocations.size()); + } + + grid.setItems(filteredLocations.subList(start, end)); + updatePaginationLabel(filteredLocations.size()); + } + +} diff --git a/src/main/java/org/vaadin/example/api/WeatherApiClient.java b/src/main/java/org/vaadin/example/api/WeatherApiClient.java new file mode 100644 index 000000000..b73f1d36a --- /dev/null +++ b/src/main/java/org/vaadin/example/api/WeatherApiClient.java @@ -0,0 +1,94 @@ +package org.vaadin.example.api; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.vaadin.example.util.Tools; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +public class WeatherApiClient { + private static final String LOCATION_API_URL = "https://geocoding-api.open-meteo.com/v1/"; + private static final String WEATHER_API_URL = "https://api.open-meteo.com/v1/"; + + public List getLocationsByCityName(String cityName, int limit) { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(LOCATION_API_URL + "search") + .queryParam("name", cityName) + .queryParam("count", limit); + + Response response = target.request().get(); + String jsonResponse = response.readEntity(String.class); + response.close(); + + JsonReader jsonReader = Json.createReader(new StringReader(jsonResponse)); + JsonObject jsonObject = jsonReader.readObject(); + JsonArray locationsArray = jsonObject.getJsonArray("results"); + + List locations = new ArrayList<>(); + + // Check if locationsArray is null before attempting to access its size + if (locationsArray != null) { + for (int i = 0; i < locationsArray.size(); i++) { + locations.add(locationsArray.getJsonObject(i)); + } + } else { + // Handle the case when locationsArray is null (e.g., log an error, display a message) + System.err.println("No 'results' array found in the JSON response."); + } + + + return locations; + } + + public JsonObject getWeatherForecastDaily(double latitude, double longitude) { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(WEATHER_API_URL + "forecast") + .queryParam("latitude", latitude) + .queryParam("longitude", longitude) + .queryParam("forecast_days", Tools.WEATHER_FORECAST_DAYS_SEVEN) + .queryParam(Tools.WEATHER_FORECAST_TYPE_DAILY, Tools.WEATHER_FORECAST_TYPE_DAILY_PARAMS); + + Response response = target.request().get(); + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + JsonObject responseObject = Json.createReader(new StringReader(jsonResponse)).readObject(); + response.close(); + return responseObject; + } else { + System.err.println("Failed to fetch daily forecast: " + response.getStatusInfo().getReasonPhrase()); + response.close(); + return null; + } + } + public JsonObject getWeatherForecastHourly(double latitude, double longitude, String date) { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(WEATHER_API_URL + "forecast") + .queryParam("latitude", latitude) + .queryParam("longitude", longitude) + .queryParam("start_date", date) + .queryParam("end_date", date) + .queryParam(Tools.WEATHER_FORECAST_TYPE_HOURLY, Tools.WEATHER_FORECAST_TYPE_HOURLY_PARAMS); + + Response response = target.request().get(); + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + JsonObject responseObject = Json.createReader(new StringReader(jsonResponse)).readObject(); + response.close(); + return responseObject; + } else { + System.err.println("Failed to fetch daily forecast: " + response.getStatusInfo().getReasonPhrase()); + response.close(); + return null; + } + } + +} + diff --git a/src/main/java/org/vaadin/example/config/DatabaseManager.java b/src/main/java/org/vaadin/example/config/DatabaseManager.java new file mode 100644 index 000000000..d190cfebb --- /dev/null +++ b/src/main/java/org/vaadin/example/config/DatabaseManager.java @@ -0,0 +1,38 @@ +package org.vaadin.example.config; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DatabaseManager { + + private static final String URL = "jdbc:postgresql://127.0.0.1:5432/weatherapp"; + //use the credentials here... + private static final String USER = ""; + private static final String PASSWORD = ""; + + private static DatabaseManager instance; + + // Private constructor to prevent instantiation + private DatabaseManager() { + try { + Class.forName("org.postgresql.Driver"); + } catch (ClassNotFoundException e) { + throw new RuntimeException("PostgreSQL JDBC driver not found", e); + } + } + + // Public method to provide access to the singleton instance + public static synchronized DatabaseManager getInstance() { + if (instance == null) { + instance = new DatabaseManager(); + } + return instance; + } + + // Method to get a database connection + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(URL, USER, PASSWORD); + } +} + diff --git a/src/main/java/org/vaadin/example/model/Location.java b/src/main/java/org/vaadin/example/model/Location.java new file mode 100644 index 000000000..ad025a95b --- /dev/null +++ b/src/main/java/org/vaadin/example/model/Location.java @@ -0,0 +1,86 @@ +package org.vaadin.example.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class Location { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String admin1; + private String admin2; + private String admin3; + private double latitude; + private double longitude; + private long userId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAdmin1() { + return admin1; + } + + public void setAdmin1(String admin1) { + this.admin1 = admin1; + } + + public String getAdmin2() { + return admin2; + } + + public void setAdmin2(String admin2) { + this.admin2 = admin2; + } + + public String getAdmin3() { + return admin3; + } + + public void setAdmin3(String admin3) { + this.admin3 = admin3; + } + + 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 long getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } +// Constructors, getters, and setters +} + diff --git a/src/main/java/org/vaadin/example/model/Users.java b/src/main/java/org/vaadin/example/model/Users.java new file mode 100644 index 000000000..ef3a165b3 --- /dev/null +++ b/src/main/java/org/vaadin/example/model/Users.java @@ -0,0 +1,53 @@ +package org.vaadin.example.model; + +public class Users { + + private Long id; + private String username; + private String password; + private String fullName; + + public Users() { + } + + public Users(Long id, String username, String password, String fullName) { + this.id = id; + this.username = username; + this.password = password; + this.fullName = fullName; + } + + // Getters and setters + + 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; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } +} diff --git a/src/main/java/org/vaadin/example/repository/LocationRepository.java b/src/main/java/org/vaadin/example/repository/LocationRepository.java new file mode 100644 index 000000000..e6e892cfa --- /dev/null +++ b/src/main/java/org/vaadin/example/repository/LocationRepository.java @@ -0,0 +1,111 @@ +package org.vaadin.example.repository; + +import org.vaadin.example.config.DatabaseManager; +import org.vaadin.example.model.Location; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class LocationRepository { + + public void save(Location location) { + String sql = "INSERT INTO location (name, admin1, admin2, admin3, latitude, longitude, user_id) VALUES (?, ?, ?, ?, ?, ?, ?)"; + try (Connection connection = DatabaseManager.getInstance().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + + statement.setString(1, location.getName()); + statement.setString(2, location.getAdmin1()); + statement.setString(3, location.getAdmin2()); + statement.setString(4, location.getAdmin3()); + statement.setDouble(5, location.getLatitude()); + statement.setDouble(6, location.getLongitude()); + statement.setDouble(7, location.getUserId()); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Error saving location", e); + } + } + + public Optional findByNameAndCoordinates(long userId, String name, double latitude, double longitude) { + System.out.println(userId + " " + name + " " + latitude + " " + longitude); + String sql = "SELECT * FROM location WHERE user_id = ? AND name = ? AND latitude = ? AND longitude = ?"; + + try (Connection connection = DatabaseManager.getInstance().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + + statement.setLong(1, userId); + statement.setString(2, name); + statement.setDouble(3, latitude); + statement.setDouble(4, longitude); + + ResultSet resultSet = statement.executeQuery(); + + if (resultSet.next()) { + Location location = new Location(); + location.setId(resultSet.getLong("id")); + location.setName(resultSet.getString("name")); + location.setAdmin1(resultSet.getString("admin1")); + location.setAdmin2(resultSet.getString("admin2")); + location.setAdmin3(resultSet.getString("admin3")); + location.setLatitude(resultSet.getDouble("latitude")); + location.setLongitude(resultSet.getDouble("longitude")); + location.setUserId(resultSet.getLong("user_id")); + + return Optional.of(location); + } else { + return Optional.empty(); + } + } catch (SQLException e) { + throw new RuntimeException("Error finding location", e); + } + } + + public List findAllLocationsByUserId(long userId) { + String sql = " SELECT l.* FROM location l WHERE l.user_id = ? "; + List locations = new ArrayList<>(); + + try (Connection connection = DatabaseManager.getInstance().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setLong(1, userId); + ResultSet resultSet = statement.executeQuery(); + + while (resultSet.next()) { + Location location = new Location(); + location.setId(resultSet.getLong("id")); + location.setName(resultSet.getString("name")); + location.setAdmin1(resultSet.getString("admin1")); + location.setAdmin2(resultSet.getString("admin2")); + location.setAdmin3(resultSet.getString("admin3")); + location.setLatitude(resultSet.getDouble("latitude")); + location.setLongitude(resultSet.getDouble("longitude")); + location.setUserId(resultSet.getLong("user_id")); + locations.add(location); + } + } catch (SQLException e) { + throw new RuntimeException("Error finding locations for user ID: " + userId, e); + } + + return locations; + } + + public boolean deleteFavoriteLocation(long id) { + String sql = "DELETE FROM location WHERE id = ? "; + + try (Connection connection = DatabaseManager.getInstance().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setLong(1, id); + + int rowsAffected = statement.executeUpdate(); + return rowsAffected > 0; // Return true if at least one row was affected + } catch (SQLException e) { + throw new RuntimeException("Error deleting favorite location with ID: " + id, e); + } + } + +} diff --git a/src/main/java/org/vaadin/example/repository/UserRepository.java b/src/main/java/org/vaadin/example/repository/UserRepository.java new file mode 100644 index 000000000..0a773b0eb --- /dev/null +++ b/src/main/java/org/vaadin/example/repository/UserRepository.java @@ -0,0 +1,39 @@ +package org.vaadin.example.repository; + +import org.vaadin.example.config.DatabaseManager; +import org.vaadin.example.model.Users; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +public class UserRepository { + + public Optional findByUsernameAndPassword(String username, String password) { + String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; + try (Connection connection = DatabaseManager.getInstance().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + + statement.setString(1, username); + statement.setString(2, password); + + ResultSet resultSet = statement.executeQuery(); + + if (resultSet.next()) { + Users user = new Users(); + user.setId(resultSet.getLong("id")); + user.setUsername(resultSet.getString("username")); + user.setFullName(resultSet.getString("full_name")); + + return Optional.of(user); + } else { + return Optional.empty(); + } + } catch (SQLException e) { + throw new RuntimeException("Error finding user", e); + } + } + +} diff --git a/src/main/java/org/vaadin/example/service/LocationService.java b/src/main/java/org/vaadin/example/service/LocationService.java new file mode 100644 index 000000000..2dfd84124 --- /dev/null +++ b/src/main/java/org/vaadin/example/service/LocationService.java @@ -0,0 +1,262 @@ +package org.vaadin.example.service; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.transaction.Transactional; +import org.vaadin.example.api.WeatherApiClient; +import org.vaadin.example.model.Location; +import org.vaadin.example.repository.LocationRepository; +import org.vaadin.example.util.Tools; +import org.vaadin.example.view.FavoriteLocationsView; + +import java.util.*; +import java.util.logging.Logger; + + +@CdiComponent +public class LocationService { + @Inject + private WeatherApiClient weatherApiClient; + @Inject + private LocationRepository locationRepository; + @Inject + private UserService userService; + + private static final Logger LOGGER = Logger.getLogger(FavoriteLocationsView.class.getName()); + + @Transactional + public Map saveLocationToFavorites(JsonObject locationData) { + Map result = new LinkedHashMap(); + try { + long userId = userService.getLoggedUserId(); + result.put("isSuccess", Boolean.FALSE); + result.put("isExists", Boolean.FALSE); + Location location = createLocationFromJson(locationData); + Optional existingLocation = locationRepository.findByNameAndCoordinates( + userId, location.getName(), location.getLatitude(), location.getLongitude()); + + if (existingLocation.isPresent()) { + result.put("isExists", Boolean.TRUE); + result.put("message", "Location already exists in favorites list!"); + } else { + location.setUserId(userId); + locationRepository.save(location); + result.put("isSuccess", Boolean.TRUE); + result.put("message", "Location added to favorites successfully!"); + } + } catch (Exception e) { + e.printStackTrace(); + result.put("message", "Failed to save location to favorites: " + e.getMessage()); + } + return result; + } + + public List getUserFavoriteLocations(long userId) { + List favoriteLocations = locationRepository.findAllLocationsByUserId(userId); + return favoriteLocations; + } + + public void deleteFavoriteLocation(long id) { + locationRepository.deleteFavoriteLocation(id); + } + + + private Location createLocationFromJson(JsonObject locationData) { + Location location = new Location(); + location.setName(locationData.getString("name")); + location.setAdmin1(locationData.containsKey("admin1") ? locationData.getString("admin1") : ""); + location.setAdmin2(locationData.containsKey("admin2") ? locationData.getString("admin2") : ""); + location.setAdmin3(locationData.containsKey("admin3") ? locationData.getString("admin3") : ""); + location.setLatitude(locationData.getJsonNumber("latitude").doubleValue()); + location.setLongitude(locationData.getJsonNumber("longitude").doubleValue()); + return location; + } + + public JsonObject createJsonFromLocation(Location location) { + return Json.createObjectBuilder() + .add("name", location.getName()) + .add("admin1", location.getAdmin1()) + .add("admin2", location.getAdmin2()) + .add("admin3", location.getAdmin3()) + .add("latitude", location.getLatitude()) + .add("longitude", location.getLongitude()) + .build(); + } + + public String getLocationString(Location location) { + StringBuilder locationBuilder = new StringBuilder(); + locationBuilder.append(location.getName()); + if (!location.getAdmin1().isEmpty()) { + locationBuilder.append(" > ").append(location.getAdmin1()); + } + if (!location.getAdmin2().isEmpty()) { + locationBuilder.append(" > ").append(location.getAdmin2()); + } + if (!location.getAdmin3().isEmpty()) { + locationBuilder.append(" > ").append(location.getAdmin3()); + } + return locationBuilder.toString(); + } + + public String getLocationString(JsonObject location) { + StringBuilder locationBuilder = new StringBuilder(); + locationBuilder.append(location.getString("name").trim()); + + if (location.containsKey("admin1") && !location.getString("admin1").isEmpty()) { + locationBuilder.append(" > ").append(location.getString("admin1").trim()); + } + if (location.containsKey("admin2") && !location.getString("admin2").isEmpty()) { + locationBuilder.append(" > ").append(location.getString("admin2").trim()); + } + if (location.containsKey("admin3") && !location.getString("admin3").isEmpty()) { + locationBuilder.append(" > ").append(location.getString("admin3").trim()); + } + + return locationBuilder.toString(); + } + + public List getWeatherForecastApiData(String type, double latitude,double longitude, String date){ + JsonObject dataForecast = null; + if (type.equals(Tools.WEATHER_FORECAST_TYPE_DAILY)) { + dataForecast = weatherApiClient.getWeatherForecastDaily(latitude, longitude); + }else if (type.equals(Tools.WEATHER_FORECAST_TYPE_HOURLY)) { + dataForecast = weatherApiClient.getWeatherForecastHourly(latitude, longitude, date); + } + return processWeatherApiForecastData(type, dataForecast); + } + + public List processWeatherApiForecastData(String type, JsonObject dailyForecast) { + try { + // Create a list of JsonObjects to hold the tabular data + List weatherData = new ArrayList<>(); + + if (type.equals(Tools.WEATHER_FORECAST_TYPE_DAILY)) { + JsonArray timeArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_DAILY).getJsonArray("time"); + JsonArray temperatureMaxArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_DAILY).getJsonArray("temperature_2m_max"); + JsonArray temperatureMinArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_DAILY).getJsonArray("temperature_2m_min"); + JsonArray windSpeedMaxArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_DAILY).getJsonArray("wind_speed_10m_max"); + JsonArray windSpeedMinArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_DAILY).getJsonArray("wind_speed_10m_min"); + JsonArray rainArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_DAILY).getJsonArray("rain_sum"); + + // Create a list of JsonObjects to hold the tabular data + for (int i = 0; i < timeArray.size(); i++) { + JsonObject data = Json.createObjectBuilder() + .add("time", timeArray.getString(i)) + .add("temperatureMax", temperatureMaxArray.getJsonNumber(i).doubleValue()) + .add("temperatureMin", temperatureMinArray.getJsonNumber(i).doubleValue()) + .add("windSpeedMax", windSpeedMaxArray.getJsonNumber(i).doubleValue()) + .add("windSpeedMin", windSpeedMinArray.getJsonNumber(i).doubleValue()) + .add("rain", rainArray.getJsonNumber(i).doubleValue()) + .build(); + weatherData.add(data); + } + } else if (type.equals(Tools.WEATHER_FORECAST_TYPE_HOURLY)) { + JsonArray timeArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_HOURLY).getJsonArray("time"); + JsonArray temperatureArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_HOURLY).getJsonArray("temperature_2m"); + JsonArray windSpeedArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_HOURLY).getJsonArray("wind_speed_10m"); + JsonArray rainArray = dailyForecast.getJsonObject(Tools.WEATHER_FORECAST_TYPE_HOURLY).getJsonArray("rain"); + + for (int i = 0; i < timeArray.size(); i++) { + JsonObject data = Json.createObjectBuilder() + .add("time", timeArray.getString(i)) + .add("temperature", temperatureArray.getJsonNumber(i).doubleValue()) + .add("windSpeed", windSpeedArray.getJsonNumber(i).doubleValue()) + .add("rain", rainArray.getJsonNumber(i).doubleValue()) + .build(); + weatherData.add(data); + } + } + return weatherData; + } catch (Exception e) { + LOGGER.severe("Error displaying daily forecast: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + public void displayDailyForecast(List dailyData, double latitude, double longitude, String locName) { + try { + // Create a Grid to display the data + Grid grid = new Grid<>(); + grid.setItems(dailyData); + + grid.addColumn(data -> data.getString("time")).setHeader("Date"); + grid.addColumn(data -> + data.getJsonNumber("temperatureMax").doubleValue() + " ~ " + + data.getJsonNumber("temperatureMin").doubleValue() + ).setHeader("Temperature (°C)"); + grid.addColumn(data -> + data.getJsonNumber("windSpeedMax").doubleValue() + " ~ " + + data.getJsonNumber("windSpeedMin").doubleValue() + ).setHeader("Wind Speed (km/h)"); + grid.addColumn(data -> data.getJsonNumber("rain").doubleValue()).setHeader("Rainfall (mm)"); + + grid.addItemClickListener(event -> { + JsonObject location = event.getItem(); + String date = location.getString("time"); + List hourlyData = getWeatherForecastApiData(Tools.WEATHER_FORECAST_TYPE_HOURLY, latitude, longitude, date); + displayHourlyForecast(locName, hourlyData); + }); + grid.addThemeVariants(GridVariant.LUMO_NO_ROW_BORDERS, + GridVariant.LUMO_NO_BORDER, GridVariant.LUMO_ROW_STRIPES); + + VerticalLayout layout = new VerticalLayout(); + layout.add(grid); + + displayWeatherForecastDialog(layout,"Daily Weather forecast: " + locName,"950px"); + + } catch (Exception e) { + LOGGER.severe("Error displaying daily forecast: " + e.getMessage()); + e.printStackTrace(); + } + } + + public void displayHourlyForecast(String locName, List hourlyData) { + try { + // Create a Grid to display the data + Grid grid = new Grid<>(); + grid.setItems(hourlyData); + + grid.addThemeVariants(GridVariant.LUMO_NO_ROW_BORDERS, + GridVariant.LUMO_NO_BORDER, GridVariant.LUMO_ROW_STRIPES); + + grid.addColumn(data -> data.getString("time")).setHeader("Time"); + grid.addColumn(data -> data.getJsonNumber("temperature").doubleValue()).setHeader("Temperature (°C)"); + grid.addColumn(data -> data.getJsonNumber("windSpeed").doubleValue()).setHeader("Wind Speed (km/h)"); + grid.addColumn(data -> data.getJsonNumber("rain").doubleValue()).setHeader("Rainfall (mm)"); + + VerticalLayout layout = new VerticalLayout(); + layout.add(grid); + + displayWeatherForecastDialog(layout,"Hourly Weather forecast: " + locName,"800px"); + + } catch (Exception e) { + LOGGER.severe("Error displaying daily forecast: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void displayWeatherForecastDialog(Component layout,String headerTitle,String width){ + Dialog dialogHourly = new Dialog(); + dialogHourly.setCloseOnEsc(true); + dialogHourly.setCloseOnOutsideClick(true); + dialogHourly.setWidth(width); + dialogHourly.setHeaderTitle(headerTitle); + dialogHourly.add(layout); + dialogHourly.open(); + } + + public List getLocationsByCityName(String value, int cnt) { + return weatherApiClient.getLocationsByCityName(value, cnt); + } +} + diff --git a/src/main/java/org/vaadin/example/service/UserService.java b/src/main/java/org/vaadin/example/service/UserService.java new file mode 100644 index 000000000..87624da6d --- /dev/null +++ b/src/main/java/org/vaadin/example/service/UserService.java @@ -0,0 +1,31 @@ +package org.vaadin.example.service; + +import com.vaadin.flow.server.VaadinSession; +import jakarta.inject.Inject; +import org.vaadin.example.model.Users; +import org.vaadin.example.repository.UserRepository; + +import java.util.Optional; + +public class UserService { + + @Inject + UserRepository userRepository; + + public boolean hasSession(){ + String username = (String) VaadinSession.getCurrent().getAttribute("username"); + return username != null; + } + + public Long getLoggedUserId(){ + return Long.parseLong(VaadinSession.getCurrent().getAttribute("id").toString()); + } + + public String getLoggedUserName(){ + return (String) VaadinSession.getCurrent().getAttribute("fullName"); + } + + public Optional findByUsernameAndPassword(String username, String password){ + return userRepository.findByUsernameAndPassword(username, password); + } +} diff --git a/src/main/java/org/vaadin/example/util/Tools.java b/src/main/java/org/vaadin/example/util/Tools.java new file mode 100644 index 000000000..b06d96d0e --- /dev/null +++ b/src/main/java/org/vaadin/example/util/Tools.java @@ -0,0 +1,10 @@ +package org.vaadin.example.util; + +public class Tools { + public static String WEATHER_FORECAST_TYPE_DAILY = "daily"; + public static String WEATHER_FORECAST_TYPE_HOURLY = "hourly"; + public static String WEATHER_FORECAST_TYPE_DAILY_PARAMS = "wind_speed_10m_max,wind_speed_10m_min,temperature_2m_max,temperature_2m_min,rain_sum"; + public static String WEATHER_FORECAST_TYPE_HOURLY_PARAMS = "temperature_2m,wind_speed_10m,rain"; + public static int WEATHER_FORECAST_DAYS_SEVEN = 7; + public static int WEATHER_FORECAST_DAYS_ONE = 1; +} diff --git a/src/main/java/org/vaadin/example/view/FavoriteLocationsView.java b/src/main/java/org/vaadin/example/view/FavoriteLocationsView.java new file mode 100644 index 000000000..032a0ece9 --- /dev/null +++ b/src/main/java/org/vaadin/example/view/FavoriteLocationsView.java @@ -0,0 +1,163 @@ +package org.vaadin.example.view; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H4; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.server.VaadinSession; +import jakarta.inject.Inject; +import jakarta.json.JsonObject; +import org.vaadin.example.model.Location; +import org.vaadin.example.service.LocationService; +import org.vaadin.example.service.UserService; +import org.vaadin.example.util.Tools; + +import java.util.List; +import java.util.logging.Logger; + +public class FavoriteLocationsView extends VerticalLayout { + + private final UserService userService; + private final LocationService locationService; + + private List favoriteLocations; + private HorizontalLayout buttonLayout; + + private static final Logger LOGGER = Logger.getLogger(FavoriteLocationsView.class.getName()); + + public FavoriteLocationsView(LocationService locationService, + UserService userService) { + this.userService = userService; + this.locationService = locationService; + initFavoriteLocationsView(); + } + + private void initFavoriteLocationsView() { + if (userService.hasSession()) { + loadFavoriteLocationsBySessionUser(); + buildFavoriteLocationsLayout(); + } + } + + private void buildFavoriteLocationsLayout() { + // Create the container for the favorite list + Div favContainer = new Div(); + favContainer.setWidthFull(); + favContainer.addClassName("fav-container"); + + // Create the spans + Span spacer1 = new Span("Favorite List"); + Span spacer2 = new Span(); + spacer2.setWidth("70%"); + Span spacer3 = new Span(new Html("Click on location to view weather forecast")); + spacer3.addClassName("text-align-right"); + + // Create the horizontal layout for the legend + HorizontalLayout favLegend = new HorizontalLayout(); + favLegend.setWidthFull(); + favLegend.add(spacer1, spacer2, spacer3); + + // Create the button layout + buttonLayout = new HorizontalLayout(); + buttonLayout.getStyle().set("flex-wrap", "wrap"); + buttonLayout.setWidthFull(); + + // Add buttons for each favorite location + for (Location location : favoriteLocations) { + Component button = generateFavListAsButton(location); + button.addClassName("cursor-pointer"); + buttonLayout.add(button); + } + + // Add the legend and button layout to the bordered container + VerticalLayout containerLayout = new VerticalLayout(favLegend, buttonLayout); + containerLayout.setPadding(false); + containerLayout.setMargin(false); + favContainer.add(containerLayout); + + // Add the container to the main layout + add(favContainer); + } + + private void loadFavoriteLocationsBySessionUser() { + long userId = userService.getLoggedUserId(); + favoriteLocations = locationService.getUserFavoriteLocations(userId); + } + + private Component generateFavListAsButton(Location location) { + String locName = locationService.getLocationString(location); + String emptySpc = "       "; //added empty space to place delete button properly + // Create the main button + Button mainButton = new Button(new Html("" + locName + emptySpc + "")); + mainButton.setClassName("cursor-pointer"); + mainButton.addClickListener(event -> { + // If click, proceed with fetching weather forecast + onClickFavLocation(location); + }); + + // Create the delete icon button + Button deleteButton = new Button(VaadinIcon.CLOSE.create()); + deleteButton.addClickListener(event -> { + showRemoveConfirmationDialog(locName, location.getId()); + }); + deleteButton.addClassNames("close-button", "cursor-pointer"); + + // Create a layout to hold both the main button and the close icon + HorizontalLayout buttonLayout = new HorizontalLayout(mainButton, deleteButton); + buttonLayout.setAlignItems(Alignment.CENTER); + + return buttonLayout; + } + + private void onClickFavLocation(Location location) { + String locName = locationService.getLocationString(location); + double latitude = location.getLatitude(); + double longitude = location.getLongitude(); + List dailyData = locationService.getWeatherForecastApiData(Tools.WEATHER_FORECAST_TYPE_DAILY, latitude, longitude, ""); + locationService.displayDailyForecast(dailyData, latitude, longitude, locName); + } + + // Method to create a confirmation dialog for removing favorite location + private void showRemoveConfirmationDialog(String locName, long id) { + Html message = new Html("Are you sure you want to remove " + locName + " from favorites?"); + + Dialog confirmDialog = new Dialog(); + confirmDialog.setCloseOnEsc(false); + confirmDialog.setCloseOnOutsideClick(false); + + // Confirmation message + confirmDialog.add(message); + + // Buttons for confirmation + Button confirmButton = new Button("Confirm", event -> { + confirmDialog.close(); + locationService.deleteFavoriteLocation(id); + // Recreate and populate the bordered container after removal + updateFavoriteLocations(); + }); + Button cancelButton = new Button("Cancel", event -> confirmDialog.close()); + HorizontalLayout buttonsLayout = new HorizontalLayout(confirmButton, cancelButton); + confirmDialog.add(buttonsLayout); + + confirmDialog.open(); + } + + public void updateFavoriteLocations() { + loadFavoriteLocationsBySessionUser(); + // Remove all children from the buttonLayout + buttonLayout.removeAll(); + + // Add buttons for each favorite location + for (Location location : favoriteLocations) { + Component button = generateFavListAsButton(location); + button.addClassName("cursor-pointer"); + buttonLayout.add(button); + } + } +} diff --git a/src/main/java/org/vaadin/example/view/LoginView.java b/src/main/java/org/vaadin/example/view/LoginView.java new file mode 100644 index 000000000..01dd731c4 --- /dev/null +++ b/src/main/java/org/vaadin/example/view/LoginView.java @@ -0,0 +1,99 @@ +package org.vaadin.example.view; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinSession; +import jakarta.inject.Inject; +import org.vaadin.example.MainView; +import org.vaadin.example.model.Users; +import org.vaadin.example.service.UserService; + +import java.util.Optional; + +@Route("/WeatherApp/login") +public class LoginView extends VerticalLayout { + @Inject + private UserService userService; + + // Public no-arg constructor + public LoginView() { + initLoginView(); + } + private void initLoginView() { + setSizeFull(); + setPadding(false); + setSpacing(false); + + createHeader(); + + VerticalLayout loginFormLayout = createLoginForm(); + loginFormLayout.setSizeUndefined(); + loginFormLayout.setAlignItems(FlexComponent.Alignment.CENTER); + loginFormLayout.getStyle().set("border", "1px solid var(--lumo-contrast-10pct)"); + loginFormLayout.getStyle().set("border-radius", "8px"); + loginFormLayout.getStyle().set("padding", "var(--lumo-space-m)"); + loginFormLayout.getStyle().set("box-shadow", "0 4px 8px rgba(0, 0, 0, 0.1)"); + loginFormLayout.setWidth("300px"); + + VerticalLayout centeredLayout = new VerticalLayout(loginFormLayout); + centeredLayout.setSizeFull(); + centeredLayout.setAlignItems(FlexComponent.Alignment.CENTER); + centeredLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + + add(centeredLayout); + } + + private void createHeader() { + H3 title = new H3("Weather App"); + title.getStyle().set("margin", "0"); + + HorizontalLayout header = new HorizontalLayout(title); + header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + header.setWidth("100%"); + header.setSpacing(true); + header.getStyle().set("padding", "var(--lumo-space-m)") + .set("box-shadow", "0 4px 8px rgba(0, 0, 0, 0.1)"); + + add(header); + } + + private VerticalLayout createLoginForm() { + TextField usernameField = new TextField("Username"); + PasswordField passwordField = new PasswordField("Password"); + + Button loginButton = new Button("Login", e -> { + String username = usernameField.getValue(); + String password = passwordField.getValue(); + Optional user = userService.findByUsernameAndPassword(username, password); + if (user.isPresent()) { + VaadinSession.getCurrent().setAttribute("id", user.get().getId()); + VaadinSession.getCurrent().setAttribute("username", user.get().getUsername()); + VaadinSession.getCurrent().setAttribute("fullName", user.get().getFullName()); + getUI().ifPresent(ui -> ui.navigate(MainView.class)); + } else { + Span errorMessage = new Span("Invalid username or password"); + add(errorMessage); + } + }); + loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button backButton = new Button("Back to Main Page", e -> { + getUI().ifPresent(ui -> ui.navigate(MainView.class)); + }); + backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + VerticalLayout loginFormLayout = new VerticalLayout(usernameField, passwordField, loginButton, backButton); + loginFormLayout.setAlignItems(FlexComponent.Alignment.CENTER); + loginFormLayout.setSpacing(true); + + return loginFormLayout; + } +} diff --git a/src/main/resources/META-INF/context.xml b/src/main/resources/META-INF/context.xml new file mode 100644 index 000000000..d0969e2e1 --- /dev/null +++ b/src/main/resources/META-INF/context.xml @@ -0,0 +1 @@ + diff --git a/src/main/resources/script.sql b/src/main/resources/script.sql new file mode 100644 index 000000000..810bb8e0d --- /dev/null +++ b/src/main/resources/script.sql @@ -0,0 +1,34 @@ +-- Create database +CREATE DATABASE weatherapp; + + +-- Create table +CREATE TABLE public.users +( + id SERIAL PRIMARY KEY, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + full_name VARCHAR(255) NOT NULL, + CONSTRAINT user_username_unique UNIQUE (username) +); + +-- Insert sample data +INSERT INTO public.users (username, password, full_name) +VALUES ('john_doe', 'password123', 'John Doe'), + ('jane_smith', 'pass456', 'Jane Smith'), + ('admin', 'admin123', 'Admin User'); + + +-- Create table +CREATE TABLE public."location" +( + id serial4 NOT NULL, + "name" varchar(255) NOT NULL, + admin1 varchar(255) NULL, + admin2 varchar(255) NULL, + admin3 varchar(255) NULL, + latitude float8 NOT NULL, + longitude float8 NOT NULL, + user_id int4 NOT NULL, + CONSTRAINT location_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/beans.xml b/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 000000000..1f8521ead --- /dev/null +++ b/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,8 @@ + + + diff --git a/src/main/webapp/WEB-INF/glassfish-web.xml b/src/main/webapp/WEB-INF/glassfish-web.xml new file mode 100644 index 000000000..55a78006f --- /dev/null +++ b/src/main/webapp/WEB-INF/glassfish-web.xml @@ -0,0 +1,4 @@ + + + /WeatherApp + diff --git a/src/main/webapp/WEB-INF/jboss-web.xml b/src/main/webapp/WEB-INF/jboss-web.xml new file mode 100644 index 000000000..49e8d6e80 --- /dev/null +++ b/src/main/webapp/WEB-INF/jboss-web.xml @@ -0,0 +1,4 @@ + + + /WeatherApp + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..b4fd45234 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/main/webapp/icons/icon.png b/src/main/webapp/icons/icon.png new file mode 100644 index 000000000..5eae03a71 Binary files /dev/null and b/src/main/webapp/icons/icon.png differ diff --git a/src/test/java/org/vaadin/example/AbstractViewTest.java b/src/test/java/org/vaadin/example/AbstractViewTest.java new file mode 100644 index 000000000..2588cdea0 --- /dev/null +++ b/src/test/java/org/vaadin/example/AbstractViewTest.java @@ -0,0 +1,139 @@ +package org.vaadin.example; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; + +import com.vaadin.flow.theme.AbstractTheme; +import com.vaadin.testbench.ScreenshotOnFailureRule; +import com.vaadin.testbench.TestBench; +import com.vaadin.testbench.parallel.ParallelTest; + +import io.github.bonigarcia.wdm.WebDriverManager; + +/** + * Base class for TestBench IntegrationTests on chrome. + *

+ * The tests use Chrome driver (see pom.xml for integration-tests profile) to + * run integration tests on a headless Chrome. If a property {@code test.use + * .hub} is set to true, {@code AbstractViewTest} will assume that the + * TestBench test is running in a CI environment. In order to keep the this + * class light, it makes certain assumptions about the CI environment (such + * as available environment variables). It is not advisable to use this class + * as a base class for you own TestBench tests. + *

+ * To learn more about TestBench, visit + * Vaadin TestBench. + */ +public abstract class AbstractViewTest extends ParallelTest { + private static final int SERVER_PORT = 8080; + + private final String route; + private final By rootSelector; + + @Rule + public ScreenshotOnFailureRule rule = new ScreenshotOnFailureRule(this, + false); + + public AbstractViewTest() { + this("", By.tagName("body")); + } + + protected AbstractViewTest(String route, By rootSelector) { + this.route = route; + this.rootSelector = rootSelector; + } + + @BeforeClass + public static void setupClass() { + WebDriverManager.chromedriver().setup(); + } + + @Before + public void setup() throws Exception { + if (isUsingHub()) { + super.setup(); + } else { + ChromeOptions chromeOptions = new ChromeOptions(); + if (Boolean.getBoolean("headless")) { + chromeOptions.addArguments("--headless=new"); + } + setDriver(TestBench.createDriver(new ChromeDriver(chromeOptions))); + } + getDriver().get(getURL(route)); + } + + /** + * Convenience method for getting the root element of the view based on + * the selector passed to the constructor. + * + * @return the root element + */ + protected WebElement getRootElement() { + return findElement(rootSelector); + } + + /** + * Asserts that the given {@code element} is rendered using a theme + * identified by {@code themeClass}. If the theme is not found, JUnit + * assert will fail the test case. + * + * @param element web element to check for the theme + * @param themeClass theme class (such as {@code Lumo.class} + */ + protected void assertThemePresentOnElement( + WebElement element, Class themeClass) { + String themeName = themeClass.getSimpleName().toLowerCase(); + Boolean hasStyle = (Boolean) executeScript("" + + "var styles = Array.from(arguments[0]._template.content" + + ".querySelectorAll('style'))" + + ".filter(style => style.textContent.indexOf('" + + themeName + "') > -1);" + + "return styles.length > 0;", element); + + Assert.assertTrue("Element '" + element.getTagName() + "' should have" + + " had theme '" + themeClass.getSimpleName() + "'.", + hasStyle); + } + + /** + * Property set to true when running on a test hub. + */ + private static final String USE_HUB_PROPERTY = "test.use.hub"; + + /** + * Returns deployment host name concatenated with route. + * + * @return URL to route + */ + private static String getURL(String route) { + return String.format("http://%s:%d/%s", getDeploymentHostname(), + SERVER_PORT, route); + } + + /** + * Returns whether we are using a test hub. This means that the starter + * is running tests in Vaadin's CI environment, and uses TestBench to + * connect to the testing hub. + * + * @return whether we are using a test hub + */ + private static boolean isUsingHub() { + return Boolean.TRUE.toString().equals( + System.getProperty(USE_HUB_PROPERTY)); + } + + /** + * If running on CI, get the host name from environment variable HOSTNAME + * + * @return the host name + */ + private static String getDeploymentHostname() { + return isUsingHub() ? System.getenv("HOSTNAME") : "localhost"; + } +} diff --git a/src/test/java/org/vaadin/example/MainViewIT.java b/src/test/java/org/vaadin/example/MainViewIT.java new file mode 100644 index 000000000..a9ba3101a --- /dev/null +++ b/src/test/java/org/vaadin/example/MainViewIT.java @@ -0,0 +1,66 @@ +package org.vaadin.example; + +import com.vaadin.flow.component.textfield.testbench.TextFieldElement; +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; + +import com.vaadin.flow.component.button.testbench.ButtonElement; +import com.vaadin.flow.component.notification.testbench.NotificationElement; +import com.vaadin.flow.theme.lumo.Lumo; + +public class MainViewIT extends AbstractViewTest { + + @Test + public void clickingButtonShowsNotification() { + Assert.assertFalse($(NotificationElement.class).exists()); + $(ButtonElement.class).first().click(); + Assert.assertTrue($(NotificationElement.class).waitForFirst().isOpen()); + } + + @Test + public void clickingButtonTwiceShowsTwoNotifications() { + Assert.assertFalse($(NotificationElement.class).exists()); + ButtonElement button = $(ButtonElement.class).first(); + button.click(); + button.click(); + Assert.assertEquals(2, $(NotificationElement.class).all().size()); + } + + @Test + public void buttonIsUsingLumoTheme() { + WebElement element = $(ButtonElement.class).first(); + assertThemePresentOnElement(element, Lumo.class); + } + + @Test + public void testClickButtonShowsHelloAnonymousUserNotificationWhenUserNameIsEmpty() { + ButtonElement button = $(ButtonElement.class).first(); + button.click(); + Assert.assertTrue($(NotificationElement.class).exists()); + NotificationElement notification = $(NotificationElement.class).first(); + Assert.assertEquals("Hello anonymous user", notification.getText()); + } + + @Test + public void testClickButtonShowsHelloUserNotificationWhenUserIsNotEmpty() { + TextFieldElement textField = $(TextFieldElement.class).first(); + textField.setValue("Vaadiner"); + ButtonElement button = $(ButtonElement.class).first(); + button.click(); + Assert.assertTrue($(NotificationElement.class).exists()); + NotificationElement notification = $(NotificationElement.class).first(); + Assert.assertEquals("Hello Vaadiner", notification.getText()); + } + + @Test + public void testEnterShowsHelloUserNotificationWhenUserIsNotEmpty() { + TextFieldElement textField = $(TextFieldElement.class).first(); + textField.setValue("Vaadiner"); + textField.sendKeys(Keys.ENTER); + Assert.assertTrue($(NotificationElement.class).exists()); + NotificationElement notification = $(NotificationElement.class).first(); + Assert.assertEquals("Hello Vaadiner", notification.getText()); + } +}